From 98fe7819c6d14f4f464a5cac047f9e82dee5da58 Mon Sep 17 00:00:00 2001 From: Justin Klaassen Date: Wed, 3 Jan 2018 13:39:41 -0500 Subject: Import Android SDK Platform P [4524038] /google/data/ro/projects/android/fetch_artifact \ --bid 4524038 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4524038.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic193bf1cf0cae78d4f2bfb4fbddfe42025c5c3c2 --- .../accessibilityservice/AccessibilityService.java | 8 +- .../AccessibilityServiceInfo.java | 6 +- android/accounts/AccountManager.java | 9 +- android/annotation/CallbackExecutor.java | 41 + android/annotation/Condemned.java | 44 + android/annotation/IntDef.java | 6 +- android/annotation/LongDef.java | 62 + android/annotation/StringDef.java | 5 + android/app/ActionBar.java | 23 +- android/app/Activity.java | 14 +- android/app/ActivityManager.java | 117 +- android/app/ActivityManagerInternal.java | 22 +- android/app/ActivityOptions.java | 16 + android/app/ActivityThread.java | 1006 ++++--- android/app/AlarmManager.java | 22 +- android/app/AppComponentFactory.java | 112 + android/app/AppOpsManager.java | 25 +- android/app/Application.java | 2 +- android/app/ApplicationPackageManager.java | 19 +- android/app/ClientTransactionHandler.java | 46 +- android/app/ContextImpl.java | 216 +- android/app/DialogFragment.java | 4 +- android/app/Fragment.java | 4 +- android/app/FragmentContainer.java | 3 +- android/app/FragmentController.java | 3 +- android/app/FragmentHostCallback.java | 3 +- android/app/FragmentManager.java | 15 +- android/app/FragmentManagerNonConfig.java | 3 +- android/app/FragmentTransaction.java | 10 +- android/app/Instrumentation.java | 28 +- android/app/KeyguardManager.java | 8 +- android/app/LauncherActivity.java | 2 +- android/app/ListFragment.java | 4 +- android/app/LoadedApk.java | 104 +- android/app/LoaderManager.java | 6 +- android/app/LocalActivityManager.java | 13 +- android/app/Notification.java | 77 +- android/app/SharedPreferencesImpl.java | 133 +- android/app/StatusBarManager.java | 11 +- android/app/SystemServiceRegistry.java | 28 +- android/app/UiAutomation.java | 49 +- android/app/UiAutomationConnection.java | 7 +- android/app/UiModeManager.java | 6 +- android/app/VrManager.java | 19 + android/app/WallpaperInfo.java | 60 +- android/app/WallpaperManager.java | 15 +- android/app/WindowConfiguration.java | 17 +- android/app/admin/DeviceAdminReceiver.java | 6 +- android/app/admin/DevicePolicyManager.java | 628 ++++- android/app/admin/DevicePolicyManagerInternal.java | 22 + android/app/admin/PasswordMetrics.java | 7 +- android/app/admin/SecurityLog.java | 67 +- android/app/admin/SystemUpdateInfo.java | 6 +- android/app/admin/SystemUpdatePolicy.java | 9 +- android/app/assist/AssistStructure.java | 10 + android/app/backup/BackupAgent.java | 13 +- android/app/backup/BackupManager.java | 53 + android/app/backup/BackupManagerMonitor.java | 7 +- .../ActivityConfigurationChangeItem.java | 42 +- .../servertransaction/ActivityLifecycleItem.java | 28 +- .../app/servertransaction/ActivityResultItem.java | 44 +- .../app/servertransaction/BaseClientRequest.java | 18 +- .../app/servertransaction/ClientTransaction.java | 94 +- .../servertransaction/ConfigurationChangeItem.java | 44 +- .../app/servertransaction/DestroyActivityItem.java | 44 +- .../app/servertransaction/LaunchActivityItem.java | 178 +- .../app/servertransaction/MoveToDisplayItem.java | 47 +- .../MultiWindowModeChangeItem.java | 45 +- android/app/servertransaction/NewIntentItem.java | 57 +- android/app/servertransaction/ObjectPool.java | 77 + android/app/servertransaction/ObjectPoolItem.java | 29 + .../app/servertransaction/PauseActivityItem.java | 97 +- .../PendingTransactionActions.java | 145 + .../app/servertransaction/PipModeChangeItem.java | 46 +- .../app/servertransaction/ResumeActivityItem.java | 94 +- .../app/servertransaction/StopActivityItem.java | 60 +- .../app/servertransaction/TransactionExecutor.java | 248 ++ .../servertransaction/WindowVisibilityItem.java | 37 +- android/app/slice/Slice.java | 108 +- android/app/slice/SliceItem.java | 72 +- android/app/slice/SliceManager.java | 239 ++ android/app/slice/SliceProvider.java | 93 + android/app/slice/SliceSpec.java | 5 + android/app/timezone/Callback.java | 11 +- android/app/timezone/RulesManager.java | 16 +- android/app/timezone/RulesState.java | 10 +- android/app/usage/AppStandby.java | 83 - android/app/usage/NetworkStats.java | 18 +- android/app/usage/StorageStatsManager.java | 10 + android/app/usage/UsageEvents.java | 7 +- android/app/usage/UsageStatsManager.java | 149 +- android/app/usage/UsageStatsManagerInternal.java | 2 +- android/appwidget/AppWidgetManagerInternal.java | 39 + android/appwidget/AppWidgetProviderInfo.java | 61 +- android/arch/core/executor/ArchTaskExecutor.java | 1 + android/arch/core/executor/TaskExecutor.java | 7 +- android/arch/lifecycle/ComputableLiveData.java | 139 +- android/arch/lifecycle/Lifecycle.java | 1 + android/arch/lifecycle/LifecycleRegistry.java | 1 + android/arch/lifecycle/LiveData.java | 410 ++- .../arch/lifecycle/LiveDataReactiveStreams.java | 14 +- .../lifecycle/LiveDataReactiveStreamsTest.java | 5 +- android/arch/lifecycle/ViewModelProviderTest.java | 16 +- android/arch/lifecycle/ViewModelProvidersTest.java | 40 + android/arch/lifecycle/ViewModelStores.java | 6 + android/arch/paging/ContiguousDataSource.java | 30 +- android/arch/paging/ContiguousPagedList.java | 6 +- android/arch/paging/DataSource.java | 57 +- android/arch/paging/ItemKeyedDataSource.java | 337 +++ android/arch/paging/KeyedDataSource.java | 260 -- android/arch/paging/ListDataSource.java | 19 +- android/arch/paging/LivePagedListBuilder.java | 18 +- android/arch/paging/LivePagedListProvider.java | 90 +- android/arch/paging/PageKeyedDataSource.java | 391 +++ android/arch/paging/PagedList.java | 42 +- android/arch/paging/PositionalDataSource.java | 349 ++- android/arch/paging/TiledDataSource.java | 14 +- android/arch/paging/TiledPagedList.java | 5 +- .../paging/integration/testapp/ItemDataSource.java | 34 +- .../arch/persistence/db/SupportSQLiteProgram.java | 6 +- .../db/framework/FrameworkSQLiteDatabase.java | 6 +- .../db/framework/FrameworkSQLiteProgram.java | 2 +- .../db/framework/FrameworkSQLiteStatement.java | 38 +- android/arch/persistence/room/Database.java | 2 +- android/arch/persistence/room/RoomDatabase.java | 25 +- android/arch/persistence/room/RoomSQLiteQuery.java | 2 +- .../room/integration/testapp/TestDatabase.java | 23 + .../room/integration/testapp/dao/PetDao.java | 6 + .../room/integration/testapp/dao/UserDao.java | 9 + .../room/integration/testapp/dao/UserPetDao.java | 4 + .../database/LastNameAscCustomerDataSource.java | 26 +- .../integration/testapp/test/ConstructorTest.java | 62 +- .../testapp/test/PojoWithRelationTest.java | 90 + .../testapp/test/SimpleEntityReadWriteTest.java | 48 +- .../room/integration/testapp/test/TestUtil.java | 10 + .../room/integration/testapp/vo/Day.java | 27 + .../integration/testapp/vo/NameAndLastName.java | 36 + .../room/integration/testapp/vo/Pet.java | 27 +- .../room/integration/testapp/vo/PetWithToyIds.java | 68 + .../room/integration/testapp/vo/User.java | 25 +- .../testapp/vo/UserAndPetAdoptionDates.java | 71 + android/bluetooth/BluetoothAdapter.java | 466 +-- android/bluetooth/BluetoothHeadset.java | 138 - android/bluetooth/BluetoothHidDevice.java | 200 +- .../BluetoothHidDeviceAppConfiguration.java | 79 - .../BluetoothHidDeviceAppQosSettings.java | 37 +- .../BluetoothHidDeviceAppSdpSettings.java | 25 +- android/bluetooth/BluetoothHidDeviceCallback.java | 83 +- android/bluetooth/BluetoothPbap.java | 153 +- android/bluetooth/BluetoothProfile.java | 26 +- .../bluetooth/le/PeriodicAdvertisingReport.java | 2 +- android/companion/DeviceFilter.java | 6 +- android/content/AsyncTaskLoader.java | 3 +- android/content/ContentResolver.java | 11 +- android/content/Context.java | 99 +- android/content/ContextWrapper.java | 8 +- android/content/CursorLoader.java | 3 +- android/content/Intent.java | 57 +- android/content/Loader.java | 5 +- android/content/QuickViewConstants.java | 11 +- android/content/ServiceConnection.java | 17 + android/content/pm/ActivityInfo.java | 47 +- android/content/pm/ApplicationInfo.java | 35 +- android/content/pm/AuxiliaryResolveInfo.java | 4 +- android/content/pm/InstantAppResolveInfo.java | 36 +- android/content/pm/LauncherApps.java | 37 +- android/content/pm/PackageInfo.java | 95 +- android/content/pm/PackageInfoLite.java | 28 +- android/content/pm/PackageList.java | 74 + android/content/pm/PackageManager.java | 29 +- android/content/pm/PackageManagerInternal.java | 37 + android/content/pm/PackageParser.java | 320 +-- android/content/pm/PermissionInfo.java | 20 + android/content/pm/RegisteredServicesCache.java | 2 +- android/content/pm/SharedLibraryInfo.java | 31 +- android/content/pm/ShortcutInfo.java | 24 +- android/content/pm/ShortcutManager.java | 207 +- android/content/pm/ShortcutServiceInternal.java | 3 + android/content/pm/VersionedPackage.java | 28 +- .../content/pm/crossprofile/CrossProfileApps.java | 73 +- android/content/pm/dex/ArtManager.java | 156 + android/content/res/AssetFileDescriptor.java | 4 +- android/content/res/Configuration.java | 37 +- android/content/res/GradientColor.java | 7 +- android/content/res/XmlResourceParser.java | 13 +- .../sqlite/SQLiteCompatibilityWalFlags.java | 134 + android/database/sqlite/SQLiteConnection.java | 6 +- android/database/sqlite/SQLiteConnectionPool.java | 6 + android/database/sqlite/SQLiteDatabase.java | 4 + android/graphics/BitmapFactory_Delegate.java | 12 +- android/graphics/Bitmap_Delegate.java | 3 +- android/graphics/ImageDecoder.java | 665 +++++ android/graphics/Point.java | 16 + android/graphics/PostProcess.java | 91 + android/graphics/drawable/RippleComponent.java | 8 +- android/graphics/drawable/RippleDrawable.java | 9 +- android/graphics/drawable/RippleForeground.java | 96 +- android/graphics/drawable/VectorDrawable.java | 9 + .../graphics/drawable/VectorDrawable_Delegate.java | 15 +- .../graphics/perftests/PaintMeasureTextTest.java | 4 +- android/hardware/HardwareBuffer.java | 13 +- android/hardware/SensorAdditionalInfo.java | 11 +- android/hardware/SensorDirectChannel.java | 17 +- .../hardware/camera2/CameraAccessException.java | 20 +- .../hardware/camera2/CameraCharacteristics.java | 105 + android/hardware/camera2/CameraDevice.java | 21 + android/hardware/camera2/CameraMetadata.java | 16 + android/hardware/camera2/CaptureRequest.java | 135 +- android/hardware/camera2/CaptureResult.java | 24 + .../camera2/impl/CameraCaptureSessionImpl.java | 3 +- .../hardware/camera2/impl/CameraDeviceImpl.java | 70 +- .../camera2/impl/ICameraDeviceUserWrapper.java | 6 +- .../camera2/legacy/CameraDeviceUserShim.java | 2 +- .../camera2/params/OutputConfiguration.java | 3 + .../camera2/params/SessionConfiguration.java | 200 ++ .../hardware/display/BrightnessChangeEvent.java | 20 +- .../hardware/display/BrightnessConfiguration.java | 175 ++ android/hardware/display/DisplayManager.java | 27 +- android/hardware/display/DisplayManagerGlobal.java | 18 +- android/hardware/input/InputManager.java | 6 +- android/hardware/location/ContextHubInfo.java | 6 +- android/hardware/location/ContextHubManager.java | 132 +- .../hardware/location/ContextHubTransaction.java | 170 +- android/hardware/location/NanoAppFilter.java | 9 +- android/hardware/location/NanoAppInstanceInfo.java | 167 +- android/hardware/radio/RadioManager.java | 3 +- android/inputmethodservice/InputMethodService.java | 86 +- android/inputmethodservice/KeyboardView.java | 9 +- android/location/GnssMeasurementsEvent.java | 8 +- android/location/LocalListenerHelper.java | 9 +- android/location/LocationManager.java | 41 +- android/location/LocationRequest.java | 128 +- android/media/AudioAttributes.java | 7 +- android/media/AudioDeviceInfo.java | 62 + android/media/AudioManager.java | 105 +- android/media/AudioRecord.java | 55 +- android/media/AudioTrack.java | 55 +- android/media/MediaDrm.java | 4 +- android/media/MediaMetadata.java | 47 +- android/media/MediaMetadataRetriever.java | 25 +- android/media/MediaPlayer.java | 33 +- .../media/NativeRoutingEventHandlerDelegate.java | 51 + android/media/session/PlaybackState.java | 3 +- android/media/tv/TvContract.java | 8 +- android/mtp/MtpDatabase.java | 1333 ++++----- android/mtp/MtpPropertyGroup.java | 404 +-- android/mtp/MtpPropertyList.java | 95 +- android/mtp/MtpStorage.java | 18 +- android/mtp/MtpStorageManager.java | 1210 ++++++++ android/net/ConnectivityManager.java | 2 +- android/net/IpSecAlgorithm.java | 72 +- android/net/IpSecManager.java | 59 +- android/net/IpSecTransform.java | 22 +- android/net/MacAddress.java | 276 +- android/net/TrafficStats.java | 172 +- android/net/ip/ConnectivityPacketTracker.java | 6 +- android/net/ip/IpClient.java | 10 + android/net/ip/IpNeighborMonitor.java | 236 ++ android/net/ip/IpReachabilityMonitor.java | 386 +-- android/net/metrics/DefaultNetworkEvent.java | 2 +- android/net/metrics/WakeupStats.java | 7 +- android/net/netlink/NetlinkSocket.java | 134 +- android/net/netlink/StructNdMsg.java | 7 +- android/net/util/BlockingSocketReader.java | 249 -- android/net/util/PacketReader.java | 251 ++ android/net/wifi/WifiInfo.java | 151 +- android/net/wifi/WifiLinkLayerStats.java | 211 -- android/net/wifi/WifiManager.java | 55 +- android/net/wifi/WifiScanner.java | 30 +- android/net/wifi/aware/PeerHandle.java | 2 - android/net/wifi/aware/PublishConfig.java | 9 +- android/net/wifi/aware/SubscribeConfig.java | 12 +- android/net/wifi/aware/WifiAwareManager.java | 4 +- .../net/wifi/hotspot2/ProvisioningCallback.java | 56 +- android/net/wifi/rtt/RangingRequest.java | 236 +- android/net/wifi/rtt/RangingResult.java | 36 +- android/net/wifi/rtt/ResponderConfig.java | 474 ++++ android/net/wifi/rtt/WifiRttManager.java | 2 +- android/os/BatteryStats.java | 152 +- android/os/Binder.java | 61 +- android/os/Build.java | 44 +- android/os/Debug.java | 8 +- android/os/Environment.java | 79 +- android/os/HandlerExecutor.java | 45 + android/os/HardwarePropertiesManager.java | 16 +- android/os/Message.java | 20 +- android/os/MessageQueue.java | 6 +- android/os/PowerManager.java | 33 +- android/os/PowerManagerInternal.java | 2 +- android/os/RemoteCallbackList.java | 17 + android/os/ServiceManager.java | 105 +- android/os/StatsLogEventWrapper.java | 11 +- android/os/UserManager.java | 122 +- android/os/UserManagerInternal.java | 3 +- android/os/VintfObject.java | 14 +- android/os/WorkSource.java | 434 ++- android/os/connectivity/CellularBatteryStats.java | 242 ++ android/os/storage/StorageManager.java | 8 +- android/os/storage/StorageVolume.java | 43 +- android/os/storage/VolumeInfo.java | 20 +- android/print/PrintAttributes.java | 11 +- android/print/PrintDocumentInfo.java | 7 +- android/print/PrintJobInfo.java | 11 +- android/print/PrinterInfo.java | 7 +- android/privacy/DifferentialPrivacyConfig.java | 34 + android/privacy/DifferentialPrivacyEncoder.java | 78 + .../LongitudinalReportingConfig.java | 107 + .../LongitudinalReportingEncoder.java | 170 ++ android/privacy/internal/rappor/RapporConfig.java | 87 + android/privacy/internal/rappor/RapporEncoder.java | 125 + android/provider/AlarmClock.java | 1 - android/provider/CallLog.java | 17 +- android/provider/ContactsContract.java | 42 + android/provider/FontsContract.java | 16 +- android/provider/MediaStore.java | 9 - android/provider/Settings.java | 172 +- android/provider/Telephony.java | 6 + android/provider/VoicemailContract.java | 18 +- android/security/AttestedKeyPair.java | 75 + android/security/Credentials.java | 34 +- android/security/KeyStore.java | 24 +- .../keymaster/KeyAttestationPackageInfo.java | 10 +- .../keystore/AndroidKeyStoreKeyGeneratorSpi.java | 2 +- .../security/keystore/AndroidKeyStoreProvider.java | 90 +- .../AndroidKeyStoreSecretKeyFactorySpi.java | 5 +- android/security/keystore/AndroidKeyStoreSpi.java | 55 +- android/security/keystore/AttestationUtils.java | 53 +- .../security/keystore/KeyAttestationException.java | 46 + android/security/keystore/KeyGenParameterSpec.java | 38 +- android/security/keystore/KeyProperties.java | 34 +- .../keystore/ParcelableKeyGenParameterSpec.java | 185 ++ .../KeyDerivationParameters.java | 112 + .../recoverablekeystore/KeyEntryRecoveryData.java | 90 + .../recoverablekeystore/KeyStoreRecoveryData.java | 115 + .../KeyStoreRecoveryMetadata.java | 180 ++ .../RecoverableKeyStoreLoader.java | 467 +++ android/service/autofill/AutofillService.java | 45 +- android/service/autofill/Dataset.java | 21 +- android/service/autofill/EditDistanceScorer.java | 97 + android/service/autofill/FieldClassification.java | 168 ++ android/service/autofill/FieldsDetection.java | 127 - android/service/autofill/FillEventHistory.java | 148 +- android/service/autofill/FillRequest.java | 19 +- android/service/autofill/FillResponse.java | 197 +- android/service/autofill/InternalScorer.java | 40 + android/service/autofill/SaveInfo.java | 32 +- android/service/autofill/SaveRequest.java | 5 +- android/service/autofill/Scorer.java | 28 + android/service/autofill/UserData.java | 307 ++ android/service/autofill/Validators.java | 8 +- android/service/carrier/CarrierService.java | 4 +- android/service/euicc/EuiccService.java | 26 + android/service/notification/Condition.java | 7 +- .../notification/NotificationListenerService.java | 7 +- android/service/notification/ScheduleCalendar.java | 177 ++ android/service/notification/ZenModeConfig.java | 58 +- .../persistentdata/PersistentDataBlockManager.java | 8 +- .../service/settings/suggestions/Suggestion.java | 2 +- android/service/trust/TrustAgentService.java | 18 +- android/service/voice/AlwaysOnHotwordDetector.java | 30 +- android/service/wallpaper/WallpaperService.java | 82 +- android/speech/tts/TextToSpeech.java | 11 +- android/support/LibraryVersions.java | 16 +- android/support/Version.java | 41 +- android/support/animation/AnimationHandler.java | 4 +- android/support/animation/FloatPropertyCompat.java | 4 +- android/support/annotation/IntDef.java | 4 +- android/support/annotation/LongDef.java | 60 + android/support/car/drawer/CarDrawerActivity.java | 152 - android/support/car/drawer/CarDrawerAdapter.java | 182 -- .../support/car/drawer/CarDrawerController.java | 335 --- .../car/drawer/DrawerItemClickListener.java | 29 - .../support/car/drawer/DrawerItemViewHolder.java | 87 - android/support/car/utils/ColumnCalculator.java | 141 - android/support/car/widget/CarItemAnimator.java | 70 - android/support/car/widget/CarRecyclerView.java | 142 - android/support/car/widget/ColumnCardView.java | 115 - android/support/car/widget/DayNightStyle.java | 66 - android/support/car/widget/PagedLayoutManager.java | 1687 ----------- android/support/car/widget/PagedListView.java | 996 ------- android/support/car/widget/PagedScrollBarView.java | 264 -- android/support/checkapi/UpdateApiTask.java | 6 - .../support/design/widget/CoordinatorLayout.java | 1 + .../graphics/drawable/VectorDrawableCompat.java | 13 +- android/support/media/ExifInterface.java | 6 +- android/support/media/ExifInterfaceTest.java | 898 ++++++ android/support/media/tv/BasePreviewProgram.java | 2 - android/support/media/tv/Channel.java | 2 - android/support/media/tv/ChannelLogoUtilsTest.java | 99 + android/support/media/tv/ChannelTest.java | 250 ++ android/support/media/tv/PreviewProgram.java | 2 - android/support/media/tv/PreviewProgramTest.java | 387 +++ android/support/media/tv/Program.java | 2 - android/support/media/tv/ProgramTest.java | 274 ++ android/support/media/tv/TvContractUtilsTest.java | 159 ++ android/support/media/tv/Utils.java | 28 + android/support/media/tv/WatchNextProgram.java | 2 - android/support/media/tv/WatchNextProgramTest.java | 365 +++ .../mediacompat/testlib/util/PollingCheck.java | 6 +- .../support/mediacompat/testlib/util/TestUtil.java | 4 +- android/support/text/emoji/EmojiCompat.java | 68 +- android/support/text/emoji/EmojiMetadata.java | 5 +- android/support/text/emoji/EmojiProcessor.java | 72 +- android/support/text/emoji/MetadataListReader.java | 3 +- android/support/text/emoji/MetadataRepo.java | 3 +- android/support/text/emoji/widget/EmojiButton.java | 4 +- .../support/text/emoji/widget/EmojiEditText.java | 4 +- .../text/emoji/widget/EmojiExtractEditText.java | 4 +- android/support/transition/ArcMotionTest.java | 203 ++ android/support/transition/AutoTransitionTest.java | 116 + android/support/transition/BaseTest.java | 35 + android/support/transition/BaseTransitionTest.java | 134 + android/support/transition/ChangeBoundsTest.java | 102 + .../support/transition/ChangeClipBoundsTest.java | 121 + .../transition/ChangeImageTransformTest.java | 302 ++ android/support/transition/ChangeScrollTest.java | 76 + .../support/transition/ChangeTransformTest.java | 124 + .../support/transition/CheckCalledRunnable.java | 35 + android/support/transition/ExplodeTest.java | 166 ++ android/support/transition/FadeTest.java | 275 ++ .../support/transition/FragmentTransitionTest.java | 226 ++ android/support/transition/PathMotionTest.java | 62 + .../support/transition/PatternPathMotionTest.java | 77 + android/support/transition/PropagationTest.java | 101 + android/support/transition/SceneTest.java | 127 + android/support/transition/SlideBadEdgeTest.java | 78 + .../support/transition/SlideDefaultEdgeTest.java | 39 + android/support/transition/SlideEdgeTest.java | 273 ++ android/support/transition/SyncRunnable.java | 40 + .../support/transition/SyncTransitionListener.java | 87 + android/support/transition/TransitionActivity.java | 40 + .../support/transition/TransitionInflaterTest.java | 286 ++ .../support/transition/TransitionManagerTest.java | 183 ++ android/support/transition/TransitionSetTest.java | 123 + android/support/transition/TransitionTest.java | 442 +++ android/support/transition/VisibilityTest.java | 200 ++ android/support/v13/app/ActivityCompat.java | 26 +- android/support/v13/app/FragmentCompat.java | 50 +- android/support/v13/app/FragmentPagerAdapter.java | 45 + .../support/v13/app/FragmentStatePagerAdapter.java | 42 + android/support/v13/app/FragmentTabHost.java | 51 +- .../support/v17/leanback/app/BrowseFragment.java | 3 + .../v17/leanback/app/BrowseSupportFragment.java | 3 + .../v17/leanback/widget/GridLayoutManager.java | 63 +- .../v17/leanback/widget/WindowAlignment.java | 6 +- android/support/v4/app/ActivityCompat.java | 15 + android/support/v4/app/Fragment.java | 25 +- android/support/v4/app/NotificationCompat.java | 62 +- .../support/v4/app/NotificationCompatBuilder.java | 15 +- .../support/v4/app/NotificationManagerCompat.java | 4 +- .../v4/content/res/FontResourcesParserCompat.java | 26 +- android/support/v4/graphics/TypefaceCompat.java | 3 +- .../v4/graphics/TypefaceCompatApi24Impl.java | 3 +- .../v4/graphics/TypefaceCompatApi26Impl.java | 14 +- .../support/v4/graphics/drawable/IconCompat.java | 1 + .../fingerprint/FingerprintManagerCompat.java | 4 - .../v4/media/session/MediaControllerCompat.java | 2 + .../v4/media/session/PlaybackStateCompat.java | 5 +- .../support/v4/provider/FontsContractCompat.java | 5 +- .../support/v4/view/ViewConfigurationCompat.java | 3 - android/support/v4/widget/DrawerLayout.java | 3 +- android/support/v4/widget/NestedScrollView.java | 37 +- android/support/v7/app/AlertController.java | 53 +- android/support/v7/app/AlertDialog.java | 52 +- android/support/v7/graphics/BucketTests.java | 181 ++ android/support/v7/graphics/ConsistencyTest.java | 58 + android/support/v7/graphics/MaxColorsTest.java | 56 + android/support/v7/graphics/SwatchTests.java | 110 + android/support/v7/graphics/TestUtils.java | 41 + android/support/v7/media/MediaRouter.java | 10 +- android/support/v7/util/SortedListTest.java | 24 +- .../support/v7/view/menu/CascadingMenuPopup.java | 3 +- android/support/v7/widget/ActionMenuView.java | 4 +- android/support/v7/widget/AdapterHelperTest.java | 124 +- .../v7/widget/AppCompatTextViewAutoSizeHelper.java | 10 +- android/support/v7/widget/ButtonBarLayout.java | 8 +- android/support/v7/widget/CardView.java | 93 +- android/support/v7/widget/LinearLayoutManager.java | 45 +- android/support/v7/widget/ListPopupWindow.java | 2 +- android/support/v7/widget/OrientationHelper.java | 10 +- android/support/v7/widget/RecyclerView.java | 176 +- .../v7/widget/StaggeredGridLayoutManager.java | 4 +- android/support/v7/widget/TooltipCompat.java | 35 +- android/support/v7/widget/ViewInfoStoreTest.java | 11 +- .../support/v7/widget/helper/ItemTouchHelper.java | 2 +- .../support/wear/ambient/AmbientDelegateTest.java | 94 + android/support/wear/ambient/AmbientMode.java | 2 + .../wear/ambient/AmbientModeResumeTest.java | 48 + .../ambient/AmbientModeResumeTestActivity.java | 29 + .../support/wear/ambient/AmbientModeSupport.java | 281 ++ android/support/wear/ambient/AmbientModeTest.java | 88 + .../wear/ambient/AmbientModeTestActivity.java | 62 + .../support/wear/utils/MetadataTestActivity.java | 37 + .../support/wear/widget/BoxInsetLayoutTest.java | 364 +++ .../CircularProgressLayoutControllerTest.java | 119 + .../wear/widget/CircularProgressLayoutTest.java | 109 + .../support/wear/widget/LayoutTestActivity.java | 37 + .../support/wear/widget/RoundedDrawableTest.java | 147 + android/support/wear/widget/ScrollManagerTest.java | 202 ++ .../wear/widget/SwipeDismissFrameLayoutTest.java | 460 +++ .../SwipeDismissFrameLayoutTestActivity.java | 82 + .../widget/SwipeDismissPreferenceFragment.java | 105 + .../widget/WearableLinearLayoutManagerTest.java | 160 ++ .../wear/widget/WearableRecyclerViewTest.java | 226 ++ .../widget/WearableRecyclerViewTestActivity.java | 64 + .../wear/widget/drawer/DrawerTestActivity.java | 198 ++ .../drawer/WearableDrawerLayoutEspressoTest.java | 668 +++++ android/support/wear/widget/util/ArcSwipe.java | 176 ++ android/support/wear/widget/util/ArcSwipeTest.java | 70 + .../support/wear/widget/util/AsyncViewActions.java | 71 + .../wear/widget/util/MoreViewAssertions.java | 207 ++ android/support/wear/widget/util/WakeLockRule.java | 57 + android/system/OsConstants.java | 1 + android/telecom/Call.java | 30 +- android/telecom/Connection.java | 32 +- android/telecom/ConnectionRequest.java | 5 - android/telecom/ConnectionService.java | 130 +- android/telecom/ConnectionServiceAdapter.java | 13 + .../telecom/ConnectionServiceAdapterServant.java | 9 + android/telecom/InCallService.java | 12 + android/telecom/Phone.java | 7 + android/telecom/PhoneAccount.java | 3 + android/telecom/RemoteConnectionService.java | 3 + android/telecom/TelecomManager.java | 18 +- android/telephony/CarrierConfigManager.java | 80 +- android/telephony/CellIdentityGsm.java | 2 +- android/telephony/CellIdentityLte.java | 2 +- android/telephony/CellIdentityWcdma.java | 2 +- android/telephony/DisconnectCause.java | 40 + android/telephony/MbmsDownloadSession.java | 59 +- android/telephony/NetworkScan.java | 76 +- android/telephony/NetworkScanRequest.java | 168 +- android/telephony/PhoneStateListener.java | 4 +- android/telephony/RadioAccessSpecifier.java | 75 +- android/telephony/RadioNetworkConstants.java | 1 - android/telephony/SmsManager.java | 252 +- android/telephony/SmsMessage.java | 34 +- android/telephony/TelephonyManager.java | 460 ++- android/telephony/TelephonyScanManager.java | 8 +- android/telephony/data/DataCallResponse.java | 267 ++ android/telephony/data/InterfaceAddress.java | 127 + android/telephony/euicc/EuiccManager.java | 87 +- android/telephony/ims/feature/ImsFeature.java | 5 + android/telephony/ims/feature/MMTelFeature.java | 5 + android/telephony/ims/feature/RcsFeature.java | 5 + .../ims/internal/ImsCallSessionListener.java | 364 +++ android/telephony/ims/internal/ImsService.java | 339 +++ android/telephony/ims/internal/SmsImplBase.java | 260 ++ .../internal/feature/CapabilityChangeRequest.java | 197 ++ .../telephony/ims/internal/feature/ImsFeature.java | 462 +++ .../ims/internal/feature/MmTelFeature.java | 495 ++++ .../telephony/ims/internal/feature/RcsFeature.java | 59 + .../ims/internal/stub/ImsConfigImplBase.java | 173 ++ .../ims/internal/stub/ImsFeatureConfiguration.java | 147 + .../ims/internal/stub/ImsRegistrationImplBase.java | 276 ++ android/telephony/ims/stub/ImsConfigImplBase.java | 265 +- android/telephony/ims/stub/ImsUtImplBase.java | 18 + .../telephony/ims/stub/ImsUtListenerImplBase.java | 7 + android/telephony/mbms/ServiceInfo.java | 4 +- android/test/mock/MockContext.java | 11 +- android/test/mock/MockPackageManager.java | 9 + android/text/AutoGrowArray.java | 374 +++ android/text/DynamicLayout.java | 43 +- android/text/FontConfig.java | 6 +- android/text/Layout.java | 55 +- android/text/MeasuredText.java | 724 +++-- android/text/MeasuredText_Delegate.java | 178 ++ android/text/PremeasuredText.java | 272 ++ android/text/StaticLayout.java | 235 +- android/text/StaticLayoutPerfTest.java | 223 +- android/text/StaticLayout_Delegate.java | 76 +- android/text/TextUtils.java | 67 +- android/transition/Visibility.java | 5 +- android/util/FeatureFlagUtils.java | 22 +- android/util/KeyValueListParser.java | 28 + android/util/Log.java | 295 +- android/util/LruCache.java | 25 +- android/util/Pools.java | 13 +- android/util/SparseBooleanArray.java | 6 +- android/util/StatsLog.java | 70 + android/util/StatsManager.java | 29 +- android/util/apk/ApkSignatureSchemeV2Verifier.java | 890 +----- android/util/apk/ApkSignatureSchemeV3Verifier.java | 558 ++++ android/util/apk/ApkSignatureVerifier.java | 381 +++ android/util/apk/ApkSigningBlockUtils.java | 663 +++++ android/util/apk/SignatureNotFoundException.java | 34 + android/util/apk/VerbatimX509Certificate.java | 38 + android/util/apk/WrappedX509Certificate.java | 175 ++ android/util/jar/StrictJarVerifier.java | 29 +- android/view/Choreographer.java | 14 + android/view/Display.java | 8 +- android/view/DisplayCutout.java | 145 +- android/view/FrameInfo.java | 4 +- android/view/GestureDetector.java | 289 +- android/view/IWindowManagerImpl.java | 6 - android/view/Surface.java | 15 +- android/view/SurfaceControl.java | 180 +- android/view/SurfaceView.java | 1142 +------- android/view/ThreadedRenderer.java | 29 +- android/view/View.java | 211 +- android/view/ViewConfiguration.java | 12 + android/view/ViewRootImpl.java | 324 ++- android/view/View_Delegate.java | 49 + android/view/WindowInsets.java | 31 +- android/view/WindowManager.java | 49 +- .../AccessibilityInteractionClient.java | 79 +- .../view/accessibility/AccessibilityManager.java | 925 +----- .../view/accessibility/AccessibilityNodeInfo.java | 11 +- .../AccessibilityRequestPreparer.java | 7 +- .../accessibility/AccessibilityWindowInfo.java | 38 +- android/view/autofill/AutofillManager.java | 151 +- android/view/autofill/AutofillPopupWindow.java | 11 +- android/view/autofill/AutofillValue.java | 2 +- android/view/autofill/Helper.java | 51 +- android/view/inputmethod/InputMethodInfo.java | 28 +- android/view/inputmethod/InputMethodManager.java | 206 +- .../inputmethod/InputMethodManagerInternal.java | 7 + android/view/textclassifier/EntityConfidence.java | 57 +- .../view/textclassifier/TextClassification.java | 394 ++- android/view/textclassifier/TextClassifier.java | 99 +- .../view/textclassifier/TextClassifierImpl.java | 163 +- android/view/textclassifier/TextLinks.java | 46 +- android/view/textclassifier/TextSelection.java | 72 +- .../logging/SmartSelectionEventTracker.java | 37 +- android/view/textservice/TextServicesManager.java | 200 +- android/webkit/FilterMethods.java | 28 + android/webkit/SafeBrowsingResponse.java | 35 +- android/webkit/SingleClassAndMethod.java | 23 + android/webkit/TracingConfig.java | 203 ++ android/webkit/TracingController.java | 126 + android/webkit/TracingFileOutputStream.java | 63 + android/webkit/UserPackage.java | 2 +- android/webkit/WebKitTypeAsMethodParameter.java | 27 + android/webkit/WebKitTypeAsMethodReturn.java | 26 + android/webkit/WebSettings.java | 21 +- android/webkit/WebView.java | 2967 +------------------- android/webkit/WebViewClient.java | 540 +--- android/webkit/WebViewDelegate.java | 7 + android/webkit/WebViewFactory.java | 57 +- android/webkit/WebViewFactoryProvider.java | 8 + android/webkit/WebViewLibraryLoader.java | 9 +- android/widget/DatePicker.java | 5 +- android/widget/EditText.java | 5 + android/widget/Editor.java | 123 +- android/widget/GridLayout.java | 10 +- android/widget/GridView.java | 7 +- android/widget/LinearLayout.java | 13 +- android/widget/Magnifier.java | 2 +- android/widget/NumberPicker.java | 13 +- android/widget/SelectionActionModeHelper.java | 70 +- android/widget/TextClock.java | 24 +- android/widget/TextView.java | 39 +- android/widget/TextViewMetrics.java | 25 - android/widget/TimePicker.java | 5 +- android/widget/Toast.java | 8 +- 655 files changed, 43212 insertions(+), 20392 deletions(-) create mode 100644 android/annotation/CallbackExecutor.java create mode 100644 android/annotation/Condemned.java create mode 100644 android/annotation/LongDef.java create mode 100644 android/app/AppComponentFactory.java create mode 100644 android/app/servertransaction/ObjectPool.java create mode 100644 android/app/servertransaction/ObjectPoolItem.java create mode 100644 android/app/servertransaction/PendingTransactionActions.java create mode 100644 android/app/servertransaction/TransactionExecutor.java create mode 100644 android/app/slice/SliceManager.java delete mode 100644 android/app/usage/AppStandby.java create mode 100644 android/appwidget/AppWidgetManagerInternal.java create mode 100644 android/arch/lifecycle/ViewModelProvidersTest.java create mode 100644 android/arch/paging/ItemKeyedDataSource.java delete mode 100644 android/arch/paging/KeyedDataSource.java create mode 100644 android/arch/paging/PageKeyedDataSource.java create mode 100644 android/arch/persistence/room/integration/testapp/vo/Day.java create mode 100644 android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java create mode 100644 android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java create mode 100644 android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java delete mode 100644 android/bluetooth/BluetoothHidDeviceAppConfiguration.java create mode 100644 android/content/pm/PackageList.java create mode 100644 android/content/pm/dex/ArtManager.java create mode 100644 android/database/sqlite/SQLiteCompatibilityWalFlags.java create mode 100644 android/graphics/ImageDecoder.java create mode 100644 android/graphics/PostProcess.java create mode 100644 android/hardware/camera2/params/SessionConfiguration.java create mode 100644 android/hardware/display/BrightnessConfiguration.java create mode 100644 android/media/NativeRoutingEventHandlerDelegate.java create mode 100644 android/mtp/MtpStorageManager.java create mode 100644 android/net/ip/IpNeighborMonitor.java delete mode 100644 android/net/util/BlockingSocketReader.java create mode 100644 android/net/util/PacketReader.java delete mode 100644 android/net/wifi/WifiLinkLayerStats.java create mode 100644 android/net/wifi/rtt/ResponderConfig.java create mode 100644 android/os/HandlerExecutor.java create mode 100644 android/os/connectivity/CellularBatteryStats.java create mode 100644 android/privacy/DifferentialPrivacyConfig.java create mode 100644 android/privacy/DifferentialPrivacyEncoder.java create mode 100644 android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java create mode 100644 android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java create mode 100644 android/privacy/internal/rappor/RapporConfig.java create mode 100644 android/privacy/internal/rappor/RapporEncoder.java create mode 100644 android/security/AttestedKeyPair.java create mode 100644 android/security/keystore/KeyAttestationException.java create mode 100644 android/security/keystore/ParcelableKeyGenParameterSpec.java create mode 100644 android/security/recoverablekeystore/KeyDerivationParameters.java create mode 100644 android/security/recoverablekeystore/KeyEntryRecoveryData.java create mode 100644 android/security/recoverablekeystore/KeyStoreRecoveryData.java create mode 100644 android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java create mode 100644 android/security/recoverablekeystore/RecoverableKeyStoreLoader.java create mode 100644 android/service/autofill/EditDistanceScorer.java create mode 100644 android/service/autofill/FieldClassification.java delete mode 100644 android/service/autofill/FieldsDetection.java create mode 100644 android/service/autofill/InternalScorer.java create mode 100644 android/service/autofill/Scorer.java create mode 100644 android/service/autofill/UserData.java create mode 100644 android/service/notification/ScheduleCalendar.java create mode 100644 android/support/annotation/LongDef.java delete mode 100644 android/support/car/drawer/CarDrawerActivity.java delete mode 100644 android/support/car/drawer/CarDrawerAdapter.java delete mode 100644 android/support/car/drawer/CarDrawerController.java delete mode 100644 android/support/car/drawer/DrawerItemClickListener.java delete mode 100644 android/support/car/drawer/DrawerItemViewHolder.java delete mode 100644 android/support/car/utils/ColumnCalculator.java delete mode 100644 android/support/car/widget/CarItemAnimator.java delete mode 100644 android/support/car/widget/CarRecyclerView.java delete mode 100644 android/support/car/widget/ColumnCardView.java delete mode 100644 android/support/car/widget/DayNightStyle.java delete mode 100644 android/support/car/widget/PagedLayoutManager.java delete mode 100644 android/support/car/widget/PagedListView.java delete mode 100644 android/support/car/widget/PagedScrollBarView.java create mode 100644 android/support/media/ExifInterfaceTest.java create mode 100644 android/support/media/tv/ChannelLogoUtilsTest.java create mode 100644 android/support/media/tv/ChannelTest.java create mode 100644 android/support/media/tv/PreviewProgramTest.java create mode 100644 android/support/media/tv/ProgramTest.java create mode 100644 android/support/media/tv/TvContractUtilsTest.java create mode 100644 android/support/media/tv/Utils.java create mode 100644 android/support/media/tv/WatchNextProgramTest.java create mode 100644 android/support/transition/ArcMotionTest.java create mode 100644 android/support/transition/AutoTransitionTest.java create mode 100644 android/support/transition/BaseTest.java create mode 100644 android/support/transition/BaseTransitionTest.java create mode 100644 android/support/transition/ChangeBoundsTest.java create mode 100644 android/support/transition/ChangeClipBoundsTest.java create mode 100644 android/support/transition/ChangeImageTransformTest.java create mode 100644 android/support/transition/ChangeScrollTest.java create mode 100644 android/support/transition/ChangeTransformTest.java create mode 100644 android/support/transition/CheckCalledRunnable.java create mode 100644 android/support/transition/ExplodeTest.java create mode 100644 android/support/transition/FadeTest.java create mode 100644 android/support/transition/FragmentTransitionTest.java create mode 100644 android/support/transition/PathMotionTest.java create mode 100644 android/support/transition/PatternPathMotionTest.java create mode 100644 android/support/transition/PropagationTest.java create mode 100644 android/support/transition/SceneTest.java create mode 100644 android/support/transition/SlideBadEdgeTest.java create mode 100644 android/support/transition/SlideDefaultEdgeTest.java create mode 100644 android/support/transition/SlideEdgeTest.java create mode 100644 android/support/transition/SyncRunnable.java create mode 100644 android/support/transition/SyncTransitionListener.java create mode 100644 android/support/transition/TransitionActivity.java create mode 100644 android/support/transition/TransitionInflaterTest.java create mode 100644 android/support/transition/TransitionManagerTest.java create mode 100644 android/support/transition/TransitionSetTest.java create mode 100644 android/support/transition/TransitionTest.java create mode 100644 android/support/transition/VisibilityTest.java create mode 100644 android/support/v7/graphics/BucketTests.java create mode 100644 android/support/v7/graphics/ConsistencyTest.java create mode 100644 android/support/v7/graphics/MaxColorsTest.java create mode 100644 android/support/v7/graphics/SwatchTests.java create mode 100644 android/support/v7/graphics/TestUtils.java create mode 100644 android/support/wear/ambient/AmbientDelegateTest.java create mode 100644 android/support/wear/ambient/AmbientModeResumeTest.java create mode 100644 android/support/wear/ambient/AmbientModeResumeTestActivity.java create mode 100644 android/support/wear/ambient/AmbientModeSupport.java create mode 100644 android/support/wear/ambient/AmbientModeTest.java create mode 100644 android/support/wear/ambient/AmbientModeTestActivity.java create mode 100644 android/support/wear/utils/MetadataTestActivity.java create mode 100644 android/support/wear/widget/BoxInsetLayoutTest.java create mode 100644 android/support/wear/widget/CircularProgressLayoutControllerTest.java create mode 100644 android/support/wear/widget/CircularProgressLayoutTest.java create mode 100644 android/support/wear/widget/LayoutTestActivity.java create mode 100644 android/support/wear/widget/RoundedDrawableTest.java create mode 100644 android/support/wear/widget/ScrollManagerTest.java create mode 100644 android/support/wear/widget/SwipeDismissFrameLayoutTest.java create mode 100644 android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java create mode 100644 android/support/wear/widget/SwipeDismissPreferenceFragment.java create mode 100644 android/support/wear/widget/WearableLinearLayoutManagerTest.java create mode 100644 android/support/wear/widget/WearableRecyclerViewTest.java create mode 100644 android/support/wear/widget/WearableRecyclerViewTestActivity.java create mode 100644 android/support/wear/widget/drawer/DrawerTestActivity.java create mode 100644 android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java create mode 100644 android/support/wear/widget/util/ArcSwipe.java create mode 100644 android/support/wear/widget/util/ArcSwipeTest.java create mode 100644 android/support/wear/widget/util/AsyncViewActions.java create mode 100644 android/support/wear/widget/util/MoreViewAssertions.java create mode 100644 android/support/wear/widget/util/WakeLockRule.java create mode 100644 android/telephony/data/DataCallResponse.java create mode 100644 android/telephony/data/InterfaceAddress.java create mode 100644 android/telephony/ims/internal/ImsCallSessionListener.java create mode 100644 android/telephony/ims/internal/ImsService.java create mode 100644 android/telephony/ims/internal/SmsImplBase.java create mode 100644 android/telephony/ims/internal/feature/CapabilityChangeRequest.java create mode 100644 android/telephony/ims/internal/feature/ImsFeature.java create mode 100644 android/telephony/ims/internal/feature/MmTelFeature.java create mode 100644 android/telephony/ims/internal/feature/RcsFeature.java create mode 100644 android/telephony/ims/internal/stub/ImsConfigImplBase.java create mode 100644 android/telephony/ims/internal/stub/ImsFeatureConfiguration.java create mode 100644 android/telephony/ims/internal/stub/ImsRegistrationImplBase.java create mode 100644 android/text/AutoGrowArray.java create mode 100644 android/text/MeasuredText_Delegate.java create mode 100644 android/text/PremeasuredText.java create mode 100644 android/util/StatsLog.java create mode 100644 android/util/apk/ApkSignatureSchemeV3Verifier.java create mode 100644 android/util/apk/ApkSignatureVerifier.java create mode 100644 android/util/apk/ApkSigningBlockUtils.java create mode 100644 android/util/apk/SignatureNotFoundException.java create mode 100644 android/util/apk/VerbatimX509Certificate.java create mode 100644 android/util/apk/WrappedX509Certificate.java create mode 100644 android/webkit/FilterMethods.java create mode 100644 android/webkit/SingleClassAndMethod.java create mode 100644 android/webkit/TracingConfig.java create mode 100644 android/webkit/TracingController.java create mode 100644 android/webkit/TracingFileOutputStream.java create mode 100644 android/webkit/WebKitTypeAsMethodParameter.java create mode 100644 android/webkit/WebKitTypeAsMethodReturn.java (limited to 'android') diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java index 8824643d..97dcb90b 100644 --- a/android/accessibilityservice/AccessibilityService.java +++ b/android/accessibilityservice/AccessibilityService.java @@ -391,8 +391,12 @@ public abstract class AccessibilityService extends Service { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({SHOW_MODE_AUTO, SHOW_MODE_HIDDEN}) - public @interface SoftKeyboardShowMode {}; + @IntDef(prefix = { "SHOW_MODE_" }, value = { + SHOW_MODE_AUTO, + SHOW_MODE_HIDDEN + }) + public @interface SoftKeyboardShowMode {} + public static final int SHOW_MODE_AUTO = 0; public static final int SHOW_MODE_HIDDEN = 1; diff --git a/android/accessibilityservice/AccessibilityServiceInfo.java b/android/accessibilityservice/AccessibilityServiceInfo.java index e0d60cd0..06a9b067 100644 --- a/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/android/accessibilityservice/AccessibilityServiceInfo.java @@ -16,8 +16,6 @@ package android.accessibilityservice; -import static android.content.pm.PackageManager.FEATURE_FINGERPRINT; - import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -49,6 +47,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static android.content.pm.PackageManager.FEATURE_FINGERPRINT; + /** * This class describes an {@link AccessibilityService}. The system notifies an * {@link AccessibilityService} for {@link android.view.accessibility.AccessibilityEvent}s @@ -554,7 +554,7 @@ public class AccessibilityServiceInfo implements Parcelable { } /** - * Updates the properties that an AccessibilityService can change dynamically. + * Updates the properties that an AccessibilitySerivice can change dynamically. * * @param other The info from which to update the properties. * diff --git a/android/accounts/AccountManager.java b/android/accounts/AccountManager.java index bd9c9fa3..782733f5 100644 --- a/android/accounts/AccountManager.java +++ b/android/accounts/AccountManager.java @@ -290,8 +290,13 @@ public class AccountManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({VISIBILITY_UNDEFINED, VISIBILITY_VISIBLE, VISIBILITY_USER_MANAGED_VISIBLE, - VISIBILITY_NOT_VISIBLE, VISIBILITY_USER_MANAGED_NOT_VISIBLE}) + @IntDef(prefix = { "VISIBILITY_" }, value = { + VISIBILITY_UNDEFINED, + VISIBILITY_VISIBLE, + VISIBILITY_USER_MANAGED_VISIBLE, + VISIBILITY_NOT_VISIBLE, + VISIBILITY_USER_MANAGED_NOT_VISIBLE + }) public @interface AccountVisibility { } diff --git a/android/annotation/CallbackExecutor.java b/android/annotation/CallbackExecutor.java new file mode 100644 index 00000000..5671a3d2 --- /dev/null +++ b/android/annotation/CallbackExecutor.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.annotation; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.content.Context; +import android.os.AsyncTask; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.concurrent.Executor; + +/** + * @paramDoc Callback and listener events are dispatched through this + * {@link Executor}, providing an easy way to control which thread is + * used. To dispatch events through the main thread of your + * application, you can use {@link Context#getMainExecutor()}. To + * dispatch events through a shared thread pool, you can use + * {@link AsyncTask#THREAD_POOL_EXECUTOR}. + * @hide + */ +@Retention(SOURCE) +@Target(PARAMETER) +public @interface CallbackExecutor { +} diff --git a/android/annotation/Condemned.java b/android/annotation/Condemned.java new file mode 100644 index 00000000..186409b3 --- /dev/null +++ b/android/annotation/Condemned.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.annotation; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A program element annotated @Condemned is one that programmers are + * blocked from using, typically because it's about to be completely destroyed. + *

+ * This is a stronger version of @Deprecated, and it's typically used to + * mark APIs that only existed temporarily in a preview SDK, and which only + * continue to exist temporarily to support binary compatibility. + * + * @hide + */ +@Retention(SOURCE) +@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) +public @interface Condemned { +} diff --git a/android/annotation/IntDef.java b/android/annotation/IntDef.java index 3f9064e4..f84a6765 100644 --- a/android/annotation/IntDef.java +++ b/android/annotation/IntDef.java @@ -52,10 +52,12 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; @Target({ANNOTATION_TYPE}) public @interface IntDef { /** Defines the constant prefix for this element */ - String[] prefix() default ""; + String[] prefix() default {}; + /** Defines the constant suffix for this element */ + String[] suffix() default {}; /** Defines the allowed constants for this element */ - long[] value() default {}; + int[] value() default {}; /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ boolean flag() default false; diff --git a/android/annotation/LongDef.java b/android/annotation/LongDef.java new file mode 100644 index 00000000..8723eef8 --- /dev/null +++ b/android/annotation/LongDef.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated long element represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + *

+ *


+ *  @Retention(SOURCE)
+ *  @LongDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ *  public @interface NavigationMode {}
+ *  public static final long NAVIGATION_MODE_STANDARD = 0;
+ *  public static final long NAVIGATION_MODE_LIST = 1;
+ *  public static final long NAVIGATION_MODE_TABS = 2;
+ *  ...
+ *  public abstract void setNavigationMode(@NavigationMode long mode);
+ *  @NavigationMode
+ *  public abstract long getNavigationMode();
+ * 
+ * For a flag, set the flag attribute: + *

+ *  @LongDef(
+ *      flag = true,
+ *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * 
+ * + * @hide + */ +@Retention(SOURCE) +@Target({ANNOTATION_TYPE}) +public @interface LongDef { + /** Defines the constant prefix for this element */ + String[] prefix() default ""; + + /** Defines the allowed constants for this element */ + long[] value() default {}; + + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} diff --git a/android/annotation/StringDef.java b/android/annotation/StringDef.java index d5157c3a..a37535b9 100644 --- a/android/annotation/StringDef.java +++ b/android/annotation/StringDef.java @@ -46,6 +46,11 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; @Retention(SOURCE) @Target({ANNOTATION_TYPE}) public @interface StringDef { + /** Defines the constant prefix for this element */ + String[] prefix() default {}; + /** Defines the constant suffix for this element */ + String[] suffix() default {}; + /** Defines the allowed constants for this element */ String[] value() default {}; } diff --git a/android/app/ActionBar.java b/android/app/ActionBar.java index 0e8326de..04ff48ce 100644 --- a/android/app/ActionBar.java +++ b/android/app/ActionBar.java @@ -95,7 +95,11 @@ import java.lang.annotation.RetentionPolicy; public abstract class ActionBar { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + @IntDef(prefix = { "NAVIGATION_MODE_" }, value = { + NAVIGATION_MODE_STANDARD, + NAVIGATION_MODE_LIST, + NAVIGATION_MODE_TABS + }) public @interface NavigationMode {} /** @@ -139,15 +143,14 @@ public abstract class ActionBar { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = { - DISPLAY_USE_LOGO, - DISPLAY_SHOW_HOME, - DISPLAY_HOME_AS_UP, - DISPLAY_SHOW_TITLE, - DISPLAY_SHOW_CUSTOM, - DISPLAY_TITLE_MULTIPLE_LINES - }) + @IntDef(flag = true, prefix = { "DISPLAY_" }, value = { + DISPLAY_USE_LOGO, + DISPLAY_SHOW_HOME, + DISPLAY_HOME_AS_UP, + DISPLAY_SHOW_TITLE, + DISPLAY_SHOW_CUSTOM, + DISPLAY_TITLE_MULTIPLE_LINES + }) public @interface DisplayOptions {} /** diff --git a/android/app/Activity.java b/android/app/Activity.java index 03a3631b..aa099eb1 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -7070,7 +7070,13 @@ public class Activity extends ContextThemeWrapper mActivityTransitionState.enterReady(this); } - final void performRestart() { + /** + * Restart the activity. + * @param start Indicates whether the activity should also be started after restart. + * The option to not start immediately is needed in case a transaction with + * multiple lifecycle transitions is in progress. + */ + final void performRestart(boolean start) { mCanEnterPictureInPicture = true; mFragments.noteStateNotSaved(); @@ -7108,12 +7114,14 @@ public class Activity extends ContextThemeWrapper "Activity " + mComponent.toShortString() + " did not call through to super.onRestart()"); } - performStart(); + if (start) { + performStart(); + } } } final void performResume() { - performRestart(); + performRestart(true /* start */); mFragments.execPendingActions(); diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index 02b7f8c5..1adae7a8 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -175,7 +175,7 @@ public class ActivityManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "BUGREPORT_OPTION_" }, value = { BUGREPORT_OPTION_FULL, BUGREPORT_OPTION_INTERACTIVE, BUGREPORT_OPTION_REMOTE, @@ -457,6 +457,20 @@ public class ActivityManager { /** @hide User operation call: one of related users cannot be stopped. */ public static final int USER_OP_ERROR_RELATED_USERS_CANNOT_STOP = -4; + /** + * @hide + * Process states, describing the kind of state a particular process is in. + * When updating these, make sure to also check all related references to the + * constant in code, and update these arrays: + * + * @see com.android.internal.app.procstats.ProcessState#PROCESS_STATE_TO_STATE + * @see com.android.server.am.ProcessList#sProcStateToProcMem + * @see com.android.server.am.ProcessList#sFirstAwakePssTimes + * @see com.android.server.am.ProcessList#sSameAwakePssTimes + * @see com.android.server.am.ProcessList#sTestFirstPssTimes + * @see com.android.server.am.ProcessList#sTestSamePssTimes + */ + /** @hide Not a real process state. */ public static final int PROCESS_STATE_UNKNOWN = -1; @@ -476,35 +490,35 @@ public class ActivityManager { /** @hide Process is hosting a foreground service. */ public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; - /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */ - public static final int PROCESS_STATE_TOP_SLEEPING = 5; - /** @hide Process is important to the user, and something they are aware of. */ - public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6; + public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 5; /** @hide Process is important to the user, but not something they are aware of. */ - public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7; + public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 6; /** @hide Process is in the background transient so we will try to keep running. */ - public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 8; + public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 7; /** @hide Process is in the background running a backup/restore operation. */ - public static final int PROCESS_STATE_BACKUP = 9; - - /** @hide Process is in the background, but it can't restore its state so we want - * to try to avoid killing it. */ - public static final int PROCESS_STATE_HEAVY_WEIGHT = 10; + public static final int PROCESS_STATE_BACKUP = 8; /** @hide Process is in the background running a service. Unlike oom_adj, this level * is used for both the normal running in background state and the executing * operations state. */ - public static final int PROCESS_STATE_SERVICE = 11; + public static final int PROCESS_STATE_SERVICE = 9; /** @hide Process is in the background running a receiver. Note that from the * perspective of oom_adj, receivers run at a higher foreground level, but for our * prioritization here that is not necessary and putting them below services means * many fewer changes in some process states as they receive broadcasts. */ - public static final int PROCESS_STATE_RECEIVER = 12; + public static final int PROCESS_STATE_RECEIVER = 10; + + /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */ + public static final int PROCESS_STATE_TOP_SLEEPING = 11; + + /** @hide Process is in the background, but it can't restore its state so we want + * to try to avoid killing it. */ + public static final int PROCESS_STATE_HEAVY_WEIGHT = 12; /** @hide Process is in the background but hosts the home activity. */ public static final int PROCESS_STATE_HOME = 13; @@ -533,10 +547,10 @@ public class ActivityManager { // to frameworks/base/core/proto/android/app/activitymanager.proto and the following method must // be updated to correctly map between them. /** - * Maps ActivityManager.PROCESS_STATE_ values to ActivityManagerProto.ProcessState enum. + * Maps ActivityManager.PROCESS_STATE_ values to ProcessState enum. * * @param amInt a process state of the form ActivityManager.PROCESS_STATE_ - * @return the value of the corresponding android.app.ActivityManagerProto's ProcessState enum. + * @return the value of the corresponding ActivityManager's ProcessState enum. * @hide */ public static final int processStateAmToProto(int amInt) { @@ -810,7 +824,7 @@ public class ActivityManager { * impose on your application to let the overall system work best. The * returned value is in megabytes; the baseline Android memory class is * 16 (which happens to be the Java heap limit of those devices); some - * device with more memory may return 24 or even higher numbers. + * devices with more memory may return 24 or even higher numbers. */ public int getMemoryClass() { return staticGetMemoryClass(); @@ -837,7 +851,7 @@ public class ActivityManager { * constrained devices, or it may be significantly larger on devices with * a large amount of available RAM. * - *

The is the size of the application's Dalvik heap if it has + *

This is the size of the application's Dalvik heap if it has * specified android:largeHeap="true" in its manifest. */ public int getLargeMemoryClass() { @@ -1944,15 +1958,17 @@ public class ActivityManager { * @param animate Whether we should play an animation for the moving the task * @param initialBounds If the primary stack gets created, it will use these bounds for the * docked stack. Pass {@code null} to use default bounds. + * @param showRecents If the recents activity should be shown on the other side of the task + * going into split-screen mode. * @hide */ @TestApi @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public void setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop, - boolean animate, Rect initialBounds) throws SecurityException { + boolean animate, Rect initialBounds, boolean showRecents) throws SecurityException { try { getService().setTaskWindowingModeSplitScreenPrimary(taskId, createMode, toTop, animate, - initialBounds); + initialBounds, showRecents); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2611,7 +2627,7 @@ public class ActivityManager { Manifest.permission.ACCESS_INSTANT_APPS}) public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) { try { - return getService().clearApplicationUserData(packageName, + return getService().clearApplicationUserData(packageName, false, observer, UserHandle.myUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -2882,13 +2898,13 @@ public class ActivityManager { public static final int IMPORTANCE_FOREGROUND_SERVICE = 125; /** - * Constant for {@link #importance}: This process is running the foreground - * UI, but the device is asleep so it is not visible to the user. This means - * the user is not really aware of the process, because they can not see or - * interact with it, but it is quite important because it what they expect to - * return to once unlocking the device. + * @deprecated Pre-{@link android.os.Build.VERSION_CODES#P} version of + * {@link #IMPORTANCE_TOP_SLEEPING}. As of Android + * {@link android.os.Build.VERSION_CODES#P}, this is considered much less + * important since we want to reduce what apps can do when the screen is off. */ - public static final int IMPORTANCE_TOP_SLEEPING = 150; + @Deprecated + public static final int IMPORTANCE_TOP_SLEEPING_PRE_28 = 150; /** * Constant for {@link #importance}: This process is running something @@ -2939,14 +2955,6 @@ public class ActivityManager { */ public static final int IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170; - /** - * Constant for {@link #importance}: This process is running an - * application that can not save its state, and thus can't be killed - * while in the background. - * @hide - */ - public static final int IMPORTANCE_CANT_SAVE_STATE= 270; - /** * Constant for {@link #importance}: This process is contains services * that should remain running. These are background services apps have @@ -2956,6 +2964,23 @@ public class ActivityManager { */ public static final int IMPORTANCE_SERVICE = 300; + /** + * Constant for {@link #importance}: This process is running the foreground + * UI, but the device is asleep so it is not visible to the user. Though the + * system will try hard to keep its process from being killed, in all other + * ways we consider it a kind of cached process, with the limitations that go + * along with that state: network access, running background services, etc. + */ + public static final int IMPORTANCE_TOP_SLEEPING = 325; + + /** + * Constant for {@link #importance}: This process is running an + * application that can not save its state, and thus can't be killed + * while in the background. This will be used with apps that have + * {@link android.R.attr#cantSaveState} set on their application tag. + */ + public static final int IMPORTANCE_CANT_SAVE_STATE = 350; + /** * Constant for {@link #importance}: This process process contains * cached code that is expendable, not actively running any app components @@ -2991,16 +3016,16 @@ public class ActivityManager { return IMPORTANCE_GONE; } else if (procState >= PROCESS_STATE_HOME) { return IMPORTANCE_CACHED; + } else if (procState == PROCESS_STATE_HEAVY_WEIGHT) { + return IMPORTANCE_CANT_SAVE_STATE; + } else if (procState >= PROCESS_STATE_TOP_SLEEPING) { + return IMPORTANCE_TOP_SLEEPING; } else if (procState >= PROCESS_STATE_SERVICE) { return IMPORTANCE_SERVICE; - } else if (procState > PROCESS_STATE_HEAVY_WEIGHT) { - return IMPORTANCE_CANT_SAVE_STATE; } else if (procState >= PROCESS_STATE_TRANSIENT_BACKGROUND) { return IMPORTANCE_PERCEPTIBLE; } else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) { return IMPORTANCE_VISIBLE; - } else if (procState >= PROCESS_STATE_TOP_SLEEPING) { - return IMPORTANCE_TOP_SLEEPING; } else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) { return IMPORTANCE_FOREGROUND_SERVICE; } else { @@ -3034,6 +3059,8 @@ public class ActivityManager { switch (importance) { case IMPORTANCE_PERCEPTIBLE: return IMPORTANCE_PERCEPTIBLE_PRE_26; + case IMPORTANCE_TOP_SLEEPING: + return IMPORTANCE_TOP_SLEEPING_PRE_28; case IMPORTANCE_CANT_SAVE_STATE: return IMPORTANCE_CANT_SAVE_STATE_PRE_26; } @@ -3047,16 +3074,18 @@ public class ActivityManager { return PROCESS_STATE_NONEXISTENT; } else if (importance >= IMPORTANCE_CACHED) { return PROCESS_STATE_HOME; + } else if (importance >= IMPORTANCE_CANT_SAVE_STATE) { + return PROCESS_STATE_HEAVY_WEIGHT; + } else if (importance >= IMPORTANCE_TOP_SLEEPING) { + return PROCESS_STATE_TOP_SLEEPING; } else if (importance >= IMPORTANCE_SERVICE) { return PROCESS_STATE_SERVICE; - } else if (importance > IMPORTANCE_CANT_SAVE_STATE) { - return PROCESS_STATE_HEAVY_WEIGHT; } else if (importance >= IMPORTANCE_PERCEPTIBLE) { return PROCESS_STATE_TRANSIENT_BACKGROUND; } else if (importance >= IMPORTANCE_VISIBLE) { return PROCESS_STATE_IMPORTANT_FOREGROUND; - } else if (importance >= IMPORTANCE_TOP_SLEEPING) { - return PROCESS_STATE_TOP_SLEEPING; + } else if (importance >= IMPORTANCE_TOP_SLEEPING_PRE_28) { + return PROCESS_STATE_FOREGROUND_SERVICE; } else if (importance >= IMPORTANCE_FOREGROUND_SERVICE) { return PROCESS_STATE_FOREGROUND_SERVICE; } else { @@ -3835,7 +3864,7 @@ public class ActivityManager { pw.println(); dumpService(pw, fd, ProcessStats.SERVICE_NAME, new String[] { packageName }); pw.println(); - dumpService(pw, fd, "usagestats", new String[] { "--packages", packageName }); + dumpService(pw, fd, "usagestats", new String[] { packageName }); pw.println(); dumpService(pw, fd, BatteryStats.SERVICE_NAME, new String[] { packageName }); pw.flush(); diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java index d7efa91f..60a5a110 100644 --- a/android/app/ActivityManagerInternal.java +++ b/android/app/ActivityManagerInternal.java @@ -99,7 +99,10 @@ public abstract class ActivityManagerInternal { // Called by the power manager. public abstract void onWakefulnessChanged(int wakefulness); - public abstract int startIsolatedProcess(String entryPoint, String[] mainArgs, + /** + * @return {@code true} if process start is successful, {@code false} otherwise. + */ + public abstract boolean startIsolatedProcess(String entryPoint, String[] mainArgs, String processName, String abiOverride, int uid, Runnable crashHandler); /** @@ -260,6 +263,11 @@ public abstract class ActivityManagerInternal { */ public abstract void notifyNetworkPolicyRulesUpdated(int uid, long procStateSeq); + /** + * Called after the voice interaction service has changed. + */ + public abstract void notifyActiveVoiceInteractionServiceChanged(ComponentName component); + /** * Called after virtual display Id is updated by * {@link com.android.server.vr.Vr2dDisplay} with a specific @@ -299,4 +307,16 @@ public abstract class ActivityManagerInternal { * @return true if runtime was restarted, false if it's normal boot */ public abstract boolean isRuntimeRestarted(); + + /** + * Returns {@code true} if {@code uid} is running an activity from {@code packageName}. + */ + public abstract boolean hasRunningActivity(int uid, @Nullable String packageName); + + public interface ScreenObserver { + public void onAwakeStateChanged(boolean isAwake); + public void onKeyguardStateChanged(boolean isShowing); + } + + public abstract void registerScreenObserver(ScreenObserver observer); } diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index 4a21f5c4..e61c5b7c 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -36,6 +36,7 @@ import android.os.IRemoteCallback; import android.os.Parcelable; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.UserHandle; import android.transition.Transition; import android.transition.TransitionListenerAdapter; import android.transition.TransitionManager; @@ -265,6 +266,8 @@ public class ActivityOptions { public static final int ANIM_CUSTOM_IN_PLACE = 10; /** @hide */ public static final int ANIM_CLIP_REVEAL = 11; + /** @hide */ + public static final int ANIM_OPEN_CROSS_PROFILE_APPS = 12; private String mPackageName; private Rect mLaunchBounds; @@ -485,6 +488,19 @@ public class ActivityOptions { return opts; } + /** + * Creates an {@link ActivityOptions} object specifying an animation where the new activity + * is started in another user profile by calling {@link + * android.content.pm.crossprofile.CrossProfileApps#startMainActivity(ComponentName, UserHandle) + * }. + * @hide + */ + public static ActivityOptions makeOpenCrossProfileAppsAnimation() { + ActivityOptions options = new ActivityOptions(); + options.mAnimationType = ANIM_OPEN_CROSS_PROFILE_APPS; + return options; + } + /** * Create an ActivityOptions specifying an animation where a thumbnail * is scaled from a given position to the new activity window that is diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java index ffd012d9..aaa6bf03 100644 --- a/android/app/ActivityThread.java +++ b/android/app/ActivityThread.java @@ -16,6 +16,13 @@ package android.app; +import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME; +import static android.app.servertransaction.ActivityLifecycleItem.ON_START; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE; import static android.view.Display.INVALID_DISPLAY; import android.annotation.NonNull; @@ -23,8 +30,12 @@ import android.annotation.Nullable; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; import android.app.backup.BackupAgent; +import android.app.servertransaction.ActivityLifecycleItem.LifecycleState; import android.app.servertransaction.ActivityResultItem; import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.PendingTransactionActions; +import android.app.servertransaction.PendingTransactionActions.StopInfo; +import android.app.servertransaction.TransactionExecutor; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; import android.content.ComponentName; @@ -69,6 +80,7 @@ import android.os.DropBoxManager; import android.os.Environment; import android.os.GraphicsEnvironment; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.LocaleList; import android.os.Looper; @@ -84,7 +96,6 @@ import android.os.StrictMode; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; -import android.os.TransactionTooLargeException; import android.os.UserHandle; import android.provider.BlockedNumberContract; import android.provider.CalendarContract; @@ -102,12 +113,12 @@ import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.LogPrinter; -import android.util.LogWriter; import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.SparseIntArray; import android.util.SuperNotCalledException; +import android.util.proto.ProtoOutputStream; import android.view.ContextThemeWrapper; import android.view.Display; import android.view.ThreadedRenderer; @@ -121,6 +132,7 @@ import android.view.WindowManagerGlobal; import android.webkit.WebView; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; import com.android.internal.os.BinderInternal; @@ -128,9 +140,9 @@ import com.android.internal.os.RuntimeInit; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.IndentingPrintWriter; import com.android.org.conscrypt.OpenSSLSocketImpl; import com.android.org.conscrypt.TrustedCertificateStore; +import com.android.server.am.proto.MemInfoProto; import dalvik.system.BaseDexClassLoader; import dalvik.system.CloseGuard; @@ -161,6 +173,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.TimeZone; +import java.util.concurrent.Executor; final class RemoteServiceException extends AndroidRuntimeException { public RemoteServiceException(String msg) { @@ -188,7 +201,7 @@ public final class ActivityThread extends ClientTransactionHandler { private static final boolean DEBUG_BACKUP = false; public static final boolean DEBUG_CONFIGURATION = false; private static final boolean DEBUG_SERVICE = false; - private static final boolean DEBUG_MEMORY_TRIM = false; + public static final boolean DEBUG_MEMORY_TRIM = false; private static final boolean DEBUG_PROVIDER = false; private static final boolean DEBUG_ORDER = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; @@ -204,10 +217,6 @@ public final class ActivityThread extends ClientTransactionHandler { /** Type for IActivityManager.serviceDoneExecuting: done stopping (destroying) service */ public static final int SERVICE_DONE_EXECUTING_STOP = 2; - // Details for pausing activity. - private static final int USER_LEAVING = 1; - private static final int DONT_REPORT = 2; - // Whether to invoke an activity callback after delivering new configuration. private static final boolean REPORT_TO_ACTIVITY = true; @@ -216,6 +225,12 @@ public final class ActivityThread extends ClientTransactionHandler { */ public static final long INVALID_PROC_STATE_SEQ = -1; + /** + * Identifier for the sequence no. associated with this process start. It will be provided + * as one of the arguments when the process starts. + */ + public static final String PROC_START_SEQ_IDENT = "seq="; + private final Object mNetworkPolicyLock = new Object(); /** @@ -235,6 +250,7 @@ public final class ActivityThread extends ClientTransactionHandler { final ApplicationThread mAppThread = new ApplicationThread(); final Looper mLooper = Looper.myLooper(); final H mH = new H(); + final Executor mExecutor = new HandlerExecutor(mH); final ArrayMap mActivities = new ArrayMap<>(); // List of new activities (via ActivityRecord.nextIdle) that should // be reported when next we idle. @@ -287,12 +303,8 @@ public final class ActivityThread extends ClientTransactionHandler { final ArrayList mRelaunchingActivities = new ArrayList<>(); @GuardedBy("mResourcesManager") Configuration mPendingConfiguration = null; - // Because we merge activity relaunch operations we can't depend on the ordering provided by - // the handler messages. We need to introduce secondary ordering mechanism, which will allow - // us to drop certain events, if we know that they happened before relaunch we already executed. - // This represents the order of receiving the request from AM. - @GuardedBy("mResourcesManager") - int mLifecycleSeq = 0; + // An executor that performs multi-step transactions. + private final TransactionExecutor mTransactionExecutor = new TransactionExecutor(this); private final ResourcesManager mResourcesManager; @@ -340,8 +352,9 @@ public final class ActivityThread extends ClientTransactionHandler { Bundle mCoreSettings = null; - static final class ActivityClientRecord { - IBinder token; + /** Activity client record, used for bookkeeping for the real {@link Activity} instance. */ + public static final class ActivityClientRecord { + public IBinder token; int ident; Intent intent; String referrer; @@ -353,6 +366,7 @@ public final class ActivityThread extends ClientTransactionHandler { Activity parent; String embeddedID; Activity.NonConfigurationInstances lastNonConfigurationInstances; + // TODO(lifecycler): Use mLifecycleState instead. boolean paused; boolean stopped; boolean hideForNow; @@ -369,13 +383,13 @@ public final class ActivityThread extends ClientTransactionHandler { ActivityInfo activityInfo; CompatibilityInfo compatInfo; - LoadedApk packageInfo; + public LoadedApk loadedApk; List pendingResults; List pendingIntents; boolean startsNotResumed; - boolean isForward; + public final boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; @@ -383,15 +397,42 @@ public final class ActivityThread extends ClientTransactionHandler { WindowManager mPendingRemoveWindowManager; boolean mPreserveWindow; - // Set for relaunch requests, indicates the order number of the relaunch operation, so it - // can be compared with other lifecycle operations. - int relaunchSeq = 0; + @LifecycleState + private int mLifecycleState = PRE_ON_CREATE; - // Can only be accessed from the UI thread. This represents the latest processed message - // that is related to lifecycle events/ - int lastProcessedSeq = 0; + @VisibleForTesting + public ActivityClientRecord() { + this.isForward = false; + init(); + } - ActivityClientRecord() { + public ActivityClientRecord(IBinder token, Intent intent, int ident, + ActivityInfo info, Configuration overrideConfig, CompatibilityInfo compatInfo, + String referrer, IVoiceInteractor voiceInteractor, Bundle state, + PersistableBundle persistentState, List pendingResults, + List pendingNewIntents, boolean isForward, + ProfilerInfo profilerInfo, ClientTransactionHandler client) { + this.token = token; + this.ident = ident; + this.intent = intent; + this.referrer = referrer; + this.voiceInteractor = voiceInteractor; + this.activityInfo = info; + this.compatInfo = compatInfo; + this.state = state; + this.persistentState = persistentState; + this.pendingResults = pendingResults; + this.pendingIntents = pendingNewIntents; + this.isForward = isForward; + this.profilerInfo = profilerInfo; + this.overrideConfig = overrideConfig; + this.loadedApk = client.getLoadedApkNoCheck(activityInfo.applicationInfo, + compatInfo); + init(); + } + + /** Common initializer for all constructors. */ + private void init() { parent = null; embeddedID = null; paused = false; @@ -408,6 +449,38 @@ public final class ActivityThread extends ClientTransactionHandler { }; } + /** Get the current lifecycle state. */ + public int getLifecycleState() { + return mLifecycleState; + } + + /** Update the current lifecycle state for internal bookkeeping. */ + public void setState(@LifecycleState int newLifecycleState) { + mLifecycleState = newLifecycleState; + switch (mLifecycleState) { + case ON_CREATE: + paused = true; + stopped = true; + break; + case ON_START: + paused = true; + stopped = false; + break; + case ON_RESUME: + paused = false; + stopped = false; + break; + case ON_PAUSE: + paused = true; + stopped = false; + break; + case ON_STOP: + paused = true; + stopped = true; + break; + } + } + public boolean isPreHoneycomb() { if (activity != null) { return activity.getApplicationInfo().targetSdkVersion @@ -535,7 +608,7 @@ public final class ActivityThread extends ClientTransactionHandler { } static final class AppBindData { - LoadedApk info; + LoadedApk loadedApk; String processName; ApplicationInfo appInfo; List providers; @@ -1079,7 +1152,7 @@ public final class ActivityThread extends ClientTransactionHandler { int N = stats.dbStats.size(); if (N > 0) { pw.println(" DATABASES"); - printRow(pw, " %8s %8s %14s %14s %s", "pgsz", "dbsz", "Lookaside(b)", "cache", + printRow(pw, DB_INFO_FORMAT, "pgsz", "dbsz", "Lookaside(b)", "cache", "Dbname"); for (int i = 0; i < N; i++) { DbStats dbStats = stats.dbStats.get(i); @@ -1110,6 +1183,124 @@ public final class ActivityThread extends ClientTransactionHandler { } } + @Override + public void dumpMemInfoProto(ParcelFileDescriptor pfd, Debug.MemoryInfo mem, + boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly, + boolean dumpUnreachable, String[] args) { + ProtoOutputStream proto = new ProtoOutputStream(pfd.getFileDescriptor()); + try { + dumpMemInfo(proto, mem, dumpFullInfo, dumpDalvik, dumpSummaryOnly, dumpUnreachable); + } finally { + proto.flush(); + IoUtils.closeQuietly(pfd); + } + } + + private void dumpMemInfo(ProtoOutputStream proto, Debug.MemoryInfo memInfo, + boolean dumpFullInfo, boolean dumpDalvik, + boolean dumpSummaryOnly, boolean dumpUnreachable) { + long nativeMax = Debug.getNativeHeapSize() / 1024; + long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; + long nativeFree = Debug.getNativeHeapFreeSize() / 1024; + + Runtime runtime = Runtime.getRuntime(); + runtime.gc(); // Do GC since countInstancesOfClass counts unreachable objects. + long dalvikMax = runtime.totalMemory() / 1024; + long dalvikFree = runtime.freeMemory() / 1024; + long dalvikAllocated = dalvikMax - dalvikFree; + + Class[] classesToCount = new Class[] { + ContextImpl.class, + Activity.class, + WebView.class, + OpenSSLSocketImpl.class + }; + long[] instanceCounts = VMDebug.countInstancesOfClasses(classesToCount, true); + long appContextInstanceCount = instanceCounts[0]; + long activityInstanceCount = instanceCounts[1]; + long webviewInstanceCount = instanceCounts[2]; + long openSslSocketCount = instanceCounts[3]; + + long viewInstanceCount = ViewDebug.getViewInstanceCount(); + long viewRootInstanceCount = ViewDebug.getViewRootImplCount(); + int globalAssetCount = AssetManager.getGlobalAssetCount(); + int globalAssetManagerCount = AssetManager.getGlobalAssetManagerCount(); + int binderLocalObjectCount = Debug.getBinderLocalObjectCount(); + int binderProxyObjectCount = Debug.getBinderProxyObjectCount(); + int binderDeathObjectCount = Debug.getBinderDeathObjectCount(); + long parcelSize = Parcel.getGlobalAllocSize(); + long parcelCount = Parcel.getGlobalAllocCount(); + SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo(); + + final long mToken = proto.start(MemInfoProto.AppData.PROCESS_MEMORY); + proto.write(MemInfoProto.ProcessMemory.PID, Process.myPid()); + proto.write(MemInfoProto.ProcessMemory.PROCESS_NAME, + (mBoundApplication != null) ? mBoundApplication.processName : "unknown"); + dumpMemInfoTable(proto, memInfo, dumpDalvik, dumpSummaryOnly, + nativeMax, nativeAllocated, nativeFree, + dalvikMax, dalvikAllocated, dalvikFree); + proto.end(mToken); + + final long oToken = proto.start(MemInfoProto.AppData.OBJECTS); + proto.write(MemInfoProto.AppData.ObjectStats.VIEW_INSTANCE_COUNT, viewInstanceCount); + proto.write(MemInfoProto.AppData.ObjectStats.VIEW_ROOT_INSTANCE_COUNT, + viewRootInstanceCount); + proto.write(MemInfoProto.AppData.ObjectStats.APP_CONTEXT_INSTANCE_COUNT, + appContextInstanceCount); + proto.write(MemInfoProto.AppData.ObjectStats.ACTIVITY_INSTANCE_COUNT, + activityInstanceCount); + proto.write(MemInfoProto.AppData.ObjectStats.GLOBAL_ASSET_COUNT, globalAssetCount); + proto.write(MemInfoProto.AppData.ObjectStats.GLOBAL_ASSET_MANAGER_COUNT, + globalAssetManagerCount); + proto.write(MemInfoProto.AppData.ObjectStats.LOCAL_BINDER_OBJECT_COUNT, + binderLocalObjectCount); + proto.write(MemInfoProto.AppData.ObjectStats.PROXY_BINDER_OBJECT_COUNT, + binderProxyObjectCount); + proto.write(MemInfoProto.AppData.ObjectStats.PARCEL_MEMORY_KB, parcelSize / 1024); + proto.write(MemInfoProto.AppData.ObjectStats.PARCEL_COUNT, parcelCount); + proto.write(MemInfoProto.AppData.ObjectStats.BINDER_OBJECT_DEATH_COUNT, + binderDeathObjectCount); + proto.write(MemInfoProto.AppData.ObjectStats.OPEN_SSL_SOCKET_COUNT, openSslSocketCount); + proto.write(MemInfoProto.AppData.ObjectStats.WEBVIEW_INSTANCE_COUNT, + webviewInstanceCount); + proto.end(oToken); + + // SQLite mem info + final long sToken = proto.start(MemInfoProto.AppData.SQL); + proto.write(MemInfoProto.AppData.SqlStats.MEMORY_USED_KB, stats.memoryUsed / 1024); + proto.write(MemInfoProto.AppData.SqlStats.PAGECACHE_OVERFLOW_KB, + stats.pageCacheOverflow / 1024); + proto.write(MemInfoProto.AppData.SqlStats.MALLOC_SIZE_KB, stats.largestMemAlloc / 1024); + int n = stats.dbStats.size(); + for (int i = 0; i < n; i++) { + DbStats dbStats = stats.dbStats.get(i); + + final long dToken = proto.start(MemInfoProto.AppData.SqlStats.DATABASES); + proto.write(MemInfoProto.AppData.SqlStats.Database.NAME, dbStats.dbName); + proto.write(MemInfoProto.AppData.SqlStats.Database.PAGE_SIZE, dbStats.pageSize); + proto.write(MemInfoProto.AppData.SqlStats.Database.DB_SIZE, dbStats.dbSize); + proto.write(MemInfoProto.AppData.SqlStats.Database.LOOKASIDE_B, dbStats.lookaside); + proto.write(MemInfoProto.AppData.SqlStats.Database.CACHE, dbStats.cache); + proto.end(dToken); + } + proto.end(sToken); + + // Asset details. + String assetAlloc = AssetManager.getAssetAllocations(); + if (assetAlloc != null) { + proto.write(MemInfoProto.AppData.ASSET_ALLOCATIONS, assetAlloc); + } + + // Unreachable native memory + if (dumpUnreachable) { + int flags = mBoundApplication == null ? 0 : mBoundApplication.appInfo.flags; + boolean showContents = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 + || android.os.Build.IS_DEBUGGABLE; + proto.write(MemInfoProto.AppData.UNREACHABLE_MEMORY, + Debug.getUnreachableMemory(100, showContents)); + } + } + @Override public void dumpGfxInfo(ParcelFileDescriptor pfd, String[] args) { nDumpGraphicsInfo(pfd.getFileDescriptor()); @@ -1313,13 +1504,6 @@ public final class ActivityThread extends ClientTransactionHandler { mAppThread.updateProcessState(processState, fromIpc); } - @Override - public int getLifecycleSeq() { - synchronized (mResourcesManager) { - return mLifecycleSeq++; - } - } - class H extends Handler { public static final int BIND_APPLICATION = 110; public static final int EXIT_APPLICATION = 111; @@ -1584,7 +1768,9 @@ public final class ActivityThread extends ClientTransactionHandler { (String[]) ((SomeArgs) msg.obj).arg2); break; case EXECUTE_TRANSACTION: - ((ClientTransaction) msg.obj).execute(ActivityThread.this); + final ClientTransaction transaction = (ClientTransaction) msg.obj; + mTransactionExecutor.execute(transaction); + transaction.recycle(); break; } Object obj = msg.obj; @@ -1714,13 +1900,13 @@ public final class ActivityThread extends ClientTransactionHandler { return mH; } - public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo, - int flags) { - return getPackageInfo(packageName, compatInfo, flags, UserHandle.myUserId()); + public final LoadedApk getLoadedApkForPackageName(String packageName, + CompatibilityInfo compatInfo, int flags) { + return getLoadedApkForPackageName(packageName, compatInfo, flags, UserHandle.myUserId()); } - public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo, - int flags, int userId) { + public final LoadedApk getLoadedApkForPackageName(String packageName, + CompatibilityInfo compatInfo, int flags, int userId) { final boolean differentUser = (UserHandle.myUserId() != userId); synchronized (mResourcesManager) { WeakReference ref; @@ -1733,13 +1919,13 @@ public final class ActivityThread extends ClientTransactionHandler { ref = mResourcePackages.get(packageName); } - LoadedApk packageInfo = ref != null ? ref.get() : null; - //Slog.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo); - //if (packageInfo != null) Slog.i(TAG, "isUptoDate " + packageInfo.mResDir - // + ": " + packageInfo.mResources.getAssets().isUpToDate()); - if (packageInfo != null && (packageInfo.mResources == null - || packageInfo.mResources.getAssets().isUpToDate())) { - if (packageInfo.isSecurityViolation() + LoadedApk loadedApk = ref != null ? ref.get() : null; + //Slog.i(TAG, "getLoadedApkForPackageName " + packageName + ": " + loadedApk); + //if (loadedApk != null) Slog.i(TAG, "isUptoDate " + loadedApk.mResDir + // + ": " + loadedApk.mResources.getAssets().isUpToDate()); + if (loadedApk != null && (loadedApk.mResources == null + || loadedApk.mResources.getAssets().isUpToDate())) { + if (loadedApk.isSecurityViolation() && (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) { throw new SecurityException( "Requesting code from " + packageName @@ -1747,7 +1933,7 @@ public final class ActivityThread extends ClientTransactionHandler { + mBoundApplication.processName + "/" + mBoundApplication.appInfo.uid); } - return packageInfo; + return loadedApk; } } @@ -1762,13 +1948,13 @@ public final class ActivityThread extends ClientTransactionHandler { } if (ai != null) { - return getPackageInfo(ai, compatInfo, flags); + return getLoadedApk(ai, compatInfo, flags); } return null; } - public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo, + public final LoadedApk getLoadedApk(ApplicationInfo ai, CompatibilityInfo compatInfo, int flags) { boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0; boolean securityViolation = includeCode && ai.uid != 0 @@ -1790,16 +1976,17 @@ public final class ActivityThread extends ClientTransactionHandler { throw new SecurityException(msg); } } - return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode, + return getLoadedApk(ai, compatInfo, null, securityViolation, includeCode, registerPackage); } - public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, + @Override + public final LoadedApk getLoadedApkNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { - return getPackageInfo(ai, compatInfo, null, false, true, false); + return getLoadedApk(ai, compatInfo, null, false, true, false); } - public final LoadedApk peekPackageInfo(String packageName, boolean includeCode) { + public final LoadedApk peekLoadedApk(String packageName, boolean includeCode) { synchronized (mResourcesManager) { WeakReference ref; if (includeCode) { @@ -1811,7 +1998,7 @@ public final class ActivityThread extends ClientTransactionHandler { } } - private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, + private LoadedApk getLoadedApk(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid)); @@ -1826,35 +2013,35 @@ public final class ActivityThread extends ClientTransactionHandler { ref = mResourcePackages.get(aInfo.packageName); } - LoadedApk packageInfo = ref != null ? ref.get() : null; - if (packageInfo == null || (packageInfo.mResources != null - && !packageInfo.mResources.getAssets().isUpToDate())) { + LoadedApk loadedApk = ref != null ? ref.get() : null; + if (loadedApk == null || (loadedApk.mResources != null + && !loadedApk.mResources.getAssets().isUpToDate())) { if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package " : "Loading resource-only package ") + aInfo.packageName + " (in " + (mBoundApplication != null ? mBoundApplication.processName : null) + ")"); - packageInfo = + loadedApk = new LoadedApk(this, aInfo, compatInfo, baseLoader, securityViolation, includeCode && (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage); if (mSystemThread && "android".equals(aInfo.packageName)) { - packageInfo.installSystemApplicationInfo(aInfo, - getSystemContext().mPackageInfo.getClassLoader()); + loadedApk.installSystemApplicationInfo(aInfo, + getSystemContext().mLoadedApk.getClassLoader()); } if (differentUser) { // Caching not supported across users } else if (includeCode) { mPackages.put(aInfo.packageName, - new WeakReference(packageInfo)); + new WeakReference(loadedApk)); } else { mResourcePackages.put(aInfo.packageName, - new WeakReference(packageInfo)); + new WeakReference(loadedApk)); } } - return packageInfo; + return loadedApk; } } @@ -1885,6 +2072,10 @@ public final class ActivityThread extends ClientTransactionHandler { return mLooper; } + public Executor getExecutor() { + return mExecutor; + } + public Application getApplication() { return mInitialApplication; } @@ -2259,6 +2450,167 @@ public final class ActivityThread extends ClientTransactionHandler { } } + /** + * Dump heap info to proto. + * + * @param hasSwappedOutPss determines whether to use dirtySwap or dirtySwapPss + */ + private static void dumpMemoryInfo(ProtoOutputStream proto, long fieldId, String name, + int pss, int cleanPss, int sharedDirty, int privateDirty, + int sharedClean, int privateClean, + boolean hasSwappedOutPss, int dirtySwap, int dirtySwapPss) { + final long token = proto.start(fieldId); + + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.NAME, name); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.TOTAL_PSS_KB, pss); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.CLEAN_PSS_KB, cleanPss); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.SHARED_DIRTY_KB, sharedDirty); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.PRIVATE_DIRTY_KB, privateDirty); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.SHARED_CLEAN_KB, sharedClean); + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.PRIVATE_CLEAN_KB, privateClean); + if (hasSwappedOutPss) { + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.DIRTY_SWAP_PSS_KB, dirtySwapPss); + } else { + proto.write(MemInfoProto.ProcessMemory.MemoryInfo.DIRTY_SWAP_KB, dirtySwap); + } + + proto.end(token); + } + + /** + * Dump mem info data to proto. + */ + public static void dumpMemInfoTable(ProtoOutputStream proto, Debug.MemoryInfo memInfo, + boolean dumpDalvik, boolean dumpSummaryOnly, + long nativeMax, long nativeAllocated, long nativeFree, + long dalvikMax, long dalvikAllocated, long dalvikFree) { + + if (!dumpSummaryOnly) { + final long nhToken = proto.start(MemInfoProto.ProcessMemory.NATIVE_HEAP); + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "Native Heap", + memInfo.nativePss, memInfo.nativeSwappablePss, memInfo.nativeSharedDirty, + memInfo.nativePrivateDirty, memInfo.nativeSharedClean, + memInfo.nativePrivateClean, memInfo.hasSwappedOutPss, + memInfo.nativeSwappedOut, memInfo.nativeSwappedOutPss); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, nativeMax); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB, nativeAllocated); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, nativeFree); + proto.end(nhToken); + + final long dvToken = proto.start(MemInfoProto.ProcessMemory.DALVIK_HEAP); + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "Dalvik Heap", + memInfo.dalvikPss, memInfo.dalvikSwappablePss, memInfo.dalvikSharedDirty, + memInfo.dalvikPrivateDirty, memInfo.dalvikSharedClean, + memInfo.dalvikPrivateClean, memInfo.hasSwappedOutPss, + memInfo.dalvikSwappedOut, memInfo.dalvikSwappedOutPss); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, dalvikMax); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB, dalvikAllocated); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, dalvikFree); + proto.end(dvToken); + + int otherPss = memInfo.otherPss; + int otherSwappablePss = memInfo.otherSwappablePss; + int otherSharedDirty = memInfo.otherSharedDirty; + int otherPrivateDirty = memInfo.otherPrivateDirty; + int otherSharedClean = memInfo.otherSharedClean; + int otherPrivateClean = memInfo.otherPrivateClean; + int otherSwappedOut = memInfo.otherSwappedOut; + int otherSwappedOutPss = memInfo.otherSwappedOutPss; + + for (int i = 0; i < Debug.MemoryInfo.NUM_OTHER_STATS; i++) { + final int myPss = memInfo.getOtherPss(i); + final int mySwappablePss = memInfo.getOtherSwappablePss(i); + final int mySharedDirty = memInfo.getOtherSharedDirty(i); + final int myPrivateDirty = memInfo.getOtherPrivateDirty(i); + final int mySharedClean = memInfo.getOtherSharedClean(i); + final int myPrivateClean = memInfo.getOtherPrivateClean(i); + final int mySwappedOut = memInfo.getOtherSwappedOut(i); + final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i); + if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0 + || mySharedClean != 0 || myPrivateClean != 0 + || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) { + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.OTHER_HEAPS, + Debug.MemoryInfo.getOtherLabel(i), + myPss, mySwappablePss, mySharedDirty, myPrivateDirty, + mySharedClean, myPrivateClean, + memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss); + + otherPss -= myPss; + otherSwappablePss -= mySwappablePss; + otherSharedDirty -= mySharedDirty; + otherPrivateDirty -= myPrivateDirty; + otherSharedClean -= mySharedClean; + otherPrivateClean -= myPrivateClean; + otherSwappedOut -= mySwappedOut; + otherSwappedOutPss -= mySwappedOutPss; + } + } + + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.UNKNOWN_HEAP, "Unknown", + otherPss, otherSwappablePss, + otherSharedDirty, otherPrivateDirty, otherSharedClean, otherPrivateClean, + memInfo.hasSwappedOutPss, otherSwappedOut, otherSwappedOutPss); + final long tToken = proto.start(MemInfoProto.ProcessMemory.TOTAL_HEAP); + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "TOTAL", + memInfo.getTotalPss(), memInfo.getTotalSwappablePss(), + memInfo.getTotalSharedDirty(), memInfo.getTotalPrivateDirty(), + memInfo.getTotalSharedClean(), memInfo.getTotalPrivateClean(), + memInfo.hasSwappedOutPss, memInfo.getTotalSwappedOut(), + memInfo.getTotalSwappedOutPss()); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, nativeMax + dalvikMax); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB, + nativeAllocated + dalvikAllocated); + proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, nativeFree + dalvikFree); + proto.end(tToken); + + if (dumpDalvik) { + for (int i = Debug.MemoryInfo.NUM_OTHER_STATS; + i < Debug.MemoryInfo.NUM_OTHER_STATS + Debug.MemoryInfo.NUM_DVK_STATS; + i++) { + final int myPss = memInfo.getOtherPss(i); + final int mySwappablePss = memInfo.getOtherSwappablePss(i); + final int mySharedDirty = memInfo.getOtherSharedDirty(i); + final int myPrivateDirty = memInfo.getOtherPrivateDirty(i); + final int mySharedClean = memInfo.getOtherSharedClean(i); + final int myPrivateClean = memInfo.getOtherPrivateClean(i); + final int mySwappedOut = memInfo.getOtherSwappedOut(i); + final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i); + if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0 + || mySharedClean != 0 || myPrivateClean != 0 + || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) { + dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.DALVIK_DETAILS, + Debug.MemoryInfo.getOtherLabel(i), + myPss, mySwappablePss, mySharedDirty, myPrivateDirty, + mySharedClean, myPrivateClean, + memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss); + } + } + } + } + + final long asToken = proto.start(MemInfoProto.ProcessMemory.APP_SUMMARY); + proto.write(MemInfoProto.ProcessMemory.AppSummary.JAVA_HEAP_PSS_KB, + memInfo.getSummaryJavaHeap()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.NATIVE_HEAP_PSS_KB, + memInfo.getSummaryNativeHeap()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.CODE_PSS_KB, memInfo.getSummaryCode()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.STACK_PSS_KB, memInfo.getSummaryStack()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.GRAPHICS_PSS_KB, + memInfo.getSummaryGraphics()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.PRIVATE_OTHER_PSS_KB, + memInfo.getSummaryPrivateOther()); + proto.write(MemInfoProto.ProcessMemory.AppSummary.SYSTEM_PSS_KB, + memInfo.getSummarySystem()); + if (memInfo.hasSwappedOutPss) { + proto.write(MemInfoProto.ProcessMemory.AppSummary.TOTAL_SWAP_PSS, + memInfo.getSummaryTotalSwapPss()); + } else { + proto.write(MemInfoProto.ProcessMemory.AppSummary.TOTAL_SWAP_PSS, + memInfo.getSummaryTotalSwap()); + } + proto.end(asToken); + } + public void registerOnActivityPausedListener(Activity activity, OnActivityPausedListener listener) { synchronized (mOnPauseListeners) { @@ -2316,13 +2668,21 @@ public final class ActivityThread extends ClientTransactionHandler { + ", comp=" + name + ", token=" + token); } - return performLaunchActivity(r, null); + // TODO(lifecycler): Can't switch to use #handleLaunchActivity() because it will try to + // call #reportSizeConfigurations(), but the server might not know anything about the + // activity if it was launched from LocalAcvitivyManager. + return performLaunchActivity(r); } public final Activity getActivity(IBinder token) { return mActivities.get(token).activity; } + @Override + public ActivityClientRecord getActivityClient(IBinder token) { + return mActivities.get(token); + } + public final void sendActivityResult( IBinder token, String id, int requestCode, int resultCode, Intent data) { @@ -2330,8 +2690,8 @@ public final class ActivityThread extends ClientTransactionHandler { + " req=" + requestCode + " res=" + resultCode + " data=" + data); ArrayList list = new ArrayList(); list.add(new ResultInfo(id, requestCode, resultCode, data)); - final ClientTransaction clientTransaction = new ClientTransaction(mAppThread, token); - clientTransaction.addCallback(new ActivityResultItem(list)); + final ClientTransaction clientTransaction = ClientTransaction.obtain(mAppThread, token); + clientTransaction.addCallback(ActivityResultItem.obtain(list)); try { mAppThread.scheduleTransaction(clientTransaction); } catch (RemoteException e) { @@ -2390,12 +2750,11 @@ public final class ActivityThread extends ClientTransactionHandler { sendMessage(H.CLEAN_UP_CONTEXT, cci); } - private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { - // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); - + /** Core implementation of activity launch. */ + private Activity performLaunchActivity(ActivityClientRecord r) { ActivityInfo aInfo = r.activityInfo; - if (r.packageInfo == null) { - r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, + if (r.loadedApk == null) { + r.loadedApk = getLoadedApk(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); } @@ -2432,15 +2791,15 @@ public final class ActivityThread extends ClientTransactionHandler { } try { - Application app = r.packageInfo.makeApplication(false, mInstrumentation); + Application app = r.loadedApk.makeApplication(false, mInstrumentation); if (localLOGV) Slog.v(TAG, "Performing launch of " + r); if (localLOGV) Slog.v( TAG, r + ": app=" + app + ", appName=" + app.getPackageName() - + ", pkg=" + r.packageInfo.getPackageName() + + ", pkg=" + r.loadedApk.getPackageName() + ", comp=" + r.intent.getComponent().toShortString() - + ", dir=" + r.packageInfo.getAppDir()); + + ", dir=" + r.loadedApk.getAppDir()); if (activity != null) { CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); @@ -2462,9 +2821,6 @@ public final class ActivityThread extends ClientTransactionHandler { r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); - if (customIntent != null) { - activity.mIntent = customIntent; - } r.lastNonConfigurationInstances = null; checkAndBlockForNetworkAccess(); activity.mStartedActivity = false; @@ -2485,37 +2841,8 @@ public final class ActivityThread extends ClientTransactionHandler { " did not call through to super.onCreate()"); } r.activity = activity; - r.stopped = true; - if (!r.activity.mFinished) { - activity.performStart(); - r.stopped = false; - } - if (!r.activity.mFinished) { - if (r.isPersistable()) { - if (r.state != null || r.persistentState != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, - r.persistentState); - } - } else if (r.state != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); - } - } - if (!r.activity.mFinished) { - activity.mCalled = false; - if (r.isPersistable()) { - mInstrumentation.callActivityOnPostCreate(activity, r.state, - r.persistentState); - } else { - mInstrumentation.callActivityOnPostCreate(activity, r.state); - } - if (!activity.mCalled) { - throw new SuperNotCalledException( - "Activity " + r.intent.getComponent().toShortString() + - " did not call through to super.onPostCreate()"); - } - } } - r.paused = true; + r.setState(ON_CREATE); mActivities.put(r.token, r); @@ -2533,6 +2860,60 @@ public final class ActivityThread extends ClientTransactionHandler { return activity; } + @Override + public void handleStartActivity(ActivityClientRecord r, + PendingTransactionActions pendingActions) { + final Activity activity = r.activity; + if (r.activity == null) { + // TODO(lifecycler): What do we do in this case? + return; + } + if (!r.stopped) { + throw new IllegalStateException("Can't start activity that is not stopped."); + } + if (r.activity.mFinished) { + // TODO(lifecycler): How can this happen? + return; + } + + // Start + activity.performStart(); + r.setState(ON_START); + + if (pendingActions == null) { + // No more work to do. + return; + } + + // Restore instance state + if (pendingActions.shouldRestoreInstanceState()) { + if (r.isPersistable()) { + if (r.state != null || r.persistentState != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, + r.persistentState); + } + } else if (r.state != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); + } + } + + // Call postOnCreate() + if (pendingActions.shouldCallOnPostCreate()) { + activity.mCalled = false; + if (r.isPersistable()) { + mInstrumentation.callActivityOnPostCreate(activity, r.state, + r.persistentState); + } else { + mInstrumentation.callActivityOnPostCreate(activity, r.state); + } + if (!activity.mCalled) { + throw new SuperNotCalledException( + "Activity " + r.intent.getComponent().toShortString() + + " did not call through to super.onPostCreate()"); + } + } + } + /** * Checks if {@link #mNetworkBlockSeq} is {@link #INVALID_PROC_STATE_SEQ} and if so, returns * immediately. Otherwise, makes a blocking call to ActivityManagerService to wait for the @@ -2558,7 +2939,7 @@ public final class ActivityThread extends ClientTransactionHandler { } ContextImpl appContext = ContextImpl.createActivityContext( - this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig); + this, r.loadedApk, r.activityInfo, r.token, displayId, r.overrideConfig); final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); // For debugging purposes, if the activity's package name contains the value of @@ -2566,7 +2947,7 @@ public final class ActivityThread extends ClientTransactionHandler { // its content on a secondary display if there is one. String pkgName = SystemProperties.get("debug.second-display.pkg"); if (pkgName != null && !pkgName.isEmpty() - && r.packageInfo.mPackageName.contains(pkgName)) { + && r.loadedApk.mPackageName.contains(pkgName)) { for (int id : dm.getDisplayIds()) { if (id != Display.DEFAULT_DISPLAY) { Display display = @@ -2579,38 +2960,12 @@ public final class ActivityThread extends ClientTransactionHandler { return appContext; } + /** + * Extended implementation of activity launch. Used when server requests a launch or relaunch. + */ @Override - public void handleLaunchActivity(IBinder token, Intent intent, int ident, ActivityInfo info, - Configuration overrideConfig, CompatibilityInfo compatInfo, String referrer, - IVoiceInteractor voiceInteractor, Bundle state, PersistableBundle persistentState, - List pendingResults, List pendingNewIntents, - boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) { - ActivityClientRecord r = new ActivityClientRecord(); - - r.token = token; - r.ident = ident; - r.intent = intent; - r.referrer = referrer; - r.voiceInteractor = voiceInteractor; - r.activityInfo = info; - r.compatInfo = compatInfo; - r.state = state; - r.persistentState = persistentState; - - r.pendingResults = pendingResults; - r.pendingIntents = pendingNewIntents; - - r.startsNotResumed = notResumed; - r.isForward = isForward; - - r.profilerInfo = profilerInfo; - - r.overrideConfig = overrideConfig; - r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo); - handleLaunchActivity(r, null /* customIntent */, "LAUNCH_ACTIVITY"); - } - - private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) { + public Activity handleLaunchActivity(ActivityClientRecord r, + PendingTransactionActions pendingActions) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); @@ -2633,44 +2988,28 @@ public final class ActivityThread extends ClientTransactionHandler { } WindowManagerGlobal.initialize(); - Activity a = performLaunchActivity(r, customIntent); + final Activity a = performLaunchActivity(r); if (a != null) { r.createdConfig = new Configuration(mConfiguration); reportSizeConfigurations(r); - Bundle oldState = r.state; - handleResumeActivity(r.token, false, r.isForward, - !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason); - - if (!r.activity.mFinished && r.startsNotResumed) { - // The activity manager actually wants this one to start out paused, because it - // needs to be visible but isn't in the foreground. We accomplish this by going - // through the normal startup (because activities expect to go through onResume() - // the first time they run, before their window is displayed), and then pausing it. - // However, in this case we do -not- need to do the full pause cycle (of freezing - // and such) because the activity manager assumes it can just retain the current - // state it has. - performPauseActivityIfNeeded(r, reason); - - // We need to keep around the original state, in case we need to be created again. - // But we only do this for pre-Honeycomb apps, which always save their state when - // pausing, so we can not have them save their state when restarting from a paused - // state. For HC and later, we want to (and can) let the state be saved as the - // normal part of stopping the activity. - if (r.isPreHoneycomb()) { - r.state = oldState; - } + if (!r.activity.mFinished && pendingActions != null) { + pendingActions.setOldState(r.state); + pendingActions.setRestoreInstanceState(true); + pendingActions.setCallOnPostCreate(true); } } else { // If there was an error, for any reason, tell the activity manager to stop us. try { ActivityManager.getService() - .finishActivity(r.token, Activity.RESULT_CANCELED, null, - Activity.DONT_FINISH_TASK_WITH_ACTIVITY); + .finishActivity(r.token, Activity.RESULT_CANCELED, null, + Activity.DONT_FINISH_TASK_WITH_ACTIVITY); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } + + return a; } private void reportSizeConfigurations(ActivityClientRecord r) { @@ -2928,7 +3267,7 @@ public final class ActivityThread extends ClientTransactionHandler { String component = data.intent.getComponent().getClassName(); - LoadedApk packageInfo = getPackageInfoNoCheck( + LoadedApk loadedApk = getLoadedApkNoCheck( data.info.applicationInfo, data.compatInfo); IActivityManager mgr = ActivityManager.getService(); @@ -2937,7 +3276,7 @@ public final class ActivityThread extends ClientTransactionHandler { BroadcastReceiver receiver; ContextImpl context; try { - app = packageInfo.makeApplication(false, mInstrumentation); + app = loadedApk.makeApplication(false, mInstrumentation); context = (ContextImpl) app.getBaseContext(); if (data.info.splitName != null) { context = (ContextImpl) context.createContextForSplit(data.info.splitName); @@ -2946,7 +3285,8 @@ public final class ActivityThread extends ClientTransactionHandler { data.intent.setExtrasClassLoader(cl); data.intent.prepareToEnterProcess(); data.setExtrasClassLoader(cl); - receiver = (BroadcastReceiver)cl.loadClass(component).newInstance(); + receiver = loadedApk.getAppFactory() + .instantiateReceiver(cl, data.info.name, data.intent); } catch (Exception e) { if (DEBUG_BROADCAST) Slog.i(TAG, "Finishing failed broadcast to " + data.intent.getComponent()); @@ -2961,9 +3301,9 @@ public final class ActivityThread extends ClientTransactionHandler { TAG, "Performing receive of " + data.intent + ": app=" + app + ", appName=" + app.getPackageName() - + ", pkg=" + packageInfo.getPackageName() + + ", pkg=" + loadedApk.getPackageName() + ", comp=" + data.intent.getComponent().toShortString() - + ", dir=" + packageInfo.getAppDir()); + + ", dir=" + loadedApk.getAppDir()); sCurrentBroadcastIntent.set(data.intent); receiver.setPendingResult(data); @@ -3008,8 +3348,8 @@ public final class ActivityThread extends ClientTransactionHandler { unscheduleGcIdler(); // instantiate the BackupAgent class named in the manifest - LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo); - String packageName = packageInfo.mPackageName; + LoadedApk loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo); + String packageName = loadedApk.mPackageName; if (packageName == null) { Slog.d(TAG, "Asked to create backup agent for nonexistent package"); return; @@ -3035,11 +3375,11 @@ public final class ActivityThread extends ClientTransactionHandler { try { if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname); - java.lang.ClassLoader cl = packageInfo.getClassLoader(); + java.lang.ClassLoader cl = loadedApk.getClassLoader(); agent = (BackupAgent) cl.loadClass(classname).newInstance(); // set up the agent's context - ContextImpl context = ContextImpl.createAppContext(this, packageInfo); + ContextImpl context = ContextImpl.createAppContext(this, loadedApk); context.setOuterContext(agent); agent.attach(context); @@ -3075,8 +3415,8 @@ public final class ActivityThread extends ClientTransactionHandler { private void handleDestroyBackupAgent(CreateBackupAgentData data) { if (DEBUG_BACKUP) Slog.v(TAG, "handleDestroyBackupAgent: " + data); - LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo); - String packageName = packageInfo.mPackageName; + LoadedApk loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo); + String packageName = loadedApk.mPackageName; BackupAgent agent = mBackupAgents.get(packageName); if (agent != null) { try { @@ -3096,12 +3436,13 @@ public final class ActivityThread extends ClientTransactionHandler { // we are back active so skip it. unscheduleGcIdler(); - LoadedApk packageInfo = getPackageInfoNoCheck( + LoadedApk loadedApk = getLoadedApkNoCheck( data.info.applicationInfo, data.compatInfo); Service service = null; try { - java.lang.ClassLoader cl = packageInfo.getClassLoader(); - service = (Service) cl.loadClass(data.info.name).newInstance(); + java.lang.ClassLoader cl = loadedApk.getClassLoader(); + service = loadedApk.getAppFactory() + .instantiateService(cl, data.info.name, data.intent); } catch (Exception e) { if (!mInstrumentation.onException(service, e)) { throw new RuntimeException( @@ -3113,10 +3454,10 @@ public final class ActivityThread extends ClientTransactionHandler { try { if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name); - ContextImpl context = ContextImpl.createAppContext(this, packageInfo); + ContextImpl context = ContextImpl.createAppContext(this, loadedApk); context.setOuterContext(service); - Application app = packageInfo.makeApplication(false, mInstrumentation); + Application app = loadedApk.makeApplication(false, mInstrumentation); service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService()); service.onCreate(); @@ -3314,8 +3655,7 @@ public final class ActivityThread extends ClientTransactionHandler { //Slog.i(TAG, "Running services: " + mServices); } - public final ActivityClientRecord performResumeActivity(IBinder token, - boolean clearHide, String reason) { + ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide, String reason) { ActivityClientRecord r = mActivities.get(token); if (localLOGV) Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished); @@ -3355,10 +3695,9 @@ public final class ActivityThread extends ClientTransactionHandler { EventLog.writeEvent(LOG_AM_ON_RESUME_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), reason); - r.paused = false; - r.stopped = false; r.state = null; r.persistentState = null; + r.setState(ON_RESUME); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -3390,19 +3729,14 @@ public final class ActivityThread extends ClientTransactionHandler { @Override public void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, - boolean reallyResume, int seq, String reason) { - ActivityClientRecord r = mActivities.get(token); - if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) { - return; - } - + String reason) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); mSomeActivitiesChanged = true; // TODO Push resumeArgs into the activity for consideration - r = performResumeActivity(token, clearHide, reason); + final ActivityClientRecord r = performResumeActivity(token, clearHide, reason); if (r != null) { final Activity a = r.activity; @@ -3514,16 +3848,6 @@ public final class ActivityThread extends ClientTransactionHandler { Looper.myQueue().addIdleHandler(new Idler()); } r.onlyLocalRequest = false; - - // Tell the activity manager we have resumed. - if (reallyResume) { - try { - ActivityManager.getService().activityResumed(token); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - } else { // If an exception was thrown when trying to resume, then // just end this activity. @@ -3594,21 +3918,17 @@ public final class ActivityThread extends ClientTransactionHandler { } @Override - public void handlePauseActivity(IBinder token, boolean finished, - boolean userLeaving, int configChanges, boolean dontReport, int seq) { + public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, + int configChanges, boolean dontReport, PendingTransactionActions pendingActions) { ActivityClientRecord r = mActivities.get(token); - if (DEBUG_ORDER) Slog.d(TAG, "handlePauseActivity " + r + ", seq: " + seq); - if (!checkAndUpdateLifecycleSeq(seq, r, "pauseActivity")) { - return; - } if (r != null) { - //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r); if (userLeaving) { performUserLeavingActivity(r); } r.activity.mConfigChangeFlags |= configChanges; - performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity"); + performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity", + pendingActions); // Make sure any pending writes are now committed. if (r.isPreHoneycomb()) { @@ -3632,13 +3952,15 @@ public final class ActivityThread extends ClientTransactionHandler { } final Bundle performPauseActivity(IBinder token, boolean finished, - boolean saveState, String reason) { + boolean saveState, String reason, PendingTransactionActions pendingActions) { ActivityClientRecord r = mActivities.get(token); - return r != null ? performPauseActivity(r, finished, saveState, reason) : null; + return r != null + ? performPauseActivity(r, finished, saveState, reason, pendingActions) + : null; } - final Bundle performPauseActivity(ActivityClientRecord r, boolean finished, - boolean saveState, String reason) { + private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, boolean saveState, + String reason, PendingTransactionActions pendingActions) { if (r.paused) { if (r.activity.mFinished) { // If we are finishing, we won't call onResume() in certain cases. @@ -3672,6 +3994,18 @@ public final class ActivityThread extends ClientTransactionHandler { listeners.get(i).onPaused(r.activity); } + final Bundle oldState = pendingActions != null ? pendingActions.getOldState() : null; + if (oldState != null) { + // We need to keep around the original state, in case we need to be created again. + // But we only do this for pre-Honeycomb apps, which always save their state when + // pausing, so we can not have them save their state when restarting from a paused + // state. For HC and later, we want to (and can) let the state be saved as the + // normal part of stopping the activity. + if (r.isPreHoneycomb()) { + r.state = oldState; + } + } + return !r.activity.mFinished && saveState ? r.state : null; } @@ -3698,7 +4032,7 @@ public final class ActivityThread extends ClientTransactionHandler { + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } - r.paused = true; + r.setState(ON_PAUSE); } final void performStopActivity(IBinder token, boolean saveState, String reason) { @@ -3706,37 +4040,6 @@ public final class ActivityThread extends ClientTransactionHandler { performStopActivityInner(r, null, false, saveState, reason); } - private static class StopInfo implements Runnable { - ActivityClientRecord activity; - Bundle state; - PersistableBundle persistentState; - CharSequence description; - - @Override public void run() { - // Tell activity manager we have been stopped. - try { - if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity); - ActivityManager.getService().activityStopped( - activity.token, state, persistentState, description); - } catch (RemoteException ex) { - // Dump statistics about bundle to help developers debug - final LogWriter writer = new LogWriter(Log.WARN, TAG); - final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - pw.println("Bundle stats:"); - Bundle.dumpStats(pw, state); - pw.println("PersistableBundle stats:"); - Bundle.dumpStats(pw, persistentState); - - if (ex instanceof TransactionTooLargeException - && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { - Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex); - return; - } - throw ex.rethrowFromSystemServer(); - } - } - } - private static final class ProviderRefCount { public final ContentProviderHolder holder; public final ProviderClientRecord client; @@ -3767,8 +4070,8 @@ public final class ActivityThread extends ClientTransactionHandler { * For the client, we want to call onStop()/onStart() to indicate when * the activity's UI visibility changes. */ - private void performStopActivityInner(ActivityClientRecord r, - StopInfo info, boolean keepShown, boolean saveState, String reason) { + private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShown, + boolean saveState, String reason) { if (localLOGV) Slog.v(TAG, "Performing stop of " + r); if (r != null) { if (!keepShown && r.stopped) { @@ -3793,7 +4096,7 @@ public final class ActivityThread extends ClientTransactionHandler { // First create a thumbnail for the activity... // For now, don't create the thumbnail here; we are // doing that by doing a screen snapshot. - info.description = r.activity.onCreateDescription(); + info.setDescription(r.activity.onCreateDescription()); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -3823,7 +4126,7 @@ public final class ActivityThread extends ClientTransactionHandler { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), reason); } @@ -3859,15 +4162,13 @@ public final class ActivityThread extends ClientTransactionHandler { } @Override - public void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { - ActivityClientRecord r = mActivities.get(token); - if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) { - return; - } + public void handleStopActivity(IBinder token, boolean show, int configChanges, + PendingTransactionActions pendingActions) { + final ActivityClientRecord r = mActivities.get(token); r.activity.mConfigChangeFlags |= configChanges; - StopInfo info = new StopInfo(); - performStopActivityInner(r, info, show, true, "handleStopActivity"); + final StopInfo stopInfo = new StopInfo(); + performStopActivityInner(r, stopInfo, show, true, "handleStopActivity"); if (localLOGV) Slog.v( TAG, "Finishing stop of " + r + ": show=" + show @@ -3880,37 +4181,32 @@ public final class ActivityThread extends ClientTransactionHandler { QueuedWork.waitToFinish(); } - // Schedule the call to tell the activity manager we have - // stopped. We don't do this immediately, because we want to - // have a chance for any other pending work (in particular memory - // trim requests) to complete before you tell the activity - // manager to proceed and allow us to go fully into the background. - info.activity = r; - info.state = r.state; - info.persistentState = r.persistentState; - mH.post(info); + stopInfo.setActivity(r); + stopInfo.setState(r.state); + stopInfo.setPersistentState(r.persistentState); + pendingActions.setStopInfo(stopInfo); mSomeActivitiesChanged = true; } - private static boolean checkAndUpdateLifecycleSeq(int seq, ActivityClientRecord r, - String action) { - if (r == null) { - return true; - } - if (seq < r.lastProcessedSeq) { - if (DEBUG_ORDER) Slog.d(TAG, action + " for " + r + " ignored, because seq=" + seq - + " < mCurrentLifecycleSeq=" + r.lastProcessedSeq); - return false; - } - r.lastProcessedSeq = seq; - return true; + /** + * Schedule the call to tell the activity manager we have stopped. We don't do this + * immediately, because we want to have a chance for any other pending work (in particular + * memory trim requests) to complete before you tell the activity manager to proceed and allow + * us to go fully into the background. + */ + @Override + public void reportStop(PendingTransactionActions pendingActions) { + mH.post(pendingActions.getStopInfo()); } - final void performRestartActivity(IBinder token) { + @Override + public void performRestartActivity(IBinder token, boolean start) { ActivityClientRecord r = mActivities.get(token); if (r.stopped) { - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(start); + if (start) { + r.setState(ON_START); + } } } @@ -3930,8 +4226,8 @@ public final class ActivityThread extends ClientTransactionHandler { // we are back active so skip it. unscheduleGcIdler(); - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(true /* start */); + r.setState(ON_START); } if (r.activity.mDecor != null) { if (false) Slog.v( @@ -3969,7 +4265,7 @@ public final class ActivityThread extends ClientTransactionHandler { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), "sleeping"); } @@ -3987,8 +4283,8 @@ public final class ActivityThread extends ClientTransactionHandler { } } else { if (r.stopped && r.activity.mVisibleFromServer) { - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(true /* start */); + r.setState(ON_START); } } } @@ -4025,11 +4321,11 @@ public final class ActivityThread extends ClientTransactionHandler { } private void handleUpdatePackageCompatibilityInfo(UpdateCompatibilityData data) { - LoadedApk apk = peekPackageInfo(data.pkg, false); + LoadedApk apk = peekLoadedApk(data.pkg, false); if (apk != null) { apk.setCompatibilityInfo(data.info); } - apk = peekPackageInfo(data.pkg, true); + apk = peekLoadedApk(data.pkg, true); if (apk != null) { apk.setCompatibilityInfo(data.info); } @@ -4105,11 +4401,8 @@ public final class ActivityThread extends ClientTransactionHandler { } } - public final ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing) { - return performDestroyActivity(token, finishing, 0, false); - } - - private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, + /** Core implementation of activity destroy call. */ + ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance) { ActivityClientRecord r = mActivities.get(token); Class activityClass = null; @@ -4136,7 +4429,7 @@ public final class ActivityThread extends ClientTransactionHandler { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), "destroy"); } @@ -4173,6 +4466,7 @@ public final class ActivityThread extends ClientTransactionHandler { + ": " + e.toString(), e); } } + r.setState(ON_DESTROY); } mActivities.remove(token); StrictMode.decrementExpectedActivityCount(activityClass); @@ -4333,10 +4627,7 @@ public final class ActivityThread extends ClientTransactionHandler { target.overrideConfig = overrideConfig; } target.pendingConfigChanges |= configChanges; - target.relaunchSeq = getLifecycleSeq(); } - if (DEBUG_ORDER) Slog.d(TAG, "relaunchActivity " + ActivityThread.this + ", target " - + target + " operation received seq: " + target.relaunchSeq); } private void handleRelaunchActivity(ActivityClientRecord tmp) { @@ -4381,12 +4672,6 @@ public final class ActivityThread extends ClientTransactionHandler { } } - if (tmp.lastProcessedSeq > tmp.relaunchSeq) { - Slog.wtf(TAG, "For some reason target: " + tmp + " has lower sequence: " - + tmp.relaunchSeq + " than current sequence: " + tmp.lastProcessedSeq); - } else { - tmp.lastProcessedSeq = tmp.relaunchSeq; - } if (tmp.createdConfig != null) { // If the activity manager is passing us its current config, // assume that is really what we want regardless of what we @@ -4427,9 +4712,6 @@ public final class ActivityThread extends ClientTransactionHandler { r.activity.mConfigChangeFlags |= configChanges; r.onlyLocalRequest = tmp.onlyLocalRequest; r.mPreserveWindow = tmp.mPreserveWindow; - r.lastProcessedSeq = tmp.lastProcessedSeq; - r.relaunchSeq = tmp.relaunchSeq; - Intent currentIntent = r.activity.mIntent; r.activity.mChangingConfigurations = true; @@ -4455,7 +4737,8 @@ public final class ActivityThread extends ClientTransactionHandler { // Need to ensure state is saved. if (!r.paused) { - performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity"); + performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity", + null /* pendingActions */); } if (r.state == null && !r.stopped && !r.isPreHoneycomb()) { callCallActivityOnSaveInstanceState(r); @@ -4485,7 +4768,15 @@ public final class ActivityThread extends ClientTransactionHandler { r.startsNotResumed = tmp.startsNotResumed; r.overrideConfig = tmp.overrideConfig; - handleLaunchActivity(r, currentIntent, "handleRelaunchActivity"); + // TODO(lifecycler): Move relaunch to lifecycler. + PendingTransactionActions pendingActions = new PendingTransactionActions(); + handleLaunchActivity(r, pendingActions); + handleStartActivity(r, pendingActions); + handleResumeActivity(r.token, false /* clearHide */, r.isForward, "relaunch"); + if (r.startsNotResumed) { + performPauseActivity(r, false /* finished */, r.isPreHoneycomb(), "relaunch", + pendingActions); + } if (!tmp.onlyLocalRequest) { try { @@ -4528,7 +4819,7 @@ public final class ActivityThread extends ClientTransactionHandler { if (a != null) { Configuration thisConfig = applyConfigCompatMainThread( mCurDefaultDisplayDpi, newConfig, - ar.packageInfo.getCompatibilityInfo()); + ar.loadedApk.getCompatibilityInfo()); if (!ar.activity.mFinished && (allActivities || !ar.paused)) { // If the activity is currently resumed, its configuration // needs to change right now. @@ -5014,7 +5305,7 @@ public final class ActivityThread extends ClientTransactionHandler { } final void handleDispatchPackageBroadcast(int cmd, String[] packages) { - boolean hasPkgInfo = false; + boolean hasLoadedApk = false; switch (cmd) { case ApplicationThreadConstants.PACKAGE_REMOVED: case ApplicationThreadConstants.PACKAGE_REMOVED_DONT_KILL: @@ -5025,14 +5316,14 @@ public final class ActivityThread extends ClientTransactionHandler { } synchronized (mResourcesManager) { for (int i = packages.length - 1; i >= 0; i--) { - if (!hasPkgInfo) { + if (!hasLoadedApk) { WeakReference ref = mPackages.get(packages[i]); if (ref != null && ref.get() != null) { - hasPkgInfo = true; + hasLoadedApk = true; } else { ref = mResourcePackages.get(packages[i]); if (ref != null && ref.get() != null) { - hasPkgInfo = true; + hasLoadedApk = true; } } } @@ -5052,21 +5343,21 @@ public final class ActivityThread extends ClientTransactionHandler { synchronized (mResourcesManager) { for (int i = packages.length - 1; i >= 0; i--) { WeakReference ref = mPackages.get(packages[i]); - LoadedApk pkgInfo = ref != null ? ref.get() : null; - if (pkgInfo != null) { - hasPkgInfo = true; + LoadedApk loadedApk = ref != null ? ref.get() : null; + if (loadedApk != null) { + hasLoadedApk = true; } else { ref = mResourcePackages.get(packages[i]); - pkgInfo = ref != null ? ref.get() : null; - if (pkgInfo != null) { - hasPkgInfo = true; + loadedApk = ref != null ? ref.get() : null; + if (loadedApk != null) { + hasLoadedApk = true; } } // If the package is being replaced, yet it still has a valid // LoadedApk object, the package was updated with _DONT_KILL. // Adjust it's internal references to the application info and // resources. - if (pkgInfo != null) { + if (loadedApk != null) { try { final String packageName = packages[i]; final ApplicationInfo aInfo = @@ -5080,13 +5371,13 @@ public final class ActivityThread extends ClientTransactionHandler { if (ar.activityInfo.applicationInfo.packageName .equals(packageName)) { ar.activityInfo.applicationInfo = aInfo; - ar.packageInfo = pkgInfo; + ar.loadedApk = loadedApk; } } } final List oldPaths = sPackageManager.getPreviousCodePaths(packageName); - pkgInfo.updateApplicationInfo(aInfo, oldPaths); + loadedApk.updateApplicationInfo(aInfo, oldPaths); } catch (RemoteException e) { } } @@ -5095,7 +5386,7 @@ public final class ActivityThread extends ClientTransactionHandler { break; } } - ApplicationPackageManager.handlePackageBroadcast(cmd, packages, hasPkgInfo); + ApplicationPackageManager.handlePackageBroadcast(cmd, packages, hasLoadedApk); } final void handleLowMemory() { @@ -5299,7 +5590,7 @@ public final class ActivityThread extends ClientTransactionHandler { applyCompatConfiguration(mCurDefaultDisplayDpi); } - data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo); + data.loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo); /** * Switch this process to density compatibility mode if needed. @@ -5343,7 +5634,7 @@ public final class ActivityThread extends ClientTransactionHandler { // XXX should have option to change the port. Debug.changeDebugPort(8100); if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) { - Slog.w(TAG, "Application " + data.info.getPackageName() + Slog.w(TAG, "Application " + data.loadedApk.getPackageName() + " is waiting for the debugger on port 8100..."); IActivityManager mgr = ActivityManager.getService(); @@ -5362,7 +5653,7 @@ public final class ActivityThread extends ClientTransactionHandler { } } else { - Slog.w(TAG, "Application " + data.info.getPackageName() + Slog.w(TAG, "Application " + data.loadedApk.getPackageName() + " can be debugged on port 8100..."); } } @@ -5410,14 +5701,14 @@ public final class ActivityThread extends ClientTransactionHandler { mInstrumentationAppDir = ii.sourceDir; mInstrumentationSplitAppDirs = ii.splitSourceDirs; mInstrumentationLibDir = getInstrumentationLibrary(data.appInfo, ii); - mInstrumentedAppDir = data.info.getAppDir(); - mInstrumentedSplitAppDirs = data.info.getSplitAppDirs(); - mInstrumentedLibDir = data.info.getLibDir(); + mInstrumentedAppDir = data.loadedApk.getAppDir(); + mInstrumentedSplitAppDirs = data.loadedApk.getSplitAppDirs(); + mInstrumentedLibDir = data.loadedApk.getLibDir(); } else { ii = null; } - final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); + final ContextImpl appContext = ContextImpl.createAppContext(this, data.loadedApk); updateLocaleListFromAppContext(appContext, mResourcesManager.getConfiguration().getLocales()); @@ -5449,12 +5740,21 @@ public final class ActivityThread extends ClientTransactionHandler { // Continue loading instrumentation. if (ii != null) { - final ApplicationInfo instrApp = new ApplicationInfo(); + ApplicationInfo instrApp; + try { + instrApp = getPackageManager().getApplicationInfo(ii.packageName, 0, + UserHandle.myUserId()); + } catch (RemoteException e) { + instrApp = null; + } + if (instrApp == null) { + instrApp = new ApplicationInfo(); + } ii.copyTo(instrApp); instrApp.initForUser(UserHandle.myUserId()); - final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo, + final LoadedApk loadedApk = getLoadedApk(instrApp, data.compatInfo, appContext.getClassLoader(), false, true, false); - final ContextImpl instrContext = ContextImpl.createAppContext(this, pi); + final ContextImpl instrContext = ContextImpl.createAppContext(this, loadedApk); try { final ClassLoader cl = instrContext.getClassLoader(); @@ -5479,6 +5779,7 @@ public final class ActivityThread extends ClientTransactionHandler { } } else { mInstrumentation = new Instrumentation(); + mInstrumentation.basicInit(this); } if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) { @@ -5498,7 +5799,7 @@ public final class ActivityThread extends ClientTransactionHandler { try { // If the app is being launched for full backup or restore, bring it up in // a restricted environment with the base application class. - app = data.info.makeApplication(data.restrictedBackupMode, null); + app = data.loadedApk.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; // don't bring up providers in restricted mode; they may depend on the @@ -5552,7 +5853,7 @@ public final class ActivityThread extends ClientTransactionHandler { final int preloadedFontsResource = info.metaData.getInt( ApplicationInfo.METADATA_PRELOADED_FONTS, 0); if (preloadedFontsResource != 0) { - data.info.getResources().preloadFonts(preloadedFontsResource); + data.loadedApk.getResources().preloadFonts(preloadedFontsResource); } } } catch (RemoteException e) { @@ -6010,8 +6311,13 @@ public final class ActivityThread extends ClientTransactionHandler { try { final java.lang.ClassLoader cl = c.getClassLoader(); - localProvider = (ContentProvider)cl. - loadClass(info.name).newInstance(); + LoadedApk loadedApk = peekLoadedApk(ai.packageName, true); + if (loadedApk == null) { + // System startup case. + loadedApk = getSystemContext().mLoadedApk; + } + localProvider = loadedApk.getAppFactory() + .instantiateProvider(cl, info.name); provider = localProvider.getIContentProvider(); if (provider == null) { Slog.e(TAG, "Failed to instantiate class " + @@ -6109,7 +6415,7 @@ public final class ActivityThread extends ClientTransactionHandler { System.exit(0); } - private void attach(boolean system) { + private void attach(boolean system, long startSeq) { sCurrentActivityThread = this; mSystemThread = system; if (!system) { @@ -6124,7 +6430,7 @@ public final class ActivityThread extends ClientTransactionHandler { RuntimeInit.setApplicationObject(mAppThread.asBinder()); final IActivityManager mgr = ActivityManager.getService(); try { - mgr.attachApplication(mAppThread); + mgr.attachApplication(mAppThread, startSeq); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -6157,9 +6463,10 @@ public final class ActivityThread extends ClientTransactionHandler { UserHandle.myUserId()); try { mInstrumentation = new Instrumentation(); + mInstrumentation.basicInit(this); ContextImpl context = ContextImpl.createAppContext( - this, getSystemContext().mPackageInfo); - mInitialApplication = context.mPackageInfo.makeApplication(true, null); + this, getSystemContext().mLoadedApk); + mInitialApplication = context.mLoadedApk.makeApplication(true, null); mInitialApplication.onCreate(); } catch (Exception e) { throw new RuntimeException( @@ -6202,7 +6509,7 @@ public final class ActivityThread extends ClientTransactionHandler { ThreadedRenderer.enableForegroundTrimming(); } ActivityThread thread = new ActivityThread(); - thread.attach(true); + thread.attach(true, 0); return thread; } @@ -6274,8 +6581,19 @@ public final class ActivityThread extends ClientTransactionHandler { Looper.prepareMainLooper(); + // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line. + // It will be in the format "seq=114" + long startSeq = 0; + if (args != null) { + for (int i = args.length - 1; i >= 0; --i) { + if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { + startSeq = Long.parseLong( + args[i].substring(PROC_START_SEQ_IDENT.length())); + } + } + } ActivityThread thread = new ActivityThread(); - thread.attach(false); + thread.attach(false, startSeq); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); diff --git a/android/app/AlarmManager.java b/android/app/AlarmManager.java index 55f9e289..382719b4 100644 --- a/android/app/AlarmManager.java +++ b/android/app/AlarmManager.java @@ -568,9 +568,21 @@ public class AlarmManager { } /** - * Schedule an alarm that represents an alarm clock. + * Schedule an alarm that represents an alarm clock, which will be used to notify the user + * when it goes off. The expectation is that when this alarm triggers, the application will + * further wake up the device to tell the user about the alarm -- turning on the screen, + * playing a sound, vibrating, etc. As such, the system will typically also use the + * information supplied here to tell the user about this upcoming alarm if appropriate. * - * The system may choose to display information about this alarm to the user. + *

Due to the nature of this kind of alarm, similar to {@link #setExactAndAllowWhileIdle}, + * these alarms will be allowed to trigger even if the system is in a low-power idle + * (a.k.a. doze) mode. The system may also do some prep-work when it sees that such an + * alarm coming up, to reduce the amount of background work that could happen if this + * causes the device to fully wake up -- this is to avoid situations such as a large number + * of devices having an alarm set at the same time in the morning, all waking up at that + * time and suddenly swamping the network with pending background work. As such, these + * types of alarms can be extremely expensive on battery use and should only be used for + * their intended purpose.

* *

* This method is like {@link #setExact(int, long, PendingIntent)}, but implies @@ -783,9 +795,9 @@ public class AlarmManager { /** * Like {@link #set(int, long, PendingIntent)}, but this alarm will be allowed to execute - * even when the system is in low-power idle modes. This type of alarm must only - * be used for situations where it is actually required that the alarm go off while in - * idle -- a reasonable example would be for a calendar notification that should make a + * even when the system is in low-power idle (a.k.a. doze) modes. This type of alarm must + * only be used for situations where it is actually required that the alarm go off while + * in idle -- a reasonable example would be for a calendar notification that should make a * sound so the user is aware of it. When the alarm is dispatched, the app will also be * added to the system's temporary whitelist for approximately 10 seconds to allow that * application to acquire further wake locks in which to complete its work.

diff --git a/android/app/AppComponentFactory.java b/android/app/AppComponentFactory.java new file mode 100644 index 00000000..4df73799 --- /dev/null +++ b/android/app/AppComponentFactory.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.content.Intent; + +/** + * Interface used to control the instantiation of manifest elements. + * + * @see #instantiateApplication + * @see #instantiateActivity + * @see #instantiateService + * @see #instantiateReceiver + * @see #instantiateProvider + */ +public class AppComponentFactory { + + /** + * Allows application to override the creation of the application object. This can be used to + * perform things such as dependency injection or class loader changes to these + * classes. + * + * @param cl The default classloader to use for instantiation. + * @param className The class to be instantiated. + */ + public @NonNull Application instantiateApplication(@NonNull ClassLoader cl, + @NonNull String className) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return (Application) cl.loadClass(className).newInstance(); + } + + /** + * Allows application to override the creation of activities. This can be used to + * perform things such as dependency injection or class loader changes to these + * classes. + * + * @param cl The default classloader to use for instantiation. + * @param className The class to be instantiated. + * @param intent Intent creating the class. + */ + public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, + @Nullable Intent intent) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return (Activity) cl.loadClass(className).newInstance(); + } + + /** + * Allows application to override the creation of receivers. This can be used to + * perform things such as dependency injection or class loader changes to these + * classes. + * + * @param cl The default classloader to use for instantiation. + * @param className The class to be instantiated. + * @param intent Intent creating the class. + */ + public @NonNull BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl, + @NonNull String className, @Nullable Intent intent) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return (BroadcastReceiver) cl.loadClass(className).newInstance(); + } + + /** + * Allows application to override the creation of services. This can be used to + * perform things such as dependency injection or class loader changes to these + * classes. + * + * @param cl The default classloader to use for instantiation. + * @param className The class to be instantiated. + * @param intent Intent creating the class. + */ + public @NonNull Service instantiateService(@NonNull ClassLoader cl, + @NonNull String className, @Nullable Intent intent) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return (Service) cl.loadClass(className).newInstance(); + } + + /** + * Allows application to override the creation of providers. This can be used to + * perform things such as dependency injection or class loader changes to these + * classes. + * + * @param cl The default classloader to use for instantiation. + * @param className The class to be instantiated. + */ + public @NonNull ContentProvider instantiateProvider(@NonNull ClassLoader cl, + @NonNull String className) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return (ContentProvider) cl.loadClass(className).newInstance(); + } + + /** + * @hide + */ + public static AppComponentFactory DEFAULT = new AppComponentFactory(); +} diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java index b6fb1201..ea22d332 100644 --- a/android/app/AppOpsManager.java +++ b/android/app/AppOpsManager.java @@ -160,7 +160,7 @@ public class AppOpsManager { public static final int OP_WRITE_ICC_SMS = 22; /** @hide */ public static final int OP_WRITE_SETTINGS = 23; - /** @hide */ + /** @hide Required to draw on top of other apps. */ public static final int OP_SYSTEM_ALERT_WINDOW = 24; /** @hide */ public static final int OP_ACCESS_NOTIFICATIONS = 25; @@ -256,8 +256,12 @@ public class AppOpsManager { public static final int OP_RUN_ANY_IN_BACKGROUND = 70; /** @hide Change Wi-Fi connectivity state */ public static final int OP_CHANGE_WIFI_STATE = 71; + /** @hide Request package deletion through package installer */ + public static final int OP_REQUEST_DELETE_PACKAGES = 72; + /** @hide Bind an accessibility service. */ + public static final int OP_BIND_ACCESSIBILITY_SERVICE = 73; /** @hide */ - public static final int _NUM_OP = 72; + public static final int _NUM_OP = 74; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -410,6 +414,7 @@ public class AppOpsManager { OP_CAMERA, // Body sensors OP_BODY_SENSORS, + OP_REQUEST_DELETE_PACKAGES, // APPOP PERMISSIONS OP_ACCESS_NOTIFICATIONS, @@ -499,6 +504,8 @@ public class AppOpsManager { OP_ANSWER_PHONE_CALLS, OP_RUN_ANY_IN_BACKGROUND, OP_CHANGE_WIFI_STATE, + OP_REQUEST_DELETE_PACKAGES, + OP_BIND_ACCESSIBILITY_SERVICE, }; /** @@ -578,6 +585,8 @@ public class AppOpsManager { OPSTR_ANSWER_PHONE_CALLS, null, // OP_RUN_ANY_IN_BACKGROUND null, // OP_CHANGE_WIFI_STATE + null, // OP_REQUEST_DELETE_PACKAGES + null, // OP_BIND_ACCESSIBILITY_SERVICE }; /** @@ -657,6 +666,8 @@ public class AppOpsManager { "ANSWER_PHONE_CALLS", "RUN_ANY_IN_BACKGROUND", "CHANGE_WIFI_STATE", + "REQUEST_DELETE_PACKAGES", + "BIND_ACCESSIBILITY_SERVICE", }; /** @@ -736,6 +747,8 @@ public class AppOpsManager { Manifest.permission.ANSWER_PHONE_CALLS, null, // no permission for OP_RUN_ANY_IN_BACKGROUND Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.REQUEST_DELETE_PACKAGES, + Manifest.permission.BIND_ACCESSIBILITY_SERVICE, }; /** @@ -816,6 +829,8 @@ public class AppOpsManager { null, // ANSWER_PHONE_CALLS null, // OP_RUN_ANY_IN_BACKGROUND null, // OP_CHANGE_WIFI_STATE + null, // REQUEST_DELETE_PACKAGES + null, // OP_BIND_ACCESSIBILITY_SERVICE }; /** @@ -895,6 +910,8 @@ public class AppOpsManager { false, // ANSWER_PHONE_CALLS false, // OP_RUN_ANY_IN_BACKGROUND false, // OP_CHANGE_WIFI_STATE + false, // OP_REQUEST_DELETE_PACKAGES + false, // OP_BIND_ACCESSIBILITY_SERVICE }; /** @@ -973,6 +990,8 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS AppOpsManager.MODE_ALLOWED, // OP_RUN_ANY_IN_BACKGROUND AppOpsManager.MODE_ALLOWED, // OP_CHANGE_WIFI_STATE + AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES + AppOpsManager.MODE_ALLOWED, // OP_BIND_ACCESSIBILITY_SERVICE }; /** @@ -1055,6 +1074,8 @@ public class AppOpsManager { false, // ANSWER_PHONE_CALLS false, // OP_RUN_ANY_IN_BACKGROUND false, // OP_CHANGE_WIFI_STATE + false, // OP_REQUEST_DELETE_PACKAGES + false, // OP_BIND_ACCESSIBILITY_SERVICE }; /** diff --git a/android/app/Application.java b/android/app/Application.java index 156df36a..5822f5c8 100644 --- a/android/app/Application.java +++ b/android/app/Application.java @@ -187,7 +187,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 { */ /* package */ final void attach(Context context) { attachBaseContext(context); - mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; + mLoadedApk = ContextImpl.getImpl(context).mLoadedApk; } /* package */ void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) { diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java index 005b7c38..8641a21a 100644 --- a/android/app/ApplicationPackageManager.java +++ b/android/app/ApplicationPackageManager.java @@ -55,6 +55,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; +import android.content.pm.dex.ArtManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Bitmap; @@ -121,6 +122,8 @@ public class ApplicationPackageManager extends PackageManager { private UserManager mUserManager; @GuardedBy("mLock") private PackageInstaller mInstaller; + @GuardedBy("mLock") + private ArtManager mArtManager; @GuardedBy("mDelegates") private final ArrayList mDelegates = new ArrayList<>(); @@ -1378,7 +1381,7 @@ public class ApplicationPackageManager extends PackageManager { sameUid ? app.sourceDir : app.publicSourceDir, sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs, app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY, - mContext.mPackageInfo); + mContext.mLoadedApk); if (r != null) { return r; } @@ -2750,4 +2753,18 @@ public class ApplicationPackageManager extends PackageManager { throw e.rethrowAsRuntimeException(); } } + + @Override + public ArtManager getArtManager() { + synchronized (mLock) { + if (mArtManager == null) { + try { + mArtManager = new ArtManager(mPM.getArtManager()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return mArtManager; + } + } } diff --git a/android/app/ClientTransactionHandler.java b/android/app/ClientTransactionHandler.java index f7f4c716..45c0e0cd 100644 --- a/android/app/ClientTransactionHandler.java +++ b/android/app/ClientTransactionHandler.java @@ -16,15 +16,12 @@ package android.app; import android.app.servertransaction.ClientTransaction; -import android.content.Intent; -import android.content.pm.ActivityInfo; +import android.app.servertransaction.PendingTransactionActions; +import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; -import android.os.Bundle; import android.os.IBinder; -import android.os.PersistableBundle; -import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; import java.util.List; @@ -40,7 +37,7 @@ public abstract class ClientTransactionHandler { /** Prepare and schedule transaction for execution. */ void scheduleTransaction(ClientTransaction transaction) { - transaction.prepare(this); + transaction.preExecute(this); sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction); } @@ -50,9 +47,6 @@ public abstract class ClientTransactionHandler { // Prepare phase related logic and handlers. Methods that inform about about pending changes or // do other internal bookkeeping. - /** Get current lifecycle request number to maintain correct ordering. */ - public abstract int getLifecycleSeq(); - /** Set pending config in case it will be updated by other transaction item. */ public abstract void updatePendingConfiguration(Configuration config); @@ -69,15 +63,21 @@ public abstract class ClientTransactionHandler { /** Pause the activity. */ public abstract void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, - int configChanges, boolean dontReport, int seq); + int configChanges, boolean dontReport, PendingTransactionActions pendingActions); /** Resume the activity. */ public abstract void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, - boolean reallyResume, int seq, String reason); + String reason); /** Stop the activity. */ public abstract void handleStopActivity(IBinder token, boolean show, int configChanges, - int seq); + PendingTransactionActions pendingActions); + + /** Report that activity was stopped to server. */ + public abstract void reportStop(PendingTransactionActions pendingActions); + + /** Restart the activity after it was stopped. */ + public abstract void performRestartActivity(IBinder token, boolean start); /** Deliver activity (override) configuration change. */ public abstract void handleActivityConfigurationChanged(IBinder activityToken, @@ -102,13 +102,23 @@ public abstract class ClientTransactionHandler { public abstract void handleWindowVisibility(IBinder token, boolean show); /** Perform activity launch. */ - public abstract void handleLaunchActivity(IBinder token, Intent intent, int ident, - ActivityInfo info, Configuration overrideConfig, CompatibilityInfo compatInfo, - String referrer, IVoiceInteractor voiceInteractor, Bundle state, - PersistableBundle persistentState, List pendingResults, - List pendingNewIntents, boolean notResumed, boolean isForward, - ProfilerInfo profilerInfo); + public abstract Activity handleLaunchActivity(ActivityThread.ActivityClientRecord r, + PendingTransactionActions pendingActions); + + /** Perform activity start. */ + public abstract void handleStartActivity(ActivityThread.ActivityClientRecord r, + PendingTransactionActions pendingActions); + + /** Get package info. */ + public abstract LoadedApk getLoadedApkNoCheck(ApplicationInfo ai, + CompatibilityInfo compatInfo); /** Deliver app configuration change notification. */ public abstract void handleConfigurationChanged(Configuration config); + + /** + * Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the + * provided token. + */ + public abstract ActivityThread.ActivityClientRecord getActivityClient(IBinder token); } diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java index b0d020a7..16534305 100644 --- a/android/app/ContextImpl.java +++ b/android/app/ContextImpl.java @@ -90,6 +90,7 @@ import java.io.InputStream; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Objects; +import java.util.concurrent.Executor; class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { @@ -158,7 +159,7 @@ class ContextImpl extends Context { private ArrayMap mSharedPrefsPaths; final @NonNull ActivityThread mMainThread; - final @NonNull LoadedApk mPackageInfo; + final @NonNull LoadedApk mLoadedApk; private @Nullable ClassLoader mClassLoader; private final @Nullable IBinder mActivityToken; @@ -249,10 +250,15 @@ class ContextImpl extends Context { return mMainThread.getLooper(); } + @Override + public Executor getMainExecutor() { + return mMainThread.getExecutor(); + } + @Override public Context getApplicationContext() { - return (mPackageInfo != null) ? - mPackageInfo.getApplication() : mMainThread.getApplication(); + return (mLoadedApk != null) ? + mLoadedApk.getApplication() : mMainThread.getApplication(); } @Override @@ -296,15 +302,15 @@ class ContextImpl extends Context { @Override public ClassLoader getClassLoader() { - return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader()); + return mClassLoader != null ? mClassLoader : (mLoadedApk != null ? mLoadedApk.getClassLoader() : ClassLoader.getSystemClassLoader()); } @Override public String getPackageName() { - if (mPackageInfo != null) { - return mPackageInfo.getPackageName(); + if (mLoadedApk != null) { + return mLoadedApk.getPackageName(); } - // No mPackageInfo means this is a Context for the system itself, + // No mLoadedApk means this is a Context for the system itself, // and this here is its name. return "android"; } @@ -323,24 +329,24 @@ class ContextImpl extends Context { @Override public ApplicationInfo getApplicationInfo() { - if (mPackageInfo != null) { - return mPackageInfo.getApplicationInfo(); + if (mLoadedApk != null) { + return mLoadedApk.getApplicationInfo(); } throw new RuntimeException("Not supported in system context"); } @Override public String getPackageResourcePath() { - if (mPackageInfo != null) { - return mPackageInfo.getResDir(); + if (mLoadedApk != null) { + return mLoadedApk.getResDir(); } throw new RuntimeException("Not supported in system context"); } @Override public String getPackageCodePath() { - if (mPackageInfo != null) { - return mPackageInfo.getAppDir(); + if (mLoadedApk != null) { + return mLoadedApk.getAppDir(); } throw new RuntimeException("Not supported in system context"); } @@ -350,7 +356,7 @@ class ContextImpl extends Context { // At least one application in the world actually passes in a null // name. This happened to work because when we generated the file name // we would stringify it to "null.xml". Nice. - if (mPackageInfo.getApplicationInfo().targetSdkVersion < + if (mLoadedApk.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; @@ -1092,11 +1098,11 @@ class ContextImpl extends Context { warnIfCallingFromSystemProcess(); IIntentReceiver rd = null; if (resultReceiver != null) { - if (mPackageInfo != null) { + if (mLoadedApk != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } - rd = mPackageInfo.getReceiverDispatcher( + rd = mLoadedApk.getReceiverDispatcher( resultReceiver, getOuterContext(), scheduler, mMainThread.getInstrumentation(), false); } else { @@ -1196,11 +1202,11 @@ class ContextImpl extends Context { Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { IIntentReceiver rd = null; if (resultReceiver != null) { - if (mPackageInfo != null) { + if (mLoadedApk != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } - rd = mPackageInfo.getReceiverDispatcher( + rd = mLoadedApk.getReceiverDispatcher( resultReceiver, getOuterContext(), scheduler, mMainThread.getInstrumentation(), false); } else { @@ -1250,11 +1256,11 @@ class ContextImpl extends Context { warnIfCallingFromSystemProcess(); IIntentReceiver rd = null; if (resultReceiver != null) { - if (mPackageInfo != null) { + if (mLoadedApk != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } - rd = mPackageInfo.getReceiverDispatcher( + rd = mLoadedApk.getReceiverDispatcher( resultReceiver, getOuterContext(), scheduler, mMainThread.getInstrumentation(), false); } else { @@ -1332,11 +1338,11 @@ class ContextImpl extends Context { Bundle initialExtras) { IIntentReceiver rd = null; if (resultReceiver != null) { - if (mPackageInfo != null) { + if (mLoadedApk != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } - rd = mPackageInfo.getReceiverDispatcher( + rd = mLoadedApk.getReceiverDispatcher( resultReceiver, getOuterContext(), scheduler, mMainThread.getInstrumentation(), false); } else { @@ -1413,11 +1419,11 @@ class ContextImpl extends Context { Handler scheduler, Context context, int flags) { IIntentReceiver rd = null; if (receiver != null) { - if (mPackageInfo != null && context != null) { + if (mLoadedApk != null && context != null) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } - rd = mPackageInfo.getReceiverDispatcher( + rd = mLoadedApk.getReceiverDispatcher( receiver, context, scheduler, mMainThread.getInstrumentation(), true); } else { @@ -1444,8 +1450,8 @@ class ContextImpl extends Context { @Override public void unregisterReceiver(BroadcastReceiver receiver) { - if (mPackageInfo != null) { - IIntentReceiver rd = mPackageInfo.forgetReceiverDispatcher( + if (mLoadedApk != null) { + IIntentReceiver rd = mLoadedApk.forgetReceiverDispatcher( getOuterContext(), receiver); try { ActivityManager.getService().unregisterReceiver(rd); @@ -1578,7 +1584,7 @@ class ContextImpl extends Context { @Override public IServiceConnection getServiceDispatcher(ServiceConnection conn, Handler handler, int flags) { - return mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags); + return mLoadedApk.getServiceDispatcher(conn, getOuterContext(), handler, flags); } /** @hide */ @@ -1600,16 +1606,16 @@ class ContextImpl extends Context { if (conn == null) { throw new IllegalArgumentException("connection is null"); } - if (mPackageInfo != null) { - sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags); + if (mLoadedApk != null) { + sd = mLoadedApk.getServiceDispatcher(conn, getOuterContext(), handler, flags); } else { throw new RuntimeException("Not supported in system context"); } validateServiceIntent(service); try { IBinder token = getActivityToken(); - if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null - && mPackageInfo.getApplicationInfo().targetSdkVersion + if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mLoadedApk != null + && mLoadedApk.getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { flags |= BIND_WAIVE_PRIORITY; } @@ -1633,8 +1639,8 @@ class ContextImpl extends Context { if (conn == null) { throw new IllegalArgumentException("connection is null"); } - if (mPackageInfo != null) { - IServiceConnection sd = mPackageInfo.forgetServiceDispatcher( + if (mLoadedApk != null) { + IServiceConnection sd = mLoadedApk.forgetServiceDispatcher( getOuterContext(), conn); try { ActivityManager.getService().unbindService(sd); @@ -1979,40 +1985,20 @@ class ContextImpl extends Context { } } - private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, - int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) { - final String[] splitResDirs; - final ClassLoader classLoader; - try { - splitResDirs = pi.getSplitPaths(splitName); - classLoader = pi.getSplitClassLoader(splitName); - } catch (NameNotFoundException e) { - throw new RuntimeException(e); - } - return ResourcesManager.getInstance().getResources(activityToken, - pi.getResDir(), - splitResDirs, - pi.getOverlayDirs(), - pi.getApplicationInfo().sharedLibraryFiles, - displayId, - overrideConfig, - compatInfo, - classLoader); - } - @Override public Context createApplicationContext(ApplicationInfo application, int flags) throws NameNotFoundException { - LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(), + LoadedApk loadedApk = mMainThread.getLoadedApk(application, + mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE); - if (pi != null) { - ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, + if (loadedApk != null) { + ContextImpl c = new ContextImpl(this, mMainThread, loadedApk, null, mActivityToken, new UserHandle(UserHandle.getUserId(application.uid)), flags, null); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - c.setResources(createResources(mActivityToken, pi, null, displayId, null, + c.setResources(loadedApk.createResources(mActivityToken, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); if (c.mResources != null) { return c; @@ -2036,20 +2022,21 @@ class ContextImpl extends Context { if (packageName.equals("system") || packageName.equals("android")) { // The system resources are loaded in every application, so we can safely copy // the context without reloading Resources. - return new ContextImpl(this, mMainThread, mPackageInfo, null, mActivityToken, user, + return new ContextImpl(this, mMainThread, mLoadedApk, null, mActivityToken, user, flags, null); } - LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), + LoadedApk loadedApk = mMainThread.getLoadedApkForPackageName(packageName, + mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); - if (pi != null) { - ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, user, + if (loadedApk != null) { + ContextImpl c = new ContextImpl(this, mMainThread, loadedApk, null, mActivityToken, user, flags, null); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - c.setResources(createResources(mActivityToken, pi, null, displayId, null, + c.setResources(loadedApk.createResources(mActivityToken, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); if (c.mResources != null) { return c; @@ -2063,30 +2050,21 @@ class ContextImpl extends Context { @Override public Context createContextForSplit(String splitName) throws NameNotFoundException { - if (!mPackageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) { + if (!mLoadedApk.getApplicationInfo().requestsIsolatedSplitLoading()) { // All Splits are always loaded. return this; } - final ClassLoader classLoader = mPackageInfo.getSplitClassLoader(splitName); - final String[] paths = mPackageInfo.getSplitPaths(splitName); + final ClassLoader classLoader = mLoadedApk.getSplitClassLoader(splitName); - final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, splitName, + final ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, splitName, mActivityToken, mUser, mFlags, classLoader); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - context.setResources(ResourcesManager.getInstance().getResources( - mActivityToken, - mPackageInfo.getResDir(), - paths, - mPackageInfo.getOverlayDirs(), - mPackageInfo.getApplicationInfo().sharedLibraryFiles, - displayId, - null, - mPackageInfo.getCompatibilityInfo(), - classLoader)); + context.setResources(mLoadedApk.getOrCreateResourcesForSplit(splitName, + mActivityToken, displayId)); return context; } @@ -2096,11 +2074,11 @@ class ContextImpl extends Context { throw new IllegalArgumentException("overrideConfiguration must not be null"); } - ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, + ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser, mFlags, mClassLoader); final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; - context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId, + context.setResources(mLoadedApk.createResources(mActivityToken, mSplitName, displayId, overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo())); return context; } @@ -2111,11 +2089,11 @@ class ContextImpl extends Context { throw new IllegalArgumentException("display must not be null"); } - ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, + ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser, mFlags, mClassLoader); final int displayId = display.getDisplayId(); - context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId, + context.setResources(mLoadedApk.createResources(mActivityToken, mSplitName, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); context.mDisplay = display; return context; @@ -2125,7 +2103,7 @@ class ContextImpl extends Context { public Context createDeviceProtectedStorageContext() { final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE) | Context.CONTEXT_DEVICE_PROTECTED_STORAGE; - return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser, + return new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser, flags, mClassLoader); } @@ -2133,7 +2111,7 @@ class ContextImpl extends Context { public Context createCredentialProtectedStorageContext() { final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE) | Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE; - return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser, + return new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser, flags, mClassLoader); } @@ -2182,14 +2160,14 @@ class ContextImpl extends Context { @Override public File getDataDir() { - if (mPackageInfo != null) { + if (mLoadedApk != null) { File res = null; if (isCredentialProtectedStorage()) { - res = mPackageInfo.getCredentialProtectedDataDirFile(); + res = mLoadedApk.getCredentialProtectedDataDirFile(); } else if (isDeviceProtectedStorage()) { - res = mPackageInfo.getDeviceProtectedDataDirFile(); + res = mLoadedApk.getDeviceProtectedDataDirFile(); } else { - res = mPackageInfo.getDataDirFile(); + res = mLoadedApk.getDataDirFile(); } if (res != null) { @@ -2240,10 +2218,10 @@ class ContextImpl extends Context { } static ContextImpl createSystemContext(ActivityThread mainThread) { - LoadedApk packageInfo = new LoadedApk(mainThread); - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, + LoadedApk loadedApk = new LoadedApk(mainThread); + ContextImpl context = new ContextImpl(null, mainThread, loadedApk, null, null, null, 0, null); - context.setResources(packageInfo.getResources()); + context.setResources(loadedApk.getResources()); context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), context.mResourcesManager.getDisplayMetrics()); return context; @@ -2254,35 +2232,35 @@ class ContextImpl extends Context { * Make sure that the created system UI context shares the same LoadedApk as the system context. */ static ContextImpl createSystemUiContext(ContextImpl systemContext) { - final LoadedApk packageInfo = systemContext.mPackageInfo; - ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo, null, + final LoadedApk loadedApk = systemContext.mLoadedApk; + ContextImpl context = new ContextImpl(null, systemContext.mMainThread, loadedApk, null, null, null, 0, null); - context.setResources(createResources(null, packageInfo, null, Display.DEFAULT_DISPLAY, null, - packageInfo.getCompatibilityInfo())); + context.setResources(loadedApk.createResources(null, null, Display.DEFAULT_DISPLAY, null, + loadedApk.getCompatibilityInfo())); return context; } - static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { - if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, + static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk loadedApk) { + if (loadedApk == null) throw new IllegalArgumentException("loadedApk"); + ContextImpl context = new ContextImpl(null, mainThread, loadedApk, null, null, null, 0, null); - context.setResources(packageInfo.getResources()); + context.setResources(loadedApk.getResources()); return context; } static ContextImpl createActivityContext(ActivityThread mainThread, - LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, + LoadedApk loadedApk, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) { - if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); + if (loadedApk == null) throw new IllegalArgumentException("loadedApk"); - String[] splitDirs = packageInfo.getSplitResDirs(); - ClassLoader classLoader = packageInfo.getClassLoader(); + String[] splitDirs = loadedApk.getSplitResDirs(); + ClassLoader classLoader = loadedApk.getClassLoader(); - if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) { + if (loadedApk.getApplicationInfo().requestsIsolatedSplitLoading()) { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies"); try { - classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName); - splitDirs = packageInfo.getSplitPaths(activityInfo.splitName); + classLoader = loadedApk.getSplitClassLoader(activityInfo.splitName); + splitDirs = loadedApk.getSplitPaths(activityInfo.splitName); } catch (NameNotFoundException e) { // Nothing above us can handle a NameNotFoundException, better crash. throw new RuntimeException(e); @@ -2291,14 +2269,14 @@ class ContextImpl extends Context { } } - ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, + ContextImpl context = new ContextImpl(null, mainThread, loadedApk, activityInfo.splitName, activityToken, null, 0, classLoader); // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY. displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY; final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY) - ? packageInfo.getCompatibilityInfo() + ? loadedApk.getCompatibilityInfo() : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; final ResourcesManager resourcesManager = ResourcesManager.getInstance(); @@ -2306,10 +2284,10 @@ class ContextImpl extends Context { // Create the base resources for which all configuration contexts for this Activity // will be rebased upon. context.setResources(resourcesManager.createBaseActivityResources(activityToken, - packageInfo.getResDir(), + loadedApk.getResDir(), splitDirs, - packageInfo.getOverlayDirs(), - packageInfo.getApplicationInfo().sharedLibraryFiles, + loadedApk.getOverlayDirs(), + loadedApk.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, @@ -2320,7 +2298,7 @@ class ContextImpl extends Context { } private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread, - @NonNull LoadedApk packageInfo, @Nullable String splitName, + @NonNull LoadedApk loadedApk, @Nullable String splitName, @Nullable IBinder activityToken, @Nullable UserHandle user, int flags, @Nullable ClassLoader classLoader) { mOuterContext = this; @@ -2329,10 +2307,10 @@ class ContextImpl extends Context { // location for application. if ((flags & (Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE | Context.CONTEXT_DEVICE_PROTECTED_STORAGE)) == 0) { - final File dataDir = packageInfo.getDataDirFile(); - if (Objects.equals(dataDir, packageInfo.getCredentialProtectedDataDirFile())) { + final File dataDir = loadedApk.getDataDirFile(); + if (Objects.equals(dataDir, loadedApk.getCredentialProtectedDataDirFile())) { flags |= Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE; - } else if (Objects.equals(dataDir, packageInfo.getDeviceProtectedDataDirFile())) { + } else if (Objects.equals(dataDir, loadedApk.getDeviceProtectedDataDirFile())) { flags |= Context.CONTEXT_DEVICE_PROTECTED_STORAGE; } } @@ -2346,7 +2324,7 @@ class ContextImpl extends Context { } mUser = user; - mPackageInfo = packageInfo; + mLoadedApk = loadedApk; mSplitName = splitName; mClassLoader = classLoader; mResourcesManager = ResourcesManager.getInstance(); @@ -2357,8 +2335,8 @@ class ContextImpl extends Context { setResources(container.mResources); mDisplay = container.mDisplay; } else { - mBasePackageName = packageInfo.mPackageName; - ApplicationInfo ainfo = packageInfo.getApplicationInfo(); + mBasePackageName = loadedApk.mPackageName; + ApplicationInfo ainfo = loadedApk.getApplicationInfo(); if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) { // Special case: system components allow themselves to be loaded in to other // processes. For purposes of app ops, we must then consider the context as @@ -2381,7 +2359,7 @@ class ContextImpl extends Context { } void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) { - mPackageInfo.installSystemApplicationInfo(info, classLoader); + mLoadedApk.installSystemApplicationInfo(info, classLoader); } final void scheduleFinalCleanup(String who, String what) { @@ -2390,7 +2368,7 @@ class ContextImpl extends Context { final void performFinalCleanup(String who, String what) { //Log.i(TAG, "Cleanup up context: " + this); - mPackageInfo.removeContextRegistrations(getOuterContext(), who, what); + mLoadedApk.removeContextRegistrations(getOuterContext(), who, what); } final Context getReceiverRestrictedContext() { diff --git a/android/app/DialogFragment.java b/android/app/DialogFragment.java index a0fb6eeb..3a355d97 100644 --- a/android/app/DialogFragment.java +++ b/android/app/DialogFragment.java @@ -137,7 +137,9 @@ import java.io.PrintWriter; * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java * embed} * - * @deprecated Use {@link android.support.v4.app.DialogFragment} + * @deprecated Use the Support Library + * {@link android.support.v4.app.DialogFragment} for consistent behavior across all devices + * and access to Lifecycle. */ @Deprecated public class DialogFragment extends Fragment diff --git a/android/app/Fragment.java b/android/app/Fragment.java index a92684b5..4ff07f2e 100644 --- a/android/app/Fragment.java +++ b/android/app/Fragment.java @@ -257,7 +257,9 @@ import java.lang.reflect.InvocationTargetException; * pressing back will pop it to return the user to whatever previous state * the activity UI was in. * - * @deprecated Use {@link android.support.v4.app.Fragment} + * @deprecated Use the Support Library + * {@link android.support.v4.app.Fragment} for consistent behavior across all devices + * and access to Lifecycle. */ @Deprecated public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener { diff --git a/android/app/FragmentContainer.java b/android/app/FragmentContainer.java index a1dd32ff..536c8660 100644 --- a/android/app/FragmentContainer.java +++ b/android/app/FragmentContainer.java @@ -25,7 +25,8 @@ import android.view.View; /** * Callbacks to a {@link Fragment}'s container. * - * @deprecated Use {@link android.support.v4.app.FragmentContainer} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentContainer}. */ @Deprecated public abstract class FragmentContainer { diff --git a/android/app/FragmentController.java b/android/app/FragmentController.java index cbb58d40..40bc2483 100644 --- a/android/app/FragmentController.java +++ b/android/app/FragmentController.java @@ -38,7 +38,8 @@ import java.util.List; * It is the responsibility of the host to take care of the Fragment's lifecycle. * The methods provided by {@link FragmentController} are for that purpose. * - * @deprecated Use {@link android.support.v4.app.FragmentController} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentController} */ @Deprecated public class FragmentController { diff --git a/android/app/FragmentHostCallback.java b/android/app/FragmentHostCallback.java index 1edc68ed..b48817b1 100644 --- a/android/app/FragmentHostCallback.java +++ b/android/app/FragmentHostCallback.java @@ -38,7 +38,8 @@ import java.io.PrintWriter; * host fragments, implement {@link FragmentHostCallback}, overriding the methods * applicable to the host. * - * @deprecated Use {@link android.support.v4.app.FragmentHostCallback} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentHostCallback} */ @Deprecated public abstract class FragmentHostCallback extends FragmentContainer { diff --git a/android/app/FragmentManager.java b/android/app/FragmentManager.java index 12e60b87..708450f6 100644 --- a/android/app/FragmentManager.java +++ b/android/app/FragmentManager.java @@ -75,7 +75,9 @@ import java.util.concurrent.CopyOnWriteArrayList; * * Fragments For All for more details. * - * @deprecated Use {@link android.support.v4.app.FragmentManager} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentManager} for consistent behavior across all devices + * and access to Lifecycle. */ @Deprecated public abstract class FragmentManager { @@ -90,7 +92,8 @@ public abstract class FragmentManager { * the identifier as returned by {@link #getId} is the only thing that * will be persisted across activity instances. * - * @deprecated Use {@link android.support.v4.app.FragmentManager.BackStackEntry} + * @deprecated Use the + * Support Library {@link android.support.v4.app.FragmentManager.BackStackEntry} */ @Deprecated public interface BackStackEntry { @@ -136,7 +139,9 @@ public abstract class FragmentManager { /** * Interface to watch for changes to the back stack. * - * @deprecated Use {@link android.support.v4.app.FragmentManager.OnBackStackChangedListener} + * @deprecated Use the + * Support Library + * {@link android.support.v4.app.FragmentManager.OnBackStackChangedListener} */ @Deprecated public interface OnBackStackChangedListener { @@ -438,7 +443,9 @@ public abstract class FragmentManager { * Callback interface for listening to fragment state changes that happen * within a given FragmentManager. * - * @deprecated Use {@link android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks} + * @deprecated Use the + * Support Library + * {@link android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks} */ @Deprecated public abstract static class FragmentLifecycleCallbacks { diff --git a/android/app/FragmentManagerNonConfig.java b/android/app/FragmentManagerNonConfig.java index beb1a15a..326438af 100644 --- a/android/app/FragmentManagerNonConfig.java +++ b/android/app/FragmentManagerNonConfig.java @@ -28,7 +28,8 @@ import java.util.List; * {@link FragmentController#retainNonConfig()} and * {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.

* - * @deprecated Use {@link android.support.v4.app.FragmentManagerNonConfig} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentManagerNonConfig} */ @Deprecated public class FragmentManagerNonConfig { diff --git a/android/app/FragmentTransaction.java b/android/app/FragmentTransaction.java index 0f4a7fb5..713a559a 100644 --- a/android/app/FragmentTransaction.java +++ b/android/app/FragmentTransaction.java @@ -22,7 +22,8 @@ import java.lang.annotation.RetentionPolicy; * guide.

* * - * @deprecated Use {@link android.support.v4.app.FragmentTransaction} + * @deprecated Use the Support Library + * {@link android.support.v4.app.FragmentTransaction} */ @Deprecated public abstract class FragmentTransaction { @@ -182,7 +183,12 @@ public abstract class FragmentTransaction { public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK; /** @hide */ - @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE}) + @IntDef(prefix = { "TRANSIT_" }, value = { + TRANSIT_NONE, + TRANSIT_FRAGMENT_OPEN, + TRANSIT_FRAGMENT_CLOSE, + TRANSIT_FRAGMENT_FADE + }) @Retention(RetentionPolicy.SOURCE) public @interface Transit {} diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java index d49e11f4..b469de56 100644 --- a/android/app/Instrumentation.java +++ b/android/app/Instrumentation.java @@ -1114,7 +1114,10 @@ public class Instrumentation { public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { - return newApplication(cl.loadClass(className), context); + Application app = getFactory(context.getPackageName()) + .instantiateApplication(cl, className); + app.attach(context); + return app; } /** @@ -1201,7 +1204,20 @@ public class Instrumentation { Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { - return (Activity)cl.loadClass(className).newInstance(); + String pkg = intent.getComponent().getPackageName(); + return getFactory(pkg).instantiateActivity(cl, className, intent); + } + + private AppComponentFactory getFactory(String pkg) { + if (mThread == null) { + Log.e(TAG, "Uninitialized ActivityThread, likely app-created Instrumentation," + + " disabling AppComponentFactory", new Throwable()); + return AppComponentFactory.DEFAULT; + } + LoadedApk loadedApk = mThread.peekLoadedApk(pkg, true); + // This is in the case of starting up "android". + if (loadedApk == null) loadedApk = mThread.getSystemContext().mLoadedApk; + return loadedApk.getAppFactory(); } private void prePerformCreate(Activity activity) { @@ -1950,6 +1966,14 @@ public class Instrumentation { mUiAutomationConnection = uiAutomationConnection; } + /** + * Only sets the ActivityThread up, keeps everything else null because app is not being + * instrumented. + */ + final void basicInit(ActivityThread thread) { + mThread = thread; + } + /** @hide */ public static void checkStartActivityResult(int res, Object intent) { if (!ActivityManager.isStartResultFatalError(res)) { diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java index 1fe29004..d0f84c8e 100644 --- a/android/app/KeyguardManager.java +++ b/android/app/KeyguardManager.java @@ -382,6 +382,8 @@ public class KeyguardManager { } /** + * @deprecated Use {@link #isKeyguardLocked()} instead. + * * If keyguard screen is showing or in restricted key input mode (i.e. in * keyguard password emergency screen). When in such mode, certain keys, * such as the Home key and the right soft keys, don't work. @@ -389,11 +391,7 @@ public class KeyguardManager { * @return true if in keyguard restricted input mode. */ public boolean inKeyguardRestrictedInputMode() { - try { - return mWM.inKeyguardRestrictedInputMode(); - } catch (RemoteException ex) { - return false; - } + return isKeyguardLocked(); } /** diff --git a/android/app/LauncherActivity.java b/android/app/LauncherActivity.java index 9ec7f413..88e23566 100644 --- a/android/app/LauncherActivity.java +++ b/android/app/LauncherActivity.java @@ -166,7 +166,7 @@ public abstract class LauncherActivity extends ListActivity { if (item.icon == null) { item.icon = mIconResizer.createIconThumbnail(item.resolveInfo.loadIcon(getPackageManager())); } - text.setCompoundDrawablesWithIntrinsicBounds(item.icon, null, null, null); + text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, null, null, null); } } diff --git a/android/app/ListFragment.java b/android/app/ListFragment.java index 90b77b39..7790f70b 100644 --- a/android/app/ListFragment.java +++ b/android/app/ListFragment.java @@ -145,7 +145,9 @@ import android.widget.TextView; * @see #setListAdapter * @see android.widget.ListView * - * @deprecated Use {@link android.support.v4.app.ListFragment} + * @deprecated Use the Support Library + * {@link android.support.v4.app.ListFragment} for consistent behavior across all devices + * and access to Lifecycle. */ @Deprecated public class ListFragment extends Fragment { diff --git a/android/app/LoadedApk.java b/android/app/LoadedApk.java index f6d9710d..26f49808 100644 --- a/android/app/LoadedApk.java +++ b/android/app/LoadedApk.java @@ -31,6 +31,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.split.SplitDependencyLoader; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; @@ -48,15 +49,13 @@ import android.text.TextUtils; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.Log; +import android.util.LogPrinter; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayAdjustments; - import com.android.internal.util.ArrayUtils; - import dalvik.system.VMRuntime; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -125,6 +124,7 @@ public final class LoadedApk { = new ArrayMap<>(); private final ArrayMap> mUnboundServices = new ArrayMap<>(); + private AppComponentFactory mAppComponentFactory; Application getApplication() { return mApplication; @@ -148,6 +148,7 @@ public final class LoadedApk { mIncludeCode = includeCode; mRegisterPackage = registerPackage; mDisplayAdjustments.setCompatibilityInfo(compatInfo); + mAppComponentFactory = createAppFactory(mApplicationInfo, mBaseClassLoader); } private static ApplicationInfo adjustNativeLibraryPaths(ApplicationInfo info) { @@ -203,6 +204,7 @@ public final class LoadedApk { mRegisterPackage = false; mClassLoader = ClassLoader.getSystemClassLoader(); mResources = Resources.getSystem(); + mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader); } /** @@ -212,6 +214,23 @@ public final class LoadedApk { assert info.packageName.equals("android"); mApplicationInfo = info; mClassLoader = classLoader; + mAppComponentFactory = createAppFactory(info, classLoader); + } + + private AppComponentFactory createAppFactory(ApplicationInfo appInfo, ClassLoader cl) { + if (appInfo.appComponentFactory != null) { + try { + return (AppComponentFactory) cl.loadClass(appInfo.appComponentFactory) + .newInstance(); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + Slog.e(TAG, "Unable to instantiate appComponentFactory", e); + } + } + return AppComponentFactory.DEFAULT; + } + + public AppComponentFactory getAppFactory() { + return mAppComponentFactory; } public String getPackageName() { @@ -313,6 +332,7 @@ public final class LoadedApk { getClassLoader()); } } + mAppComponentFactory = createAppFactory(aInfo, mClassLoader); } private void setApplicationInfo(ApplicationInfo aInfo) { @@ -638,8 +658,7 @@ public final class LoadedApk { final String defaultSearchPaths = System.getProperty("java.library.path"); final boolean treatVendorApkAsUnbundled = !defaultSearchPaths.contains("/vendor/lib"); if (mApplicationInfo.getCodePath() != null - && mApplicationInfo.getCodePath().startsWith("/vendor/") - && treatVendorApkAsUnbundled) { + && mApplicationInfo.isVendor() && treatVendorApkAsUnbundled) { isBundledApp = false; } @@ -948,14 +967,78 @@ public final class LoadedApk { throw new AssertionError("null split not found"); } - mResources = ResourcesManager.getInstance().getResources(null, mResDir, - splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles, - Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(), + mResources = ResourcesManager.getInstance().getResources( + null, + mResDir, + splitPaths, + mOverlayDirs, + mApplicationInfo.sharedLibraryFiles, + Display.DEFAULT_DISPLAY, + null, + getCompatibilityInfo(), getClassLoader()); } return mResources; } + public Resources getOrCreateResourcesForSplit(@NonNull String splitName, + @Nullable IBinder activityToken, int displayId) throws NameNotFoundException { + return ResourcesManager.getInstance().getResources( + activityToken, + mResDir, + getSplitPaths(splitName), + mOverlayDirs, + mApplicationInfo.sharedLibraryFiles, + displayId, + null, + getCompatibilityInfo(), + getSplitClassLoader(splitName)); + } + + /** + * Creates the top level resources for the given package. Will return an existing + * Resources if one has already been created. + */ + public Resources getOrCreateTopLevelResources(@NonNull ApplicationInfo appInfo) { + // Request for this app, short circuit + if (appInfo.uid == Process.myUid()) { + return getResources(); + } + + // Get resources for a different package + return ResourcesManager.getInstance().getResources( + null, + appInfo.publicSourceDir, + appInfo.splitPublicSourceDirs, + appInfo.resourceDirs, + appInfo.sharedLibraryFiles, + Display.DEFAULT_DISPLAY, + null, + getCompatibilityInfo(), + getClassLoader()); + } + + public Resources createResources(IBinder activityToken, String splitName, + int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) { + final String[] splitResDirs; + final ClassLoader classLoader; + try { + splitResDirs = getSplitPaths(splitName); + classLoader = getSplitClassLoader(splitName); + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } + return ResourcesManager.getInstance().getResources(activityToken, + mResDir, + splitResDirs, + mOverlayDirs, + mApplicationInfo.sharedLibraryFiles, + displayId, + overrideConfig, + compatInfo, + classLoader); + } + public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { if (mApplication != null) { @@ -1647,9 +1730,12 @@ public final class LoadedApk { if (dead) { mConnection.onBindingDied(name); } - // If there is a new service, it is now connected. + // If there is a new viable service, it is now connected. if (service != null) { mConnection.onServiceConnected(name, service); + } else { + // The binding machinery worked, but the remote returned null from onBind(). + mConnection.onNullBinding(name); } } diff --git a/android/app/LoaderManager.java b/android/app/LoaderManager.java index 7969684a..86d0fd62 100644 --- a/android/app/LoaderManager.java +++ b/android/app/LoaderManager.java @@ -55,14 +55,16 @@ import java.lang.reflect.Modifier; * Loaders developer guide.

* * - * @deprecated Use {@link android.support.v4.app.LoaderManager} + * @deprecated Use the Support Library + * {@link android.support.v4.app.LoaderManager} */ @Deprecated public abstract class LoaderManager { /** * Callback interface for a client to interact with the manager. * - * @deprecated Use {@link android.support.v4.app.LoaderManager.LoaderCallbacks} + * @deprecated Use the + * Support Library {@link android.support.v4.app.LoaderManager.LoaderCallbacks} */ @Deprecated public interface LoaderCallbacks { diff --git a/android/app/LocalActivityManager.java b/android/app/LocalActivityManager.java index 3b273bc1..998ac5f2 100644 --- a/android/app/LocalActivityManager.java +++ b/android/app/LocalActivityManager.java @@ -22,6 +22,7 @@ import android.os.Binder; import android.os.Bundle; import android.util.Log; import android.view.Window; + import com.android.internal.content.ReferrerIntent; import java.util.ArrayList; @@ -161,12 +162,12 @@ public class LocalActivityManager { case CREATED: if (desiredState == STARTED) { if (localLOGV) Log.v(TAG, r.id + ": restarting"); - mActivityThread.performRestartActivity(r); + mActivityThread.performRestartActivity(r, true /* start */); r.curState = STARTED; } if (desiredState == RESUMED) { if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming"); - mActivityThread.performRestartActivity(r); + mActivityThread.performRestartActivity(r, true /* start */); mActivityThread.performResumeActivity(r, true, "moveToState-CREATED"); r.curState = RESUMED; } @@ -207,7 +208,7 @@ public class LocalActivityManager { private void performPause(LocalActivityRecord r, boolean finishing) { final boolean needState = r.instanceState == null; final Bundle instanceState = mActivityThread.performPauseActivity( - r, finishing, needState, "performPause"); + r, finishing, needState, "performPause", null /* pendingActions */); if (needState) { r.instanceState = instanceState; } @@ -361,7 +362,8 @@ public class LocalActivityManager { performPause(r, finish); } if (localLOGV) Log.v(TAG, r.id + ": destroying"); - mActivityThread.performDestroyActivity(r, finish); + mActivityThread.performDestroyActivity(r, finish, 0 /* configChanges */, + false /* getNonConfigInstance */); r.activity = null; r.window = null; if (finish) { @@ -625,7 +627,8 @@ public class LocalActivityManager { for (int i=0; i + * For backwards compatibility {@code extras} holds some references to "real" member data such + * as {@link getLargeIcon()} which is mirrored by {@link #EXTRA_LARGE_ICON}. This is mostly + * fine as long as the object stays in one process. + *

+ * However, once the notification goes into a parcel each reference gets marshalled separately, + * wasting memory. Especially with large images on Auto and TV, this is worth fixing. + */ + private void fixDuplicateExtras() { + if (extras != null) { + fixDuplicateExtra(mSmallIcon, EXTRA_SMALL_ICON); + fixDuplicateExtra(mLargeIcon, EXTRA_LARGE_ICON); + } + } + + /** + * If we find an extra that's exactly the same as one of the "real" fields but refers to a + * separate object, replace it with the field's version to avoid holding duplicate copies. + */ + private void fixDuplicateExtra(@Nullable Parcelable original, @NonNull String extraName) { + if (original != null && extras.getParcelable(extraName) != null) { + extras.putParcelable(extraName, original); + } + } + /** * Sets the {@link #contentView} field to be a view with the standard "Latest Event" * layout. @@ -5926,9 +5966,10 @@ public class Notification implements Parcelable public static final int MAXIMUM_RETAINED_MESSAGES = 25; CharSequence mUserDisplayName; - CharSequence mConversationTitle; + @Nullable CharSequence mConversationTitle; List mMessages = new ArrayList<>(); List mHistoricMessages = new ArrayList<>(); + boolean mIsGroupConversation; MessagingStyle() { } @@ -5951,20 +5992,20 @@ public class Notification implements Parcelable } /** - * Sets the title to be displayed on this conversation. This should only be used for - * group messaging and left unset for one-on-one conversations. - * @param conversationTitle + * Sets the title to be displayed on this conversation. May be set to {@code null}. + * + * @param conversationTitle A name for the conversation, or {@code null} * @return this object for method chaining. */ - public MessagingStyle setConversationTitle(CharSequence conversationTitle) { + public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) { mConversationTitle = conversationTitle; return this; } /** - * Return the title to be displayed on this conversation. Can be null and - * should be for one-on-one conversations + * Return the title to be displayed on this conversation. May return {@code null}. */ + @Nullable public CharSequence getConversationTitle() { return mConversationTitle; } @@ -6040,6 +6081,24 @@ public class Notification implements Parcelable return mHistoricMessages; } + /** + * Sets whether this conversation notification represents a group. + * @param isGroupConversation {@code true} if the conversation represents a group, + * {@code false} otherwise. + * @return this object for method chaining + */ + public MessagingStyle setGroupConversation(boolean isGroupConversation) { + mIsGroupConversation = isGroupConversation; + return this; + } + + /** + * Returns {@code true} if this notification represents a group conversation. + */ + public boolean isGroupConversation() { + return mIsGroupConversation; + } + /** * @hide */ @@ -6060,6 +6119,7 @@ public class Notification implements Parcelable } fixTitleAndTextExtras(extras); + extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation); } private void fixTitleAndTextExtras(Bundle extras) { @@ -6102,6 +6162,7 @@ public class Notification implements Parcelable mMessages = Message.getMessagesFromBundleArray(messages); Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES); mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); + mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION); } /** diff --git a/android/app/SharedPreferencesImpl.java b/android/app/SharedPreferencesImpl.java index 8c47598f..6dca4004 100644 --- a/android/app/SharedPreferencesImpl.java +++ b/android/app/SharedPreferencesImpl.java @@ -50,6 +50,11 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; final class SharedPreferencesImpl implements SharedPreferences { private static final String TAG = "SharedPreferencesImpl"; @@ -69,15 +74,11 @@ final class SharedPreferencesImpl implements SharedPreferences { private final Object mLock = new Object(); private final Object mWritingToDiskLock = new Object(); - @GuardedBy("mLock") - private Map mMap; + private Future> mMap; @GuardedBy("mLock") private int mDiskWritesInFlight = 0; - @GuardedBy("mLock") - private boolean mLoaded = false; - @GuardedBy("mLock") private StructTimespec mStatTimestamp; @@ -105,27 +106,18 @@ final class SharedPreferencesImpl implements SharedPreferences { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; - mLoaded = false; mMap = null; startLoadFromDisk(); } private void startLoadFromDisk() { - synchronized (mLock) { - mLoaded = false; - } - new Thread("SharedPreferencesImpl-load") { - public void run() { - loadFromDisk(); - } - }.start(); + FutureTask> futureTask = new FutureTask<>(() -> loadFromDisk()); + mMap = futureTask; + new Thread(futureTask, "SharedPreferencesImpl-load").start(); } - private void loadFromDisk() { + private Map loadFromDisk() { synchronized (mLock) { - if (mLoaded) { - return; - } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); @@ -158,16 +150,14 @@ final class SharedPreferencesImpl implements SharedPreferences { } synchronized (mLock) { - mLoaded = true; if (map != null) { - mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { - mMap = new HashMap<>(); + map = new HashMap<>(); } - mLock.notifyAll(); } + return map; } static File makeBackupFile(File prefsFile) { @@ -226,36 +216,42 @@ final class SharedPreferencesImpl implements SharedPreferences { } } - private void awaitLoadedLocked() { - if (!mLoaded) { + private @GuardedBy("mLock") Map 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 getLoadedWithBlockGuard() { + if (!mMap.isDone()) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } - while (!mLoaded) { - try { - mLock.wait(); - } catch (InterruptedException unused) { - } - } + return getLoaded(); } @Override public Map getAll() { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - //noinspection unchecked - return new HashMap(mMap); + return new HashMap(map); } } @Override @Nullable public String getString(String key, @Nullable String defValue) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - String v = (String)mMap.get(key); + String v = (String) map.get(key); return v != null ? v : defValue; } } @@ -263,66 +259,65 @@ final class SharedPreferencesImpl implements SharedPreferences { @Override @Nullable public Set getStringSet(String key, @Nullable Set defValues) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - Set v = (Set) mMap.get(key); + @SuppressWarnings("unchecked") + Set v = (Set) map.get(key); return v != null ? v : defValues; } } @Override public int getInt(String key, int defValue) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - Integer v = (Integer)mMap.get(key); + Integer v = (Integer) map.get(key); return v != null ? v : defValue; } } @Override public long getLong(String key, long defValue) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - Long v = (Long)mMap.get(key); + Long v = (Long) map.get(key); return v != null ? v : defValue; } } @Override public float getFloat(String key, float defValue) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - Float v = (Float)mMap.get(key); + Float v = (Float) map.get(key); return v != null ? v : defValue; } } @Override public boolean getBoolean(String key, boolean defValue) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - Boolean v = (Boolean)mMap.get(key); + Boolean v = (Boolean) map.get(key); return v != null ? v : defValue; } } @Override public boolean contains(String key) { + Map map = getLoadedWithBlockGuard(); synchronized (mLock) { - awaitLoadedLocked(); - return mMap.containsKey(key); + return map.containsKey(key); } } @Override public Editor edit() { - // TODO: remove the need to call awaitLoadedLocked() when + // TODO: remove the need to call getLoaded() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. - synchronized (mLock) { - awaitLoadedLocked(); - } + getLoadedWithBlockGuard(); return new EditorImpl(); } @@ -476,13 +471,43 @@ final class SharedPreferencesImpl implements SharedPreferences { // a memory commit comes in when we're already // writing to disk. if (mDiskWritesInFlight > 0) { - // We can't modify our mMap as a currently + // We can't modify our map as a currently // in-flight write owns it. Clone it before // modifying it. // noinspection unchecked - mMap = new HashMap(mMap); + mMap = new Future>() { + private Map mCopiedMap = + new HashMap(getLoaded()); + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Map get() + throws InterruptedException, ExecutionException { + return mCopiedMap; + } + + @Override + public Map get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return mCopiedMap; + } + }; } - mapToWriteToDisk = mMap; + mapToWriteToDisk = getLoaded(); mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; diff --git a/android/app/StatusBarManager.java b/android/app/StatusBarManager.java index 23c4166d..85a9be35 100644 --- a/android/app/StatusBarManager.java +++ b/android/app/StatusBarManager.java @@ -80,9 +80,14 @@ public class StatusBarManager { public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS; - @IntDef(flag = true, - value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, - DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS}) + @IntDef(flag = true, prefix = { "DISABLE2_" }, value = { + DISABLE2_NONE, + DISABLE2_MASK, + DISABLE2_QUICK_SETTINGS, + DISABLE2_SYSTEM_ICONS, + DISABLE2_NOTIFICATION_SHADE, + DISABLE2_GLOBAL_ACTIONS + }) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java index e48946f2..66cf9915 100644 --- a/android/app/SystemServiceRegistry.java +++ b/android/app/SystemServiceRegistry.java @@ -22,6 +22,7 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; +import android.app.slice.SliceManager; import android.app.timezone.RulesManager; import android.app.trust.TrustManager; import android.app.usage.IStorageStatsManager; @@ -551,8 +552,16 @@ final class SystemServiceRegistry { registerService(Context.WALLPAPER_SERVICE, WallpaperManager.class, new CachedServiceFetcher() { @Override - public WallpaperManager createService(ContextImpl ctx) { - return new WallpaperManager(ctx.getOuterContext(), + public WallpaperManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + final IBinder b; + if (ctx.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) { + b = ServiceManager.getServiceOrThrow(Context.WALLPAPER_SERVICE); + } else { + b = ServiceManager.getService(Context.WALLPAPER_SERVICE); + } + IWallpaperManager service = IWallpaperManager.Stub.asInterface(b); + return new WallpaperManager(service, ctx.getOuterContext(), ctx.mMainThread.getHandler()); }}); @@ -617,12 +626,13 @@ final class SystemServiceRegistry { ConnectivityThread.getInstanceLooper()); }}); - registerService(Context.WIFI_RTT2_SERVICE, WifiRttManager.class, + registerService(Context.WIFI_RTT_RANGING_SERVICE, WifiRttManager.class, new CachedServiceFetcher() { @Override public WifiRttManager createService(ContextImpl ctx) throws ServiceNotFoundException { - IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_RTT2_SERVICE); + IBinder b = ServiceManager.getServiceOrThrow( + Context.WIFI_RTT_RANGING_SERVICE); IWifiRttManager service = IWifiRttManager.Stub.asInterface(b); return new WifiRttManager(ctx.getOuterContext(), service); }}); @@ -944,6 +954,16 @@ final class SystemServiceRegistry { ICrossProfileApps.Stub.asInterface(b)); } }); + + registerService(Context.SLICE_SERVICE, SliceManager.class, + new CachedServiceFetcher() { + @Override + public SliceManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new SliceManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler()); + } + }); } /** diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java index c99de5dd..8f016853 100644 --- a/android/app/UiAutomation.java +++ b/android/app/UiAutomation.java @@ -26,6 +26,7 @@ import android.annotation.TestApi; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; @@ -690,42 +691,15 @@ public final class UiAutomation { .getRealDisplay(Display.DEFAULT_DISPLAY); Point displaySize = new Point(); display.getRealSize(displaySize); - final int displayWidth = displaySize.x; - final int displayHeight = displaySize.y; - final float screenshotWidth; - final float screenshotHeight; - - final int rotation = display.getRotation(); - switch (rotation) { - case ROTATION_FREEZE_0: { - screenshotWidth = displayWidth; - screenshotHeight = displayHeight; - } break; - case ROTATION_FREEZE_90: { - screenshotWidth = displayHeight; - screenshotHeight = displayWidth; - } break; - case ROTATION_FREEZE_180: { - screenshotWidth = displayWidth; - screenshotHeight = displayHeight; - } break; - case ROTATION_FREEZE_270: { - screenshotWidth = displayHeight; - screenshotHeight = displayWidth; - } break; - default: { - throw new IllegalArgumentException("Invalid rotation: " - + rotation); - } - } + int rotation = display.getRotation(); // Take the screenshot Bitmap screenShot = null; try { // Calling out without a lock held. - screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth, - (int) screenshotHeight); + screenShot = mUiAutomationConnection.takeScreenshot( + new Rect(0, 0, displaySize.x, displaySize.y), rotation); if (screenShot == null) { return null; } @@ -734,21 +708,6 @@ public final class UiAutomation { return null; } - // Rotate the screenshot to the current orientation - if (rotation != ROTATION_FREEZE_0) { - Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight, - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(unrotatedScreenShot); - canvas.translate(unrotatedScreenShot.getWidth() / 2, - unrotatedScreenShot.getHeight() / 2); - canvas.rotate(getDegreesForRotation(rotation)); - canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2); - canvas.drawBitmap(screenShot, 0, 0, null); - canvas.setBitmap(null); - screenShot.recycle(); - screenShot = unrotatedScreenShot; - } - // Optimization screenShot.setHasAlpha(false); diff --git a/android/app/UiAutomationConnection.java b/android/app/UiAutomationConnection.java index 5e414b83..d3828ab4 100644 --- a/android/app/UiAutomationConnection.java +++ b/android/app/UiAutomationConnection.java @@ -21,6 +21,7 @@ import android.accessibilityservice.IAccessibilityServiceClient; import android.content.Context; import android.content.pm.IPackageManager; import android.graphics.Bitmap; +import android.graphics.Rect; import android.hardware.input.InputManager; import android.os.Binder; import android.os.IBinder; @@ -153,7 +154,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } @Override - public Bitmap takeScreenshot(int width, int height) { + public Bitmap takeScreenshot(Rect crop, int rotation) { synchronized (mLock) { throwIfCalledByNotTrustedUidLocked(); throwIfShutdownLocked(); @@ -161,7 +162,9 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } final long identity = Binder.clearCallingIdentity(); try { - return SurfaceControl.screenshot(width, height); + int width = crop.width(); + int height = crop.height(); + return SurfaceControl.screenshot(crop, width, height, rotation); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/android/app/UiModeManager.java b/android/app/UiModeManager.java index bc616686..0da5e249 100644 --- a/android/app/UiModeManager.java +++ b/android/app/UiModeManager.java @@ -98,7 +98,11 @@ public class UiModeManager { public static String ACTION_EXIT_DESK_MODE = "android.app.action.EXIT_DESK_MODE"; /** @hide */ - @IntDef({MODE_NIGHT_AUTO, MODE_NIGHT_NO, MODE_NIGHT_YES}) + @IntDef(prefix = { "MODE_" }, value = { + MODE_NIGHT_AUTO, + MODE_NIGHT_NO, + MODE_NIGHT_YES + }) @Retention(RetentionPolicy.SOURCE) public @interface NightMode {} diff --git a/android/app/VrManager.java b/android/app/VrManager.java index 392387a9..61b90e17 100644 --- a/android/app/VrManager.java +++ b/android/app/VrManager.java @@ -4,6 +4,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.os.Handler; @@ -214,4 +215,22 @@ public class VrManager { e.rethrowFromSystemServer(); } } + + /** + * Start VR Input method for the packageName in {@link ComponentName}. + * This method notifies InputMethodManagerService to use VR IME instead of + * regular phone IME. + * @param componentName ComponentName of a VR InputMethod that should be set as selected + * input by InputMethodManagerService. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + public void setVrInputMethod(ComponentName componentName) { + try { + mService.setVrInputMethod(componentName); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/android/app/WallpaperInfo.java b/android/app/WallpaperInfo.java index 9d40381f..35a17892 100644 --- a/android/app/WallpaperInfo.java +++ b/android/app/WallpaperInfo.java @@ -16,18 +16,15 @@ package android.app; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources.NotFoundException; import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; @@ -39,6 +36,9 @@ import android.util.AttributeSet; import android.util.Printer; import android.util.Xml; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; /** @@ -76,6 +76,7 @@ public final class WallpaperInfo implements Parcelable { final int mContextUriResource; final int mContextDescriptionResource; final boolean mShowMetadataInPreview; + final boolean mSupportsAmbientMode; /** * Constructor. @@ -89,15 +90,7 @@ public final class WallpaperInfo implements Parcelable { mService = service; ServiceInfo si = service.serviceInfo; - PackageManager pm = context.getPackageManager(); - String settingsActivityComponent = null; - int thumbnailRes = -1; - int authorRes = -1; - int descriptionRes = -1; - int contextUriRes = -1; - int contextDescriptionRes = -1; - boolean showMetadataInPreview = false; - + final PackageManager pm = context.getPackageManager(); XmlResourceParser parser = null; try { parser = si.loadXmlMetaData(pm, WallpaperService.SERVICE_META_DATA); @@ -123,27 +116,30 @@ public final class WallpaperInfo implements Parcelable { TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.Wallpaper); - settingsActivityComponent = sa.getString( + mSettingsActivityName = sa.getString( com.android.internal.R.styleable.Wallpaper_settingsActivity); - - thumbnailRes = sa.getResourceId( + + mThumbnailResource = sa.getResourceId( com.android.internal.R.styleable.Wallpaper_thumbnail, -1); - authorRes = sa.getResourceId( + mAuthorResource = sa.getResourceId( com.android.internal.R.styleable.Wallpaper_author, -1); - descriptionRes = sa.getResourceId( + mDescriptionResource = sa.getResourceId( com.android.internal.R.styleable.Wallpaper_description, -1); - contextUriRes = sa.getResourceId( + mContextUriResource = sa.getResourceId( com.android.internal.R.styleable.Wallpaper_contextUri, -1); - contextDescriptionRes = sa.getResourceId( + mContextDescriptionResource = sa.getResourceId( com.android.internal.R.styleable.Wallpaper_contextDescription, -1); - showMetadataInPreview = sa.getBoolean( + mShowMetadataInPreview = sa.getBoolean( com.android.internal.R.styleable.Wallpaper_showMetadataInPreview, false); + mSupportsAmbientMode = sa.getBoolean( + com.android.internal.R.styleable.Wallpaper_supportsAmbientMode, + false); sa.recycle(); } catch (NameNotFoundException e) { @@ -152,14 +148,6 @@ public final class WallpaperInfo implements Parcelable { } finally { if (parser != null) parser.close(); } - - mSettingsActivityName = settingsActivityComponent; - mThumbnailResource = thumbnailRes; - mAuthorResource = authorRes; - mDescriptionResource = descriptionRes; - mContextUriResource = contextUriRes; - mContextDescriptionResource = contextDescriptionRes; - mShowMetadataInPreview = showMetadataInPreview; } WallpaperInfo(Parcel source) { @@ -170,6 +158,7 @@ public final class WallpaperInfo implements Parcelable { mContextUriResource = source.readInt(); mContextDescriptionResource = source.readInt(); mShowMetadataInPreview = source.readInt() != 0; + mSupportsAmbientMode = source.readInt() != 0; mService = ResolveInfo.CREATOR.createFromParcel(source); } @@ -325,6 +314,16 @@ public final class WallpaperInfo implements Parcelable { return mShowMetadataInPreview; } + /** + * Returns whether a wallpaper was optimized or not for ambient mode. + * + * @return {@code true} if wallpaper can draw in ambient mode. + * @hide + */ + public boolean getSupportsAmbientMode() { + return mSupportsAmbientMode; + } + /** * Return the class name of an activity that provides a settings UI for * the wallpaper. You can launch this activity be starting it with @@ -366,6 +365,7 @@ public final class WallpaperInfo implements Parcelable { dest.writeInt(mContextUriResource); dest.writeInt(mContextDescriptionResource); dest.writeInt(mShowMetadataInPreview ? 1 : 0); + dest.writeInt(mSupportsAmbientMode ? 1 : 0); mService.writeToParcel(dest, flags); } diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java index 081bd814..f21746cd 100644 --- a/android/app/WallpaperManager.java +++ b/android/app/WallpaperManager.java @@ -176,7 +176,7 @@ public class WallpaperManager { // flags for which kind of wallpaper to act on /** @hide */ - @IntDef(flag = true, value = { + @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_SYSTEM, FLAG_LOCK }) @@ -286,9 +286,8 @@ public class WallpaperManager { private Bitmap mDefaultWallpaper; private Handler mMainLooperHandler; - Globals(Looper looper) { - IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); - mService = IWallpaperManager.Stub.asInterface(b); + Globals(IWallpaperManager service, Looper looper) { + mService = service; mMainLooperHandler = new Handler(looper); forgetLoadedWallpaper(); } @@ -497,17 +496,17 @@ public class WallpaperManager { private static final Object sSync = new Object[0]; private static Globals sGlobals; - static void initGlobals(Looper looper) { + static void initGlobals(IWallpaperManager service, Looper looper) { synchronized (sSync) { if (sGlobals == null) { - sGlobals = new Globals(looper); + sGlobals = new Globals(service, looper); } } } - /*package*/ WallpaperManager(Context context, Handler handler) { + /*package*/ WallpaperManager(IWallpaperManager service, Context context, Handler handler) { mContext = context; - initGlobals(context.getMainLooper()); + initGlobals(service, context.getMainLooper()); } /** diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index 80399ae6..085fc79f 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -89,7 +89,7 @@ public class WindowConfiguration implements Parcelable, Comparable blacklist) { + if (blacklist == null) { + return; + } + long characterCount = 0; + for (final String item : blacklist) { + characterCount += item.length(); + } + if (characterCount > PASSWORD_BLACKLIST_CHARACTER_LIMIT) { + throw new IllegalArgumentException("128 thousand blacklist character limit exceeded by " + + (characterCount - PASSWORD_BLACKLIST_CHARACTER_LIMIT) + " characters"); + } + } + + /** + * Called by an application that is administering the device to blacklist passwords. + *

+ * Any blacklisted password or PIN is prevented from being enrolled by the user or the admin. + * Note that the match against the blacklist is case insensitive. The blacklist applies for all + * password qualities requested by {@link #setPasswordQuality} however it is not taken into + * consideration by {@link #isActivePasswordSufficient}. + *

+ * The blacklist can be cleared by passing {@code null} or an empty list. The blacklist is + * given a name that is used to track which blacklist is currently set by calling {@link + * #getPasswordBlacklistName}. If the blacklist is being cleared, the name is ignored and {@link + * #getPasswordBlacklistName} will return {@code null}. The name can only be {@code null} when + * the blacklist is being cleared. + *

+ * The blacklist is limited to a total of 128 thousand characters rather than limiting to a + * number of entries. + *

+ * This method can be called on the {@link DevicePolicyManager} instance returned by + * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent + * profile. + * + * @param admin the {@link DeviceAdminReceiver} this request is associated with + * @param name name to associate with the blacklist + * @param blacklist list of passwords to blacklist or {@code null} to clear the blacklist + * @return whether the new blacklist was successfully installed + * @throws SecurityException if {@code admin} is not a device or profile owner + * @throws IllegalArgumentException if the blacklist surpasses the character limit + * @throws NullPointerException if {@code name} is {@code null} when setting a non-empty list + * + * @see #getPasswordBlacklistName + * @see #isActivePasswordSufficient + * @see #resetPasswordWithToken + */ + public boolean setPasswordBlacklist(@NonNull ComponentName admin, @Nullable String name, + @Nullable List blacklist) { + enforcePasswordBlacklistSize(blacklist); + + try { + return mService.setPasswordBlacklist(admin, name, blacklist, mParentInstance); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Get the name of the password blacklist set by the given admin. + * + * @param admin the {@link DeviceAdminReceiver} this request is associated with + * @return the name of the blacklist or {@code null} if no blacklist is set + * + * @see #setPasswordBlacklist + */ + public @Nullable String getPasswordBlacklistName(@NonNull ComponentName admin) { + try { + return mService.getPasswordBlacklistName(admin, myUserId(), mParentInstance); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Test if a given password is blacklisted. + * + * @param userId the user to valiate for + * @param password the password to check against the blacklist + * @return whether the password is blacklisted + * + * @see #setPasswordBlacklist + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.TEST_BLACKLISTED_PASSWORD) + public boolean isPasswordBlacklisted(@UserIdInt int userId, @NonNull String password) { + try { + return mService.isPasswordBlacklisted(userId, password); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** * Determine whether the current password the user has set is sufficient to meet the policy * requirements (e.g. quality, minimum length) that have been requested by the admins of this * user and its participating profiles. Restrictions on profiles that have a separate challenge - * are not taken into account. The user must be unlocked in order to perform the check. + * are not taken into account. The user must be unlocked in order to perform the check. The + * password blacklist is not considered when checking sufficiency. *

* The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has @@ -2634,6 +2769,29 @@ public class DevicePolicyManager { return false; } + /** + * When called by a profile owner of a managed profile returns true if the profile uses unified + * challenge with its parent user. + * + * Note: This method is not concerned with password quality and will return false if + * the profile has empty password as a separate challenge. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @throws SecurityException if {@code admin} is not a profile owner of a managed profile. + * @see UserManager#DISALLOW_UNIFIED_PASSWORD + */ + public boolean isUsingUnifiedPassword(@NonNull ComponentName admin) { + throwIfParentInstance("isUsingUnifiedPassword"); + if (mService != null) { + try { + return mService.isUsingUnifiedPassword(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return true; + } + /** * Determine whether the current profile password the user has set is sufficient * to meet the policy requirements (e.g. quality, minimum length) that have been @@ -3048,23 +3206,6 @@ public class DevicePolicyManager { return 0; } - /** - * Returns maximum time to lock that applied by all profiles in this user. We do this because we - * do not have a separate timeout to lock for work challenge only. - * - * @hide - */ - public long getMaximumTimeToLockForUserAndProfiles(int userHandle) { - if (mService != null) { - try { - return mService.getMaximumTimeToLockForUserAndProfiles(userHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - return 0; - } - /** * Called by a device/profile owner to set the timeout after which unlocking with secondary, non * strong auth (e.g. fingerprint, trust agents) times out, i.e. the user has to use a strong @@ -3153,7 +3294,9 @@ public class DevicePolicyManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag=true, value={FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}) + @IntDef(flag = true, prefix = { "FLAG_EVICT_" }, value = { + FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY + }) public @interface LockNowFlag {} /** @@ -3468,6 +3611,16 @@ public class DevicePolicyManager { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION"; + + /** + * Broadcast action: notify managed provisioning that new managed user is created. + * + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MANAGED_USER_CREATED = + "android.app.action.MANAGED_USER_CREATED"; + /** * Widgets are enabled in keyguard */ @@ -3942,6 +4095,108 @@ public class DevicePolicyManager { } } + /** + * Called by a device or profile owner, or delegated certificate installer, to generate a + * new private/public key pair. If the device supports key generation via secure hardware, + * this method is useful for creating a key in KeyChain that never left the secure hardware. + * + * Access to the key is controlled the same way as in {@link #installKeyPair}. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or + * {@code null} if calling from a delegated certificate installer. + * @param algorithm The key generation algorithm, see {@link java.security.KeyPairGenerator}. + * @param keySpec Specification of the key to generate, see + * {@link java.security.KeyPairGenerator}. + * @return A non-null {@code AttestedKeyPair} if the key generation succeeded, null otherwise. + * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile + * owner. + * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, or if the + * algorithm specification in {@code keySpec} is not {@code RSAKeyGenParameterSpec} + * or {@code ECGenParameterSpec}. + */ + public AttestedKeyPair generateKeyPair(@Nullable ComponentName admin, + @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec) { + throwIfParentInstance("generateKeyPair"); + try { + final ParcelableKeyGenParameterSpec parcelableSpec = + new ParcelableKeyGenParameterSpec(keySpec); + KeymasterCertificateChain attestationChain = new KeymasterCertificateChain(); + final boolean success = mService.generateKeyPair( + admin, mContext.getPackageName(), algorithm, parcelableSpec, attestationChain); + if (!success) { + Log.e(TAG, "Error generating key via DevicePolicyManagerService."); + return null; + } + + final String alias = keySpec.getKeystoreAlias(); + final KeyPair keyPair = KeyChain.getKeyPair(mContext, alias); + Certificate[] outputChain = null; + try { + if (AttestationUtils.isChainValid(attestationChain)) { + outputChain = AttestationUtils.parseCertificateChain(attestationChain); + } + } catch (KeyAttestationException e) { + Log.e(TAG, "Error parsing attestation chain for alias " + alias, e); + mService.removeKeyPair(admin, mContext.getPackageName(), alias); + return null; + } + return new AttestedKeyPair(keyPair, outputChain); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (KeyChainException e) { + Log.w(TAG, "Failed to generate key", e); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while generating key", e); + Thread.currentThread().interrupt(); + } + return null; + } + + + /** + * Called by a device or profile owner, or delegated certificate installer, to associate + * certificates with a key pair that was generated using {@link #generateKeyPair}, and + * set whether the key is available for the user to choose in the certificate selection + * prompt. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or + * {@code null} if calling from a delegated certificate installer. + * @param alias The private key alias under which to install the certificate. The {@code alias} + * should denote an existing private key. If a certificate with that alias already + * exists, it will be overwritten. + * @param certs The certificate chain to install. The chain should start with the leaf + * certificate and include the chain of trust in order. This will be returned by + * {@link android.security.KeyChain#getCertificateChain}. + * @param isUserSelectable {@code true} to indicate that a user can select this key via the + * certificate selection prompt, {@code false} to indicate that this key can only be + * granted access by implementing + * {@link android.app.admin.DeviceAdminReceiver#onChoosePrivateKeyAlias}. + * @return {@code true} if the provided {@code alias} exists and the certificates has been + * successfully associated with it, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile + * owner, or {@code admin} is null but the calling application is not a delegated + * certificate installer. + */ + public boolean setKeyPairCertificate(@Nullable ComponentName admin, + @NonNull String alias, @NonNull List certs, boolean isUserSelectable) { + throwIfParentInstance("setKeyPairCertificate"); + try { + final byte[] pemCert = Credentials.convertToPem(certs.get(0)); + byte[] pemChain = null; + if (certs.size() > 1) { + pemChain = Credentials.convertToPem( + certs.subList(1, certs.size()).toArray(new Certificate[0])); + } + return mService.setKeyPairCertificate(admin, mContext.getPackageName(), alias, pemCert, + pemChain, isUserSelectable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (CertificateException | IOException e) { + Log.w(TAG, "Could not pem-encode certificate", e); + } + return false; + } + + /** * @return the alias of a given CA certificate in the certificate store, or {@code null} if it * doesn't exist. @@ -4212,16 +4467,16 @@ public class DevicePolicyManager { /** * Called by a device owner to request a bugreport. *

- * If the device contains secondary users or profiles, they must be affiliated with the device - * owner user. Otherwise a {@link SecurityException} will be thrown. See - * {@link #setAffiliationIds}. + * If the device contains secondary users or profiles, they must be affiliated with the device. + * Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @return {@code true} if the bugreport collection started successfully, or {@code false} if it * wasn't triggered because a previous bugreport operation is still active (either the * bugreport is still running or waiting for the user to share or decline) * @throws SecurityException if {@code admin} is not a device owner, or there is at least one - * profile or secondary user that is not affiliated with the device owner user. + * profile or secondary user that is not affiliated with the device. + * @see #isAffiliatedUser */ public boolean requestBugreport(@NonNull ComponentName admin) { throwIfParentInstance("requestBugreport"); @@ -6035,7 +6290,6 @@ public class DevicePolicyManager { * @return List of package names to keep cached. * @see #setDelegatedScopes * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES - * @hide */ public @Nullable List getKeepUninstalledPackages(@Nullable ComponentName admin) { throwIfParentInstance("getKeepUninstalledPackages"); @@ -6063,7 +6317,6 @@ public class DevicePolicyManager { * @throws SecurityException if {@code admin} is not a device owner. * @see #setDelegatedScopes * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES - * @hide */ public void setKeepUninstalledPackages(@Nullable ComponentName admin, @NonNull List packageNames) { @@ -6151,21 +6404,27 @@ public class DevicePolicyManager { public static final int MAKE_USER_DEMO = 0x0004; /** - * Flag used by {@link #createAndManageUser} to specificy that the newly created user should be + * Flag used by {@link #createAndManageUser} to specify that the newly created user should be * started in the background as part of the user creation. */ - // TODO: Investigate solutions for the case where reboot happens before setup is completed. public static final int START_USER_IN_BACKGROUND = 0x0008; + /** + * Flag used by {@link #createAndManageUser} to specify that the newly created user should skip + * the disabling of system apps during provisioning. + */ + public static final int LEAVE_ALL_SYSTEM_APPS_ENABLED = 0x0010; + /** * @hide */ - @IntDef( - flag = true, - prefix = {"SKIP_", "MAKE_USER_", "START_"}, - value = {SKIP_SETUP_WIZARD, MAKE_USER_EPHEMERAL, MAKE_USER_DEMO, - START_USER_IN_BACKGROUND} - ) + @IntDef(flag = true, prefix = { "SKIP_", "MAKE_USER_", "START_", "LEAVE_" }, value = { + SKIP_SETUP_WIZARD, + MAKE_USER_EPHEMERAL, + MAKE_USER_DEMO, + START_USER_IN_BACKGROUND, + LEAVE_ALL_SYSTEM_APPS_ENABLED + }) @Retention(RetentionPolicy.SOURCE) public @interface CreateAndManageUserFlags {} @@ -6219,7 +6478,7 @@ public class DevicePolicyManager { * @return {@code true} if the user was removed, {@code false} otherwise. * @throws SecurityException if {@code admin} is not a device owner. */ - public boolean removeUser(@NonNull ComponentName admin, UserHandle userHandle) { + public boolean removeUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) { throwIfParentInstance("removeUser"); try { return mService.removeUser(admin, userHandle); @@ -6230,6 +6489,7 @@ public class DevicePolicyManager { /** * Called by a device owner to switch the specified user to the foreground. + *

This cannot be used to switch to a managed profile. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param userHandle the user to switch to; null will switch to primary. @@ -6246,6 +6506,80 @@ public class DevicePolicyManager { } } + /** + * Called by a device owner to stop the specified secondary user. + *

This cannot be used to stop the primary user or a managed profile. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param userHandle the user to be stopped. + * @return {@code true} if the user can be stopped, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) { + throwIfParentInstance("stopUser"); + try { + return mService.stopUser(admin, userHandle); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Called by a profile owner that is affiliated with the device to stop the calling user + * and switch back to primary. + *

This has no effect when called on a managed profile. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return {@code true} if the exit was successful, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a profile owner affiliated with the device. + * @see #isAffiliatedUser + */ + public boolean logoutUser(@NonNull ComponentName admin) { + throwIfParentInstance("logoutUser"); + try { + return mService.logoutUser(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Called by a device owner to list all secondary users on the device, excluding managed + * profiles. + *

Used for various user management APIs, including {@link #switchUser}, {@link #removeUser} + * and {@link #stopUser}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return list of other {@link UserHandle}s on the device. + * @throws SecurityException if {@code admin} is not a device owner. + * @see #switchUser + * @see #removeUser + * @see #stopUser + */ + public List getSecondaryUsers(@NonNull ComponentName admin) { + throwIfParentInstance("getSecondaryUsers"); + try { + return mService.getSecondaryUsers(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Checks if the profile owner is running in an ephemeral user. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return whether the profile owner is running in an ephemeral user. + */ + public boolean isEphemeralUser(@NonNull ComponentName admin) { + throwIfParentInstance("isEphemeralUser"); + try { + return mService.isEphemeralUser(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** * Retrieves the application restrictions for a given target application running in the calling * user. @@ -6480,6 +6814,37 @@ public class DevicePolicyManager { return 0; } + /** + * Install an existing package that has been installed in another user, or has been kept after + * removal via {@link #setKeepUninstalledPackages}. + * This function can be called by a device owner, profile owner or a delegate given + * the {@link #DELEGATION_INSTALL_EXISTING_PACKAGE} scope via {@link #setDelegatedScopes}. + * When called in a secondary user or managed profile, the user/profile must be affiliated with + * the device. See {@link #isAffiliatedUser}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param packageName The package to be installed in the calling profile. + * @return {@code true} if the app is installed; {@code false} otherwise. + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + * @see #setKeepUninstalledPackages + * @see #setDelegatedScopes + * @see #isAffiliatedUser + * @see #DELEGATION_PACKAGE_ACCESS + */ + public boolean installExistingPackage(@NonNull ComponentName admin, String packageName) { + throwIfParentInstance("installExistingPackage"); + if (mService != null) { + try { + return mService.installExistingPackage(admin, mContext.getPackageName(), + packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + /** * Called by a device owner or profile owner to disable account management for a specific type * of account. @@ -6551,13 +6916,14 @@ public class DevicePolicyManager { * package list results in locked tasks belonging to those packages to be finished. *

* This function can only be called by the device owner or by a profile owner of a user/profile - * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any packages + * that is affiliated with the device. See {@link #isAffiliatedUser}. Any packages * set via this method will be cleared if the user becomes unaffiliated. * * @param packages The list of packages allowed to enter lock task mode * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of * an affiliated user or profile. + * @see #isAffiliatedUser * @see Activity#startLockTask() * @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String) * @see DeviceAdminReceiver#onLockTaskModeExiting(Context, Intent) @@ -6580,6 +6946,7 @@ public class DevicePolicyManager { * * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of * an affiliated user or profile. + * @see #isAffiliatedUser * @see #setLockTaskPackages */ public @NonNull String[] getLockTaskPackages(@NonNull ComponentName admin) { @@ -6619,7 +6986,7 @@ public class DevicePolicyManager { * enabled. *

* This function can only be called by the device owner or by a profile owner of a user/profile - * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any features + * that is affiliated with the device. See {@link #isAffiliatedUser}. Any features * set via this method will be cleared if the user becomes unaffiliated. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. @@ -6633,6 +7000,7 @@ public class DevicePolicyManager { * {@link #LOCK_TASK_FEATURE_KEYGUARD} * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of * an affiliated user or profile. + * @see #isAffiliatedUser */ public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) { throwIfParentInstance("setLockTaskFeatures"); @@ -6652,7 +7020,8 @@ public class DevicePolicyManager { * @return bitfield of flags. See {@link #setLockTaskFeatures(ComponentName, int)} for a list. * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of * an affiliated user or profile. - * @see #setLockTaskFeatures(ComponentName, int) + * @see #isAffiliatedUser + * @see #setLockTaskFeatures */ public @LockTaskFeature int getLockTaskFeatures(@NonNull ComponentName admin) { throwIfParentInstance("getLockTaskFeatures"); @@ -6667,7 +7036,7 @@ public class DevicePolicyManager { } /** - * Called by device owners to update {@link android.provider.Settings.Global} settings. + * Called by device owner to update {@link android.provider.Settings.Global} settings. * Validation that the value of the setting is in the correct form for the setting type should * be performed by the caller. *

@@ -6715,6 +7084,37 @@ public class DevicePolicyManager { } } + /** + * Called by device owner to update {@link android.provider.Settings.System} settings. + * Validation that the value of the setting is in the correct form for the setting type should + * be performed by the caller. + *

+ * The settings that can be updated with this method are: + *

    + *
  • {@link android.provider.Settings.System#SCREEN_BRIGHTNESS}
  • + *
  • {@link android.provider.Settings.System#SCREEN_BRIGHTNESS_MODE}
  • + *
  • {@link android.provider.Settings.System#SCREEN_OFF_TIMEOUT}
  • + *
+ *

+ * + * @see android.provider.Settings.System#SCREEN_OFF_TIMEOUT + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param setting The name of the setting to update. + * @param value The value to update the setting to. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public void setSystemSetting(@NonNull ComponentName admin, @NonNull String setting, + String value) { + throwIfParentInstance("setSystemSetting"); + if (mService != null) { + try { + mService.setSystemSetting(admin, setting, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + /** * Called by device owner to set the system wall clock time. This only takes effect if called * when {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be @@ -7619,6 +8019,7 @@ public class DevicePolicyManager { * @param admin Which device owner this request is associated with. * @param enabled whether security logging should be enabled or not. * @throws SecurityException if {@code admin} is not a device owner. + * @see #setAffiliationIds * @see #retrieveSecurityLogs */ public void setSecurityLoggingEnabled(@NonNull ComponentName admin, boolean enabled) { @@ -7657,14 +8058,14 @@ public class DevicePolicyManager { * owner has been notified via {@link DeviceAdminReceiver#onSecurityLogsAvailable}. * *

If there is any other user or profile on the device, it must be affiliated with the - * device owner. Otherwise a {@link SecurityException} will be thrown. See - * {@link #setAffiliationIds} + * device. Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}. * * @param admin Which device owner this request is associated with. * @return the new batch of security logs which is a list of {@link SecurityEvent}, * or {@code null} if rate limitation is exceeded or if logging is currently disabled. * @throws SecurityException if {@code admin} is not a device owner, or there is at least one - * profile or secondary user that is not affiliated with the device owner user. + * profile or secondary user that is not affiliated with the device. + * @see #isAffiliatedUser * @see DeviceAdminReceiver#onSecurityLogsAvailable */ public @Nullable List retrieveSecurityLogs(@NonNull ComponentName admin) { @@ -7707,14 +8108,14 @@ public class DevicePolicyManager { * about data corruption when parsing. * *

If there is any other user or profile on the device, it must be affiliated with the - * device owner. Otherwise a {@link SecurityException} will be thrown. See - * {@link #setAffiliationIds} + * device. Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}. * * @param admin Which device owner this request is associated with. * @return Device logs from before the latest reboot of the system, or {@code null} if this API * is not supported on the device. * @throws SecurityException if {@code admin} is not a device owner, or there is at least one - * profile or secondary user that is not affiliated with the device owner user. + * profile or secondary user that is not affiliated with the device. + * @see #isAffiliatedUser * @see #retrieveSecurityLogs */ public @Nullable List retrievePreRebootSecurityLogs( @@ -7922,6 +8323,9 @@ public class DevicePolicyManager { * Indicates the entity that controls the device or profile owner. Two users/profiles are * affiliated if the set of ids set by their device or profile owners intersect. * + *

A user/profile that is affiliated with the device owner user is considered to be + * affiliated with the device. + * *

Note: Features that depend on user affiliation (such as security logging * or {@link #bindDeviceAdminServiceAsUser}) won't be available when a secondary user or profile * is created, until it becomes affiliated. Therefore it is recommended that the appropriate @@ -7932,6 +8336,7 @@ public class DevicePolicyManager { * @param ids A set of opaque non-empty affiliation ids. * * @throws IllegalArgumentException if {@code ids} is null or contains an empty string. + * @see #isAffiliatedUser */ public void setAffiliationIds(@NonNull ComponentName admin, @NonNull Set ids) { throwIfParentInstance("setAffiliationIds"); @@ -7959,13 +8364,12 @@ public class DevicePolicyManager { } /** - * @hide * Returns whether this user/profile is affiliated with the device. *

* By definition, the user that the device owner runs on is always affiliated with the device. * Any other user/profile is considered affiliated with the device if the set specified by its * profile owner via {@link #setAffiliationIds} intersects with the device owner's. - * + * @see #setAffiliationIds */ public boolean isAffiliatedUser() { throwIfParentInstance("isAffiliatedUser"); @@ -8178,6 +8582,7 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param enabled whether network logging should be enabled or not. * @throws SecurityException if {@code admin} is not a device owner. + * @see #setAffiliationIds * @see #retrieveNetworkLogs */ public void setNetworkLoggingEnabled(@NonNull ComponentName admin, boolean enabled) { @@ -8233,7 +8638,8 @@ public class DevicePolicyManager { * {@code null} if the batch represented by batchToken is no longer available or if * logging is disabled. * @throws SecurityException if {@code admin} is not a device owner, or there is at least one - * profile or secondary user that is not affiliated with the device owner user. + * profile or secondary user that is not affiliated with the device. + * @see #setAffiliationIds * @see DeviceAdminReceiver#onNetworkLogsAvailable */ public @Nullable List retrieveNetworkLogs(@NonNull ComponentName admin, @@ -8411,6 +8817,15 @@ public class DevicePolicyManager { } } + /** {@hide} */ + @Condemned + @Deprecated + public boolean clearApplicationUserData(@NonNull ComponentName admin, + @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener, + @NonNull Handler handler) { + return clearApplicationUserData(admin, packageName, listener, new HandlerExecutor(handler)); + } + /** * Called by the device owner or profile owner to clear application user data of a given * package. The behaviour of this is equivalent to the target application calling @@ -8422,19 +8837,20 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param packageName The name of the package which will have its user data wiped. * @param listener A callback object that will inform the caller when the clearing is done. - * @param handler The handler indicating the thread on which the listener should be invoked. + * @param executor The executor through which the listener should be invoked. * @throws SecurityException if the caller is not the device owner/profile owner. * @return whether the clearing succeeded. */ public boolean clearApplicationUserData(@NonNull ComponentName admin, @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener, - @NonNull Handler handler) { + @NonNull @CallbackExecutor Executor executor) { throwIfParentInstance("clearAppData"); + Preconditions.checkNotNull(executor); try { return mService.clearApplicationUserData(admin, packageName, new IPackageDataObserver.Stub() { public void onRemoveCompleted(String pkg, boolean succeeded) { - handler.post(() -> + executor.execute(() -> listener.onApplicationUserDataCleared(pkg, succeeded)); } }); @@ -8443,6 +8859,37 @@ public class DevicePolicyManager { } } + /** + * Called by a device owner to specify whether logout is enabled for all secondary users. The + * system may show a logout button that stops the user and switches back to the primary user. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param enabled whether logout should be enabled or not. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public void setLogoutEnabled(@NonNull ComponentName admin, boolean enabled) { + throwIfParentInstance("setLogoutEnabled"); + try { + mService.setLogoutEnabled(admin, enabled); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Returns whether logout is enabled by a device owner. + * + * @return {@code true} if logout is enabled by device owner, {@code false} otherwise. + */ + public boolean isLogoutEnabled() { + throwIfParentInstance("isLogoutEnabled"); + try { + return mService.isLogoutEnabled(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** * Callback used in {@link #clearApplicationUserData} * to indicate that the clearing of an application's user data is done. @@ -8457,4 +8904,65 @@ public class DevicePolicyManager { */ void onApplicationUserDataCleared(String packageName, boolean succeeded); } + + /** + * Returns set of system apps that should be removed during provisioning. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param userId ID of the user to be provisioned. + * @param provisioningAction action indicating type of provisioning, should be one of + * {@link #ACTION_PROVISION_MANAGED_DEVICE}, {@link #ACTION_PROVISION_MANAGED_PROFILE} or + * {@link #ACTION_PROVISION_MANAGED_USER}. + * + * @hide + */ + public Set getDisallowedSystemApps(ComponentName admin, int userId, + String provisioningAction) { + try { + return new ArraySet<>( + mService.getDisallowedSystemApps(admin, userId, provisioningAction)); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + //TODO STOPSHIP Add link to onTransferComplete callback when implemented. + /** + * Transfers the current administrator. All policies from the current administrator are + * migrated to the new administrator. The whole operation is atomic - the transfer is either + * complete or not done at all. + * + * Depending on the current administrator (device owner, profile owner, corporate owned + * profile owner), you have the following expected behaviour: + *

    + *
  • A device owner can only be transferred to a new device owner
  • + *
  • A profile owner can only be transferred to a new profile owner
  • + *
  • A corporate owned managed profile can have two cases: + *
      + *
    • If the device owner and profile owner are the same package, + * both will be transferred.
    • + *
    • 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.
    • + *
    + *
  • + *
+ * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param target Which {@link DeviceAdminReceiver} we want the new administrator to be. + * @param bundle Parameters - This bundle allows the current administrator to pass data to the + * new administrator. The parameters will be received in the + * onTransferComplete callback. + * @hide + */ + public void transferOwner(@NonNull ComponentName admin, @NonNull ComponentName target, + PersistableBundle bundle) { + throwIfParentInstance("transferOwner"); + try { + mService.transferOwner(admin, target, bundle); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } } diff --git a/android/app/admin/DevicePolicyManagerInternal.java b/android/app/admin/DevicePolicyManagerInternal.java index eef2f983..b692ffd9 100644 --- a/android/app/admin/DevicePolicyManagerInternal.java +++ b/android/app/admin/DevicePolicyManagerInternal.java @@ -16,6 +16,7 @@ package android.app.admin; +import android.annotation.UserIdInt; import android.content.Intent; import java.util.List; @@ -101,4 +102,25 @@ public abstract class DevicePolicyManagerInternal { * not enforced by the profile/device owner. */ public abstract Intent createUserRestrictionSupportIntent(int userId, String userRestriction); + + /** + * Returns whether this user/profile is affiliated with the device. + * + *

+ * By definition, the user that the device owner runs on is always affiliated with the device. + * Any other user/profile is considered affiliated with the device if the set specified by its + * profile owner via {@link DevicePolicyManager#setAffiliationIds} intersects with the device + * owner's. + *

+ * Profile owner on the primary user will never be considered as affiliated as there is no + * device owner to be affiliated with. + */ + public abstract boolean isUserAffiliatedWithDevice(int userId); + + /** + * Reports that a profile has changed to use a unified or separate credential. + * + * @param userId User ID of the profile. + */ + public abstract void reportSeparateProfileChallengeChanged(@UserIdInt int userId); } diff --git a/android/app/admin/PasswordMetrics.java b/android/app/admin/PasswordMetrics.java index 4658a474..5fee8532 100644 --- a/android/app/admin/PasswordMetrics.java +++ b/android/app/admin/PasswordMetrics.java @@ -223,7 +223,12 @@ public class PasswordMetrics implements Parcelable { } @Retention(RetentionPolicy.SOURCE) - @IntDef({CHAR_UPPER_CASE, CHAR_LOWER_CASE, CHAR_DIGIT, CHAR_SYMBOL}) + @IntDef(prefix = { "CHAR_" }, value = { + CHAR_UPPER_CASE, + CHAR_LOWER_CASE, + CHAR_DIGIT, + CHAR_SYMBOL + }) private @interface CharacterCatagory {} private static final int CHAR_LOWER_CASE = 0; private static final int CHAR_UPPER_CASE = 1; diff --git a/android/app/admin/SecurityLog.java b/android/app/admin/SecurityLog.java index 2b590e0d..d3b66d0d 100644 --- a/android/app/admin/SecurityLog.java +++ b/android/app/admin/SecurityLog.java @@ -17,6 +17,7 @@ package android.app.admin; import android.annotation.IntDef; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemProperties; @@ -26,6 +27,7 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collection; +import java.util.Objects; /** * Definitions for working with security logs. @@ -43,10 +45,17 @@ public class SecurityLog { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({TAG_ADB_SHELL_INTERACTIVE, TAG_ADB_SHELL_CMD, TAG_SYNC_RECV_FILE, TAG_SYNC_SEND_FILE, - TAG_APP_PROCESS_START, TAG_KEYGUARD_DISMISSED, TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT, - TAG_KEYGUARD_SECURED}) - public @interface SECURITY_LOG_TAG {} + @IntDef(prefix = { "TAG_" }, value = { + TAG_ADB_SHELL_INTERACTIVE, + TAG_ADB_SHELL_CMD, + TAG_SYNC_RECV_FILE, + TAG_SYNC_SEND_FILE, + TAG_APP_PROCESS_START, + TAG_KEYGUARD_DISMISSED, + TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT, + TAG_KEYGUARD_SECURED + }) + public @interface SecurityLogTag {} /** * Indicate that an ADB interactive shell was opened via "adb shell". @@ -128,9 +137,28 @@ public class SecurityLog { */ public static final class SecurityEvent implements Parcelable { private Event mEvent; + private long mId; + + /** + * Constructor used by native classes to generate SecurityEvent instances. + * @hide + */ + /* package */ SecurityEvent(byte[] data) { + this(0, data); + } + + /** + * Constructor used by Parcelable.Creator to generate SecurityEvent instances. + * @hide + */ + /* package */ SecurityEvent(Parcel source) { + this(source.readLong(), source.createByteArray()); + } /** @hide */ - /*package*/ SecurityEvent(byte[] data) { + @TestApi + public SecurityEvent(long id, byte[] data) { + mId = id; mEvent = Event.fromBytes(data); } @@ -143,13 +171,8 @@ public class SecurityLog { /** * Returns the tag of this log entry, which specifies entry's semantics. - * Could be one of {@link SecurityLog#TAG_SYNC_RECV_FILE}, - * {@link SecurityLog#TAG_SYNC_SEND_FILE}, {@link SecurityLog#TAG_ADB_SHELL_CMD}, - * {@link SecurityLog#TAG_ADB_SHELL_INTERACTIVE}, {@link SecurityLog#TAG_APP_PROCESS_START}, - * {@link SecurityLog#TAG_KEYGUARD_DISMISSED}, {@link SecurityLog#TAG_KEYGUARD_SECURED}, - * {@link SecurityLog#TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT}. */ - public @SECURITY_LOG_TAG int getTag() { + public @SecurityLogTag int getTag() { return mEvent.getTag(); } @@ -160,6 +183,21 @@ public class SecurityLog { return mEvent.getData(); } + /** + * @hide + */ + public void setId(long id) { + this.mId = id; + } + + /** + * Returns the id of the event, where the id monotonically increases for each event. The id + * is reset when the device reboots, and when security logging is enabled. + */ + public long getId() { + return mId; + } + @Override public int describeContents() { return 0; @@ -167,6 +205,7 @@ public class SecurityLog { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mId); dest.writeByteArray(mEvent.getBytes()); } @@ -174,7 +213,7 @@ public class SecurityLog { new Parcelable.Creator() { @Override public SecurityEvent createFromParcel(Parcel source) { - return new SecurityEvent(source.createByteArray()); + return new SecurityEvent(source); } @Override @@ -191,7 +230,7 @@ public class SecurityLog { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SecurityEvent other = (SecurityEvent) o; - return mEvent.equals(other.mEvent); + return mEvent.equals(other.mEvent) && mId == other.mId; } /** @@ -199,7 +238,7 @@ public class SecurityLog { */ @Override public int hashCode() { - return mEvent.hashCode(); + return Objects.hash(mEvent, mId); } } /** diff --git a/android/app/admin/SystemUpdateInfo.java b/android/app/admin/SystemUpdateInfo.java index fa31273e..b0376b50 100644 --- a/android/app/admin/SystemUpdateInfo.java +++ b/android/app/admin/SystemUpdateInfo.java @@ -52,7 +52,11 @@ public final class SystemUpdateInfo implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({SECURITY_PATCH_STATE_FALSE, SECURITY_PATCH_STATE_TRUE, SECURITY_PATCH_STATE_UNKNOWN}) + @IntDef(prefix = { "SECURITY_PATCH_STATE_" }, value = { + SECURITY_PATCH_STATE_FALSE, + SECURITY_PATCH_STATE_TRUE, + SECURITY_PATCH_STATE_UNKNOWN + }) public @interface SecurityPatchState {} private static final String ATTR_RECEIVED_TIME = "received-time"; diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java index 995d98a7..232a6887 100644 --- a/android/app/admin/SystemUpdatePolicy.java +++ b/android/app/admin/SystemUpdatePolicy.java @@ -36,10 +36,11 @@ import java.lang.annotation.RetentionPolicy; public class SystemUpdatePolicy implements Parcelable { /** @hide */ - @IntDef({ - TYPE_INSTALL_AUTOMATIC, - TYPE_INSTALL_WINDOWED, - TYPE_POSTPONE}) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_INSTALL_AUTOMATIC, + TYPE_INSTALL_WINDOWED, + TYPE_POSTPONE + }) @Retention(RetentionPolicy.SOURCE) @interface SystemUpdatePolicyType {} diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java index da5569d2..7b549cd5 100644 --- a/android/app/assist/AssistStructure.java +++ b/android/app/assist/AssistStructure.java @@ -2139,6 +2139,16 @@ public class AssistStructure implements Parcelable { return mActivityComponent; } + /** + * Called by Autofill server when app forged a different value. + * + * @hide + */ + public void setActivityComponent(ComponentName componentName) { + ensureData(); + mActivityComponent = componentName; + } + /** @hide */ public int getFlags() { return mFlags; diff --git a/android/app/backup/BackupAgent.java b/android/app/backup/BackupAgent.java index 7aa80d26..861cb9a8 100644 --- a/android/app/backup/BackupAgent.java +++ b/android/app/backup/BackupAgent.java @@ -262,6 +262,17 @@ public abstract class BackupAgent extends ContextWrapper { public abstract void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException; + /** + * New version of {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)} + * that handles a long app version code. Default implementation casts the version code to + * an int and calls {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}. + */ + public void onRestore(BackupDataInput data, long appVersionCode, + ParcelFileDescriptor newState) + throws IOException { + onRestore(data, (int) appVersionCode, newState); + } + /** * The application is having its entire file system contents backed up. {@code data} * points to the backup destination, and the app has the opportunity to choose which @@ -947,7 +958,7 @@ public abstract class BackupAgent extends ContextWrapper { } @Override - public void doRestore(ParcelFileDescriptor data, int appVersionCode, + public void doRestore(ParcelFileDescriptor data, long appVersionCode, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder) throws RemoteException { // Ensure that we're running with the app's normal permission level diff --git a/android/app/backup/BackupManager.java b/android/app/backup/BackupManager.java index 9f9b2170..6512b98c 100644 --- a/android/app/backup/BackupManager.java +++ b/android/app/backup/BackupManager.java @@ -16,10 +16,12 @@ package android.app.backup; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -445,6 +447,57 @@ public class BackupManager { return null; } + /** + * Update the attributes of the transport identified by {@code transportComponent}. If the + * specified transport has not been bound at least once (for registration), this call will be + * ignored. Only the host process of the transport can change its description, otherwise a + * {@link SecurityException} will be thrown. + * + * @param transportComponent The identity of the transport being described. + * @param name A {@link String} with the new name for the transport. This is NOT for + * identification. MUST NOT be {@code null}. + * @param configurationIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's configuration UI. It may + * be {@code null} if the transport does not offer any user-facing configuration UI. + * @param currentDestinationString A {@link String} describing the destination to which the + * transport is currently sending data. MUST NOT be {@code null}. + * @param dataManagementIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's data-management UI. It + * may be {@code null} if the transport does not offer any user-facing data + * management UI. + * @param dataManagementLabel A {@link String} to be used as the label for the transport's data + * management affordance. This MUST be {@code null} when dataManagementIntent is + * {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}. + * @throws SecurityException If the UID of the calling process differs from the package UID of + * {@code transportComponent} or if the caller does NOT have BACKUP permission. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BACKUP) + public void updateTransportAttributes( + ComponentName transportComponent, + String name, + @Nullable Intent configurationIntent, + String currentDestinationString, + @Nullable Intent dataManagementIntent, + @Nullable String dataManagementLabel) { + checkServiceBinder(); + if (sService != null) { + try { + sService.updateTransportAttributes( + transportComponent, + name, + configurationIntent, + currentDestinationString, + dataManagementIntent, + dataManagementLabel); + } catch (RemoteException e) { + Log.e(TAG, "describeTransport() couldn't connect"); + } + } + } + /** * Specify the current backup transport. * diff --git a/android/app/backup/BackupManagerMonitor.java b/android/app/backup/BackupManagerMonitor.java index ebad16e0..ae4a98a4 100644 --- a/android/app/backup/BackupManagerMonitor.java +++ b/android/app/backup/BackupManagerMonitor.java @@ -40,9 +40,14 @@ public class BackupManagerMonitor { /** string : the package name */ public static final String EXTRA_LOG_EVENT_PACKAGE_NAME = "android.app.backup.extra.LOG_EVENT_PACKAGE_NAME"; - /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */ + /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME + * @deprecated Use {@link #EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION} */ + @Deprecated public static final String EXTRA_LOG_EVENT_PACKAGE_VERSION = "android.app.backup.extra.LOG_EVENT_PACKAGE_VERSION"; + /** long : the full versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */ + public static final String EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION = + "android.app.backup.extra.LOG_EVENT_PACKAGE_FULL_VERSION"; /** int : the id of the log message, will be a unique identifier */ public static final String EXTRA_LOG_EVENT_ID = "android.app.backup.extra.LOG_EVENT_ID"; /** diff --git a/android/app/servertransaction/ActivityConfigurationChangeItem.java b/android/app/servertransaction/ActivityConfigurationChangeItem.java index 07001e2b..a2b7d580 100644 --- a/android/app/servertransaction/ActivityConfigurationChangeItem.java +++ b/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -19,25 +19,25 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.view.Display.INVALID_DISPLAY; +import android.app.ClientTransactionHandler; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import java.util.Objects; + /** * Activity configuration changed callback. * @hide */ public class ActivityConfigurationChangeItem extends ClientTransactionItem { - private final Configuration mConfiguration; - - public ActivityConfigurationChangeItem(Configuration configuration) { - mConfiguration = configuration; - } + private Configuration mConfiguration; @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { // TODO(lifecycler): detect if PIP or multi-window mode changed and report it here. Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); client.handleActivityConfigurationChanged(token, mConfiguration, INVALID_DISPLAY); @@ -45,6 +45,29 @@ public class ActivityConfigurationChangeItem extends ClientTransactionItem { } + // ObjectPoolItem implementation + + private ActivityConfigurationChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ActivityConfigurationChangeItem obtain(Configuration config) { + ActivityConfigurationChangeItem instance = + ObjectPool.obtain(ActivityConfigurationChangeItem.class); + if (instance == null) { + instance = new ActivityConfigurationChangeItem(); + } + instance.mConfiguration = config; + + return instance; + } + + @Override + public void recycle() { + mConfiguration = null; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -78,11 +101,16 @@ public class ActivityConfigurationChangeItem extends ClientTransactionItem { return false; } final ActivityConfigurationChangeItem other = (ActivityConfigurationChangeItem) o; - return mConfiguration.equals(other.mConfiguration); + return Objects.equals(mConfiguration, other.mConfiguration); } @Override public int hashCode() { return mConfiguration.hashCode(); } + + @Override + public String toString() { + return "ActivityConfigurationChange{config=" + mConfiguration + "}"; + } } diff --git a/android/app/servertransaction/ActivityLifecycleItem.java b/android/app/servertransaction/ActivityLifecycleItem.java index a64108db..0fdc7c56 100644 --- a/android/app/servertransaction/ActivityLifecycleItem.java +++ b/android/app/servertransaction/ActivityLifecycleItem.java @@ -27,16 +27,28 @@ import java.lang.annotation.RetentionPolicy; */ public abstract class ActivityLifecycleItem extends ClientTransactionItem { - static final boolean DEBUG_ORDER = false; - - @IntDef({UNDEFINED, RESUMED, PAUSED, STOPPED, DESTROYED}) + @IntDef(prefix = { "UNDEFINED", "PRE_", "ON_" }, value = { + UNDEFINED, + PRE_ON_CREATE, + ON_CREATE, + ON_START, + ON_RESUME, + ON_PAUSE, + ON_STOP, + ON_DESTROY, + ON_RESTART + }) @Retention(RetentionPolicy.SOURCE) - @interface LifecycleState{} + public @interface LifecycleState{} public static final int UNDEFINED = -1; - public static final int RESUMED = 0; - public static final int PAUSED = 1; - public static final int STOPPED = 2; - public static final int DESTROYED = 3; + public static final int PRE_ON_CREATE = 0; + public static final int ON_CREATE = 1; + public static final int ON_START = 2; + public static final int ON_RESUME = 3; + public static final int ON_PAUSE = 4; + public static final int ON_STOP = 5; + public static final int ON_DESTROY = 6; + public static final int ON_RESTART = 7; /** A final lifecycle state that an activity should reach. */ @LifecycleState diff --git a/android/app/servertransaction/ActivityResultItem.java b/android/app/servertransaction/ActivityResultItem.java index 76664d8e..73b5ec44 100644 --- a/android/app/servertransaction/ActivityResultItem.java +++ b/android/app/servertransaction/ActivityResultItem.java @@ -16,9 +16,10 @@ package android.app.servertransaction; -import static android.app.servertransaction.ActivityLifecycleItem.PAUSED; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ClientTransactionHandler; import android.app.ResultInfo; import android.os.IBinder; import android.os.Parcel; @@ -26,6 +27,7 @@ import android.os.Parcelable; import android.os.Trace; import java.util.List; +import java.util.Objects; /** * Activity result delivery callback. @@ -33,25 +35,44 @@ import java.util.List; */ public class ActivityResultItem extends ClientTransactionItem { - private final List mResultInfoList; - - public ActivityResultItem(List resultInfos) { - mResultInfoList = resultInfos; - } + private List mResultInfoList; @Override public int getPreExecutionState() { - return PAUSED; + return ON_PAUSE; } @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult"); client.handleSendResult(token, mResultInfoList); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + // ObjectPoolItem implementation + + private ActivityResultItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ActivityResultItem obtain(List resultInfoList) { + ActivityResultItem instance = ObjectPool.obtain(ActivityResultItem.class); + if (instance == null) { + instance = new ActivityResultItem(); + } + instance.mResultInfoList = resultInfoList; + + return instance; + } + + @Override + public void recycle() { + mResultInfoList = null; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -85,11 +106,16 @@ public class ActivityResultItem extends ClientTransactionItem { return false; } final ActivityResultItem other = (ActivityResultItem) o; - return mResultInfoList.equals(other.mResultInfoList); + return Objects.equals(mResultInfoList, other.mResultInfoList); } @Override public int hashCode() { return mResultInfoList.hashCode(); } + + @Override + public String toString() { + return "ActivityResultItem{resultInfoList=" + mResultInfoList + "}"; + } } diff --git a/android/app/servertransaction/BaseClientRequest.java b/android/app/servertransaction/BaseClientRequest.java index 4bd01afb..c91e0ca5 100644 --- a/android/app/servertransaction/BaseClientRequest.java +++ b/android/app/servertransaction/BaseClientRequest.java @@ -24,7 +24,7 @@ import android.os.IBinder; * Each of them can be prepared before scheduling and, eventually, executed. * @hide */ -public interface BaseClientRequest { +public interface BaseClientRequest extends ObjectPoolItem { /** * Prepare the client request before scheduling. @@ -33,13 +33,25 @@ public interface BaseClientRequest { * @param client Target client handler. * @param token Target activity token. */ - default void prepare(ClientTransactionHandler client, IBinder token) { + default void preExecute(ClientTransactionHandler client, IBinder token) { } /** * Execute the request. * @param client Target client handler. * @param token Target activity token. + * @param pendingActions Container that may have data pending to be used. */ - void execute(ClientTransactionHandler client, IBinder token); + void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions); + + /** + * Perform all actions that need to happen after execution, e.g. report the result to server. + * @param client Target client handler. + * @param token Target activity token. + * @param pendingActions Container that may have data pending to be used. + */ + default void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + } } diff --git a/android/app/servertransaction/ClientTransaction.java b/android/app/servertransaction/ClientTransaction.java index d2289ba0..3c96f069 100644 --- a/android/app/servertransaction/ClientTransaction.java +++ b/android/app/servertransaction/ClientTransaction.java @@ -16,6 +16,7 @@ package android.app.servertransaction; +import android.annotation.Nullable; import android.app.ClientTransactionHandler; import android.app.IApplicationThread; import android.os.IBinder; @@ -36,7 +37,7 @@ import java.util.Objects; * @see ActivityLifecycleItem * @hide */ -public class ClientTransaction implements Parcelable { +public class ClientTransaction implements Parcelable, ObjectPoolItem { /** A list of individual callbacks to a client. */ private List mActivityCallbacks; @@ -53,9 +54,9 @@ public class ClientTransaction implements Parcelable { /** Target client activity. Might be null if the entire transaction is targeting an app. */ private IBinder mActivityToken; - public ClientTransaction(IApplicationThread client, IBinder activityToken) { - mClient = client; - mActivityToken = activityToken; + /** Get the target client of the transaction. */ + public IApplicationThread getClient() { + return mClient; } /** @@ -69,6 +70,23 @@ public class ClientTransaction implements Parcelable { mActivityCallbacks.add(activityCallback); } + /** Get the list of callbacks. */ + @Nullable + List getCallbacks() { + return mActivityCallbacks; + } + + /** Get the target activity. */ + @Nullable + public IBinder getActivityToken() { + return mActivityToken; + } + + /** Get the target state lifecycle request. */ + ActivityLifecycleItem getLifecycleStateRequest() { + return mLifecycleStateRequest; + } + /** * Set the lifecycle state in which the client should be after executing the transaction. * @param stateRequest A lifecycle request initialized with right parameters. @@ -82,50 +100,68 @@ public class ClientTransaction implements Parcelable { * @param clientTransactionHandler Handler on the client side that will executed all operations * requested by transaction items. */ - public void prepare(android.app.ClientTransactionHandler clientTransactionHandler) { + public void preExecute(android.app.ClientTransactionHandler clientTransactionHandler) { if (mActivityCallbacks != null) { final int size = mActivityCallbacks.size(); for (int i = 0; i < size; ++i) { - mActivityCallbacks.get(i).prepare(clientTransactionHandler, mActivityToken); + mActivityCallbacks.get(i).preExecute(clientTransactionHandler, mActivityToken); } } if (mLifecycleStateRequest != null) { - mLifecycleStateRequest.prepare(clientTransactionHandler, mActivityToken); - } - } - - /** - * Execute the transaction. - * @param clientTransactionHandler Handler on the client side that will execute all operations - * requested by transaction items. - */ - public void execute(android.app.ClientTransactionHandler clientTransactionHandler) { - if (mActivityCallbacks != null) { - final int size = mActivityCallbacks.size(); - for (int i = 0; i < size; ++i) { - mActivityCallbacks.get(i).execute(clientTransactionHandler, mActivityToken); - } - } - if (mLifecycleStateRequest != null) { - mLifecycleStateRequest.execute(clientTransactionHandler, mActivityToken); + mLifecycleStateRequest.preExecute(clientTransactionHandler, mActivityToken); } } /** * Schedule the transaction after it was initialized. It will be send to client and all its * individual parts will be applied in the following sequence: - * 1. The client calls {@link #prepare(ClientTransactionHandler)}, which triggers all work that - * needs to be done before actually scheduling the transaction for callbacks and lifecycle - * state request. + * 1. The client calls {@link #preExecute(ClientTransactionHandler)}, which triggers all work + * that needs to be done before actually scheduling the transaction for callbacks and + * lifecycle state request. * 2. The transaction message is scheduled. - * 3. The client calls {@link #execute(ClientTransactionHandler)}, which executes all callbacks - * and necessary lifecycle transitions. + * 3. The client calls {@link TransactionExecutor#execute(ClientTransaction)}, which executes + * all callbacks and necessary lifecycle transitions. */ public void schedule() throws RemoteException { mClient.scheduleTransaction(this); } + // ObjectPoolItem implementation + + private ClientTransaction() {} + + /** Obtain an instance initialized with provided params. */ + public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) { + ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class); + if (instance == null) { + instance = new ClientTransaction(); + } + instance.mClient = client; + instance.mActivityToken = activityToken; + + return instance; + } + + @Override + public void recycle() { + if (mActivityCallbacks != null) { + int size = mActivityCallbacks.size(); + for (int i = 0; i < size; i++) { + mActivityCallbacks.get(i).recycle(); + } + mActivityCallbacks.clear(); + } + if (mLifecycleStateRequest != null) { + mLifecycleStateRequest.recycle(); + mLifecycleStateRequest = null; + } + mClient = null; + mActivityToken = null; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ diff --git a/android/app/servertransaction/ConfigurationChangeItem.java b/android/app/servertransaction/ConfigurationChangeItem.java index 055923ec..4ab7251e 100644 --- a/android/app/servertransaction/ConfigurationChangeItem.java +++ b/android/app/servertransaction/ConfigurationChangeItem.java @@ -16,32 +16,55 @@ package android.app.servertransaction; +import android.app.ClientTransactionHandler; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; +import java.util.Objects; + /** * App configuration change message. * @hide */ public class ConfigurationChangeItem extends ClientTransactionItem { - private final Configuration mConfiguration; - - public ConfigurationChangeItem(Configuration configuration) { - mConfiguration = new Configuration(configuration); - } + private Configuration mConfiguration; @Override - public void prepare(android.app.ClientTransactionHandler client, IBinder token) { + public void preExecute(android.app.ClientTransactionHandler client, IBinder token) { client.updatePendingConfiguration(mConfiguration); } @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { client.handleConfigurationChanged(mConfiguration); } + + // ObjectPoolItem implementation + + private ConfigurationChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ConfigurationChangeItem obtain(Configuration config) { + ConfigurationChangeItem instance = ObjectPool.obtain(ConfigurationChangeItem.class); + if (instance == null) { + instance = new ConfigurationChangeItem(); + } + instance.mConfiguration = config; + + return instance; + } + + @Override + public void recycle() { + mConfiguration = null; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -75,11 +98,16 @@ public class ConfigurationChangeItem extends ClientTransactionItem { return false; } final ConfigurationChangeItem other = (ConfigurationChangeItem) o; - return mConfiguration.equals(other.mConfiguration); + return Objects.equals(mConfiguration, other.mConfiguration); } @Override public int hashCode() { return mConfiguration.hashCode(); } + + @Override + public String toString() { + return "ConfigurationChangeItem{config=" + mConfiguration + "}"; + } } diff --git a/android/app/servertransaction/DestroyActivityItem.java b/android/app/servertransaction/DestroyActivityItem.java index 38fd5fb6..83da5f33 100644 --- a/android/app/servertransaction/DestroyActivityItem.java +++ b/android/app/servertransaction/DestroyActivityItem.java @@ -29,16 +29,12 @@ import android.os.Trace; */ public class DestroyActivityItem extends ActivityLifecycleItem { - private final boolean mFinished; - private final int mConfigChanges; - - public DestroyActivityItem(boolean finished, int configChanges) { - mFinished = finished; - mConfigChanges = configChanges; - } + private boolean mFinished; + private int mConfigChanges; @Override - public void execute(ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy"); client.handleDestroyActivity(token, mFinished, mConfigChanges, false /* getNonConfigInstance */); @@ -47,7 +43,31 @@ public class DestroyActivityItem extends ActivityLifecycleItem { @Override public int getTargetState() { - return DESTROYED; + return ON_DESTROY; + } + + + // ObjectPoolItem implementation + + private DestroyActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static DestroyActivityItem obtain(boolean finished, int configChanges) { + DestroyActivityItem instance = ObjectPool.obtain(DestroyActivityItem.class); + if (instance == null) { + instance = new DestroyActivityItem(); + } + instance.mFinished = finished; + instance.mConfigChanges = configChanges; + + return instance; + } + + @Override + public void recycle() { + mFinished = false; + mConfigChanges = 0; + ObjectPool.recycle(this); } @@ -96,4 +116,10 @@ public class DestroyActivityItem extends ActivityLifecycleItem { result = 31 * result + mConfigChanges; return result; } + + @Override + public String toString() { + return "DestroyActivityItem{finished=" + mFinished + ",mConfigChanges=" + + mConfigChanges + "}"; + } } diff --git a/android/app/servertransaction/LaunchActivityItem.java b/android/app/servertransaction/LaunchActivityItem.java index 417ebac8..7be82bf9 100644 --- a/android/app/servertransaction/LaunchActivityItem.java +++ b/android/app/servertransaction/LaunchActivityItem.java @@ -18,6 +18,7 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; import android.app.ProfilerInfo; import android.app.ResultInfo; @@ -42,68 +43,69 @@ import java.util.Objects; * Request to launch an activity. * @hide */ -public class LaunchActivityItem extends ActivityLifecycleItem { - - private final Intent mIntent; - private final int mIdent; - private final ActivityInfo mInfo; - private final Configuration mCurConfig; - private final Configuration mOverrideConfig; - private final CompatibilityInfo mCompatInfo; - private final String mReferrer; - private final IVoiceInteractor mVoiceInteractor; - private final int mProcState; - private final Bundle mState; - private final PersistableBundle mPersistentState; - private final List mPendingResults; - private final List mPendingNewIntents; - // TODO(lifecycler): use lifecycle request instead of this param. - private final boolean mNotResumed; - private final boolean mIsForward; - private final ProfilerInfo mProfilerInfo; - - public LaunchActivityItem(Intent intent, int ident, ActivityInfo info, - Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, - String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, - PersistableBundle persistentState, List pendingResults, - List pendingNewIntents, boolean notResumed, boolean isForward, - ProfilerInfo profilerInfo) { - mIntent = intent; - mIdent = ident; - mInfo = info; - mCurConfig = curConfig; - mOverrideConfig = overrideConfig; - mCompatInfo = compatInfo; - mReferrer = referrer; - mVoiceInteractor = voiceInteractor; - mProcState = procState; - mState = state; - mPersistentState = persistentState; - mPendingResults = pendingResults; - mPendingNewIntents = pendingNewIntents; - mNotResumed = notResumed; - mIsForward = isForward; - mProfilerInfo = profilerInfo; - } +public class LaunchActivityItem extends ClientTransactionItem { + + private Intent mIntent; + private int mIdent; + private ActivityInfo mInfo; + private Configuration mCurConfig; + private Configuration mOverrideConfig; + private CompatibilityInfo mCompatInfo; + private String mReferrer; + private IVoiceInteractor mVoiceInteractor; + private int mProcState; + private Bundle mState; + private PersistableBundle mPersistentState; + private List mPendingResults; + private List mPendingNewIntents; + private boolean mIsForward; + private ProfilerInfo mProfilerInfo; @Override - public void prepare(ClientTransactionHandler client, IBinder token) { + public void preExecute(ClientTransactionHandler client, IBinder token) { client.updateProcessState(mProcState, false); client.updatePendingConfiguration(mCurConfig); } @Override - public void execute(ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); - client.handleLaunchActivity(token, mIntent, mIdent, mInfo, mOverrideConfig, mCompatInfo, - mReferrer, mVoiceInteractor, mState, mPersistentState, mPendingResults, - mPendingNewIntents, mNotResumed, mIsForward, mProfilerInfo); + ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo, + mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState, + mPendingResults, mPendingNewIntents, mIsForward, + mProfilerInfo, client); + client.handleLaunchActivity(r, pendingActions); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + + // ObjectPoolItem implementation + + private LaunchActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static LaunchActivityItem obtain(Intent intent, int ident, ActivityInfo info, + Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, + String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, + PersistableBundle persistentState, List pendingResults, + List pendingNewIntents, boolean isForward, ProfilerInfo profilerInfo) { + LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class); + if (instance == null) { + instance = new LaunchActivityItem(); + } + setValues(instance, intent, ident, info, curConfig, overrideConfig, compatInfo, referrer, + voiceInteractor, procState, state, persistentState, pendingResults, + pendingNewIntents, isForward, profilerInfo); + + return instance; + } + @Override - public int getTargetState() { - return mNotResumed ? PAUSED : RESUMED; + public void recycle() { + setValues(this, null, 0, null, null, null, null, null, null, 0, null, null, null, null, + false, null); + ObjectPool.recycle(this); } @@ -119,35 +121,28 @@ public class LaunchActivityItem extends ActivityLifecycleItem { dest.writeTypedObject(mOverrideConfig, flags); dest.writeTypedObject(mCompatInfo, flags); dest.writeString(mReferrer); - dest.writeStrongBinder(mVoiceInteractor != null ? mVoiceInteractor.asBinder() : null); + dest.writeStrongInterface(mVoiceInteractor); dest.writeInt(mProcState); dest.writeBundle(mState); dest.writePersistableBundle(mPersistentState); dest.writeTypedList(mPendingResults, flags); dest.writeTypedList(mPendingNewIntents, flags); - dest.writeBoolean(mNotResumed); dest.writeBoolean(mIsForward); dest.writeTypedObject(mProfilerInfo, flags); } /** Read from Parcel. */ private LaunchActivityItem(Parcel in) { - mIntent = in.readTypedObject(Intent.CREATOR); - mIdent = in.readInt(); - mInfo = in.readTypedObject(ActivityInfo.CREATOR); - mCurConfig = in.readTypedObject(Configuration.CREATOR); - mOverrideConfig = in.readTypedObject(Configuration.CREATOR); - mCompatInfo = in.readTypedObject(CompatibilityInfo.CREATOR); - mReferrer = in.readString(); - mVoiceInteractor = (IVoiceInteractor) in.readStrongBinder(); - mProcState = in.readInt(); - mState = in.readBundle(getClass().getClassLoader()); - mPersistentState = in.readPersistableBundle(getClass().getClassLoader()); - mPendingResults = in.createTypedArrayList(ResultInfo.CREATOR); - mPendingNewIntents = in.createTypedArrayList(ReferrerIntent.CREATOR); - mNotResumed = in.readBoolean(); - mIsForward = in.readBoolean(); - mProfilerInfo = in.readTypedObject(ProfilerInfo.CREATOR); + setValues(this, in.readTypedObject(Intent.CREATOR), in.readInt(), + in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR), + in.readTypedObject(Configuration.CREATOR), + in.readTypedObject(CompatibilityInfo.CREATOR), in.readString(), + IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(), + in.readBundle(getClass().getClassLoader()), + in.readPersistableBundle(getClass().getClassLoader()), + in.createTypedArrayList(ResultInfo.CREATOR), + in.createTypedArrayList(ReferrerIntent.CREATOR), in.readBoolean(), + in.readTypedObject(ProfilerInfo.CREATOR)); } public static final Creator CREATOR = @@ -170,7 +165,9 @@ public class LaunchActivityItem extends ActivityLifecycleItem { return false; } final LaunchActivityItem other = (LaunchActivityItem) o; - return mIntent.filterEquals(other.mIntent) && mIdent == other.mIdent + final boolean intentsEqual = (mIntent == null && other.mIntent == null) + || (mIntent != null && mIntent.filterEquals(other.mIntent)); + return intentsEqual && mIdent == other.mIdent && activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig) && Objects.equals(mOverrideConfig, other.mOverrideConfig) && Objects.equals(mCompatInfo, other.mCompatInfo) @@ -179,7 +176,7 @@ public class LaunchActivityItem extends ActivityLifecycleItem { && areBundlesEqual(mPersistentState, other.mPersistentState) && Objects.equals(mPendingResults, other.mPendingResults) && Objects.equals(mPendingNewIntents, other.mPendingNewIntents) - && mNotResumed == other.mNotResumed && mIsForward == other.mIsForward + && mIsForward == other.mIsForward && Objects.equals(mProfilerInfo, other.mProfilerInfo); } @@ -197,14 +194,17 @@ public class LaunchActivityItem extends ActivityLifecycleItem { result = 31 * result + (mPersistentState != null ? mPersistentState.size() : 0); result = 31 * result + Objects.hashCode(mPendingResults); result = 31 * result + Objects.hashCode(mPendingNewIntents); - result = 31 * result + (mNotResumed ? 1 : 0); result = 31 * result + (mIsForward ? 1 : 0); result = 31 * result + Objects.hashCode(mProfilerInfo); return result; } private boolean activityInfoEqual(ActivityInfo other) { - return mInfo.flags == other.flags && mInfo.maxAspectRatio == other.maxAspectRatio + if (mInfo == null) { + return other == null; + } + return other != null && mInfo.flags == other.flags + && mInfo.maxAspectRatio == other.maxAspectRatio && Objects.equals(mInfo.launchToken, other.launchToken) && Objects.equals(mInfo.getComponentName(), other.getComponentName()); } @@ -229,4 +229,38 @@ public class LaunchActivityItem extends ActivityLifecycleItem { } return true; } + + @Override + public String toString() { + return "LaunchActivityItem{intent=" + mIntent + ",ident=" + mIdent + ",info=" + mInfo + + ",curConfig=" + mCurConfig + ",overrideConfig=" + mOverrideConfig + + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState + + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults + + ",pendingNewIntents=" + mPendingNewIntents + ",profilerInfo=" + mProfilerInfo + + "}"; + } + + // Using the same method to set and clear values to make sure we don't forget anything + private static void setValues(LaunchActivityItem instance, Intent intent, int ident, + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List pendingResults, List pendingNewIntents, + boolean isForward, ProfilerInfo profilerInfo) { + instance.mIntent = intent; + instance.mIdent = ident; + instance.mInfo = info; + instance.mCurConfig = curConfig; + instance.mOverrideConfig = overrideConfig; + instance.mCompatInfo = compatInfo; + instance.mReferrer = referrer; + instance.mVoiceInteractor = voiceInteractor; + instance.mProcState = procState; + instance.mState = state; + instance.mPersistentState = persistentState; + instance.mPendingResults = pendingResults; + instance.mPendingNewIntents = pendingNewIntents; + instance.mIsForward = isForward; + instance.mProfilerInfo = profilerInfo; + } } diff --git a/android/app/servertransaction/MoveToDisplayItem.java b/android/app/servertransaction/MoveToDisplayItem.java index ccd80d88..b3dddfb3 100644 --- a/android/app/servertransaction/MoveToDisplayItem.java +++ b/android/app/servertransaction/MoveToDisplayItem.java @@ -18,33 +18,56 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ClientTransactionHandler; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import java.util.Objects; + /** * Activity move to a different display message. * @hide */ public class MoveToDisplayItem extends ClientTransactionItem { - private final int mTargetDisplayId; - private final Configuration mConfiguration; - - public MoveToDisplayItem(int targetDisplayId, Configuration configuration) { - mTargetDisplayId = targetDisplayId; - mConfiguration = configuration; - } + private int mTargetDisplayId; + private Configuration mConfiguration; @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay"); client.handleActivityConfigurationChanged(token, mConfiguration, mTargetDisplayId); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + // ObjectPoolItem implementation + + private MoveToDisplayItem() {} + + /** Obtain an instance initialized with provided params. */ + public static MoveToDisplayItem obtain(int targetDisplayId, Configuration configuration) { + MoveToDisplayItem instance = ObjectPool.obtain(MoveToDisplayItem.class); + if (instance == null) { + instance = new MoveToDisplayItem(); + } + instance.mTargetDisplayId = targetDisplayId; + instance.mConfiguration = configuration; + + return instance; + } + + @Override + public void recycle() { + mTargetDisplayId = 0; + mConfiguration = null; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -80,7 +103,7 @@ public class MoveToDisplayItem extends ClientTransactionItem { } final MoveToDisplayItem other = (MoveToDisplayItem) o; return mTargetDisplayId == other.mTargetDisplayId - && mConfiguration.equals(other.mConfiguration); + && Objects.equals(mConfiguration, other.mConfiguration); } @Override @@ -90,4 +113,10 @@ public class MoveToDisplayItem extends ClientTransactionItem { result = 31 * result + mConfiguration.hashCode(); return result; } + + @Override + public String toString() { + return "MoveToDisplayItem{targetDisplayId=" + mTargetDisplayId + + ",configuration=" + mConfiguration + "}"; + } } diff --git a/android/app/servertransaction/MultiWindowModeChangeItem.java b/android/app/servertransaction/MultiWindowModeChangeItem.java index a0c617fa..c3022d6f 100644 --- a/android/app/servertransaction/MultiWindowModeChangeItem.java +++ b/android/app/servertransaction/MultiWindowModeChangeItem.java @@ -16,10 +16,13 @@ package android.app.servertransaction; +import android.app.ClientTransactionHandler; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; +import java.util.Objects; + /** * Multi-window mode change message. * @hide @@ -28,18 +31,38 @@ import android.os.Parcel; // communicate multi-window mode change with WindowConfiguration. public class MultiWindowModeChangeItem extends ClientTransactionItem { - private final boolean mIsInMultiWindowMode; - private final Configuration mOverrideConfig; + private boolean mIsInMultiWindowMode; + private Configuration mOverrideConfig; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handleMultiWindowModeChanged(token, mIsInMultiWindowMode, mOverrideConfig); + } + + + // ObjectPoolItem implementation - public MultiWindowModeChangeItem(boolean isInMultiWindowMode, + private MultiWindowModeChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static MultiWindowModeChangeItem obtain(boolean isInMultiWindowMode, Configuration overrideConfig) { - mIsInMultiWindowMode = isInMultiWindowMode; - mOverrideConfig = overrideConfig; + MultiWindowModeChangeItem instance = ObjectPool.obtain(MultiWindowModeChangeItem.class); + if (instance == null) { + instance = new MultiWindowModeChangeItem(); + } + instance.mIsInMultiWindowMode = isInMultiWindowMode; + instance.mOverrideConfig = overrideConfig; + + return instance; } @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { - client.handleMultiWindowModeChanged(token, mIsInMultiWindowMode, mOverrideConfig); + public void recycle() { + mIsInMultiWindowMode = false; + mOverrideConfig = null; + ObjectPool.recycle(this); } @@ -79,7 +102,7 @@ public class MultiWindowModeChangeItem extends ClientTransactionItem { } final MultiWindowModeChangeItem other = (MultiWindowModeChangeItem) o; return mIsInMultiWindowMode == other.mIsInMultiWindowMode - && mOverrideConfig.equals(other.mOverrideConfig); + && Objects.equals(mOverrideConfig, other.mOverrideConfig); } @Override @@ -89,4 +112,10 @@ public class MultiWindowModeChangeItem extends ClientTransactionItem { result = 31 * result + mOverrideConfig.hashCode(); return result; } + + @Override + public String toString() { + return "MultiWindowModeChangeItem{isInMultiWindowMode=" + mIsInMultiWindowMode + + ",overrideConfig=" + mOverrideConfig + "}"; + } } diff --git a/android/app/servertransaction/NewIntentItem.java b/android/app/servertransaction/NewIntentItem.java index 61a8965a..7dfde73c 100644 --- a/android/app/servertransaction/NewIntentItem.java +++ b/android/app/servertransaction/NewIntentItem.java @@ -16,9 +16,7 @@ package android.app.servertransaction; -import static android.app.servertransaction.ActivityLifecycleItem.PAUSED; -import static android.app.servertransaction.ActivityLifecycleItem.RESUMED; - +import android.app.ClientTransactionHandler; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +25,7 @@ import android.os.Trace; import com.android.internal.content.ReferrerIntent; import java.util.List; +import java.util.Objects; /** * New intent message. @@ -34,32 +33,53 @@ import java.util.List; */ public class NewIntentItem extends ClientTransactionItem { - private final List mIntents; - private final boolean mPause; - - public NewIntentItem(List intents, boolean pause) { - mIntents = intents; - mPause = pause; - } + private List mIntents; + private boolean mPause; - @Override + // TODO(lifecycler): Switch new intent handling to this scheme. + /*@Override public int getPreExecutionState() { - return PAUSED; + return ON_PAUSE; } @Override public int getPostExecutionState() { - return RESUMED; - } + return ON_RESUME; + }*/ @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityNewIntent"); client.handleNewIntent(token, mIntents, mPause); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } + // ObjectPoolItem implementation + + private NewIntentItem() {} + + /** Obtain an instance initialized with provided params. */ + public static NewIntentItem obtain(List intents, boolean pause) { + NewIntentItem instance = ObjectPool.obtain(NewIntentItem.class); + if (instance == null) { + instance = new NewIntentItem(); + } + instance.mIntents = intents; + instance.mPause = pause; + + return instance; + } + + @Override + public void recycle() { + mIntents = null; + mPause = false; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -95,7 +115,7 @@ public class NewIntentItem extends ClientTransactionItem { return false; } final NewIntentItem other = (NewIntentItem) o; - return mPause == other.mPause && mIntents.equals(other.mIntents); + return mPause == other.mPause && Objects.equals(mIntents, other.mIntents); } @Override @@ -105,4 +125,9 @@ public class NewIntentItem extends ClientTransactionItem { result = 31 * result + mIntents.hashCode(); return result; } + + @Override + public String toString() { + return "NewIntentItem{pause=" + mPause + ",intents=" + mIntents + "}"; + } } diff --git a/android/app/servertransaction/ObjectPool.java b/android/app/servertransaction/ObjectPool.java new file mode 100644 index 00000000..2fec30a0 --- /dev/null +++ b/android/app/servertransaction/ObjectPool.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * An object pool that can provide reused objects if available. + * @hide + */ +class ObjectPool { + + private static final Object sPoolSync = new Object(); + private static final Map> sPoolMap = + new HashMap<>(); + + private static final int MAX_POOL_SIZE = 50; + + /** + * Obtain an instance of a specific class from the pool + * @param itemClass The class of the object we're looking for. + * @return An instance or null if there is none. + */ + public static T obtain(Class itemClass) { + synchronized (sPoolSync) { + @SuppressWarnings("unchecked") + final ArrayList itemPool = (ArrayList) sPoolMap.get(itemClass); + if (itemPool != null && !itemPool.isEmpty()) { + return itemPool.remove(itemPool.size() - 1); + } + return null; + } + } + + /** + * Recycle the object to the pool. The object should be properly cleared before this. + * @param item The object to recycle. + * @see ObjectPoolItem#recycle() + */ + public static void recycle(T item) { + synchronized (sPoolSync) { + @SuppressWarnings("unchecked") + ArrayList itemPool = (ArrayList) sPoolMap.get(item.getClass()); + if (itemPool == null) { + itemPool = new ArrayList<>(); + sPoolMap.put(item.getClass(), itemPool); + } + // Check if the item is already in the pool + final int size = itemPool.size(); + for (int i = 0; i < size; i++) { + if (itemPool.get(i) == item) { + throw new IllegalStateException("Trying to recycle already recycled item"); + } + } + + if (size < MAX_POOL_SIZE) { + itemPool.add(item); + } + } + } +} diff --git a/android/app/servertransaction/ObjectPoolItem.java b/android/app/servertransaction/ObjectPoolItem.java new file mode 100644 index 00000000..17bd4f30 --- /dev/null +++ b/android/app/servertransaction/ObjectPoolItem.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +/** + * Base interface for all lifecycle items that can be put in object pool. + * @hide + */ +public interface ObjectPoolItem { + /** + * Clear the contents of the item and putting it to a pool. The implementation should call + * {@link ObjectPool#recycle(ObjectPoolItem)} passing itself. + */ + void recycle(); +} diff --git a/android/app/servertransaction/PauseActivityItem.java b/android/app/servertransaction/PauseActivityItem.java index e561a4b5..880fef73 100644 --- a/android/app/servertransaction/PauseActivityItem.java +++ b/android/app/servertransaction/PauseActivityItem.java @@ -18,11 +18,12 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ActivityManager; import android.app.ClientTransactionHandler; import android.os.IBinder; import android.os.Parcel; +import android.os.RemoteException; import android.os.Trace; -import android.util.Slog; /** * Request to move an activity to paused state. @@ -32,43 +33,81 @@ public class PauseActivityItem extends ActivityLifecycleItem { private static final String TAG = "PauseActivityItem"; - private final boolean mFinished; - private final boolean mUserLeaving; - private final int mConfigChanges; - private final boolean mDontReport; + private boolean mFinished; + private boolean mUserLeaving; + private int mConfigChanges; + private boolean mDontReport; - private int mLifecycleSeq; + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); + client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, mDontReport, + pendingActions); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } - public PauseActivityItem(boolean finished, boolean userLeaving, int configChanges, - boolean dontReport) { - mFinished = finished; - mUserLeaving = userLeaving; - mConfigChanges = configChanges; - mDontReport = dontReport; + @Override + public int getTargetState() { + return ON_PAUSE; } @Override - public void prepare(ClientTransactionHandler client, IBinder token) { - mLifecycleSeq = client.getLifecycleSeq(); - if (DEBUG_ORDER) { - Slog.d(TAG, "Pause transaction for " + client + " received seq: " - + mLifecycleSeq); + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + if (mDontReport) { + return; + } + try { + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityPaused(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } - @Override - public void execute(ClientTransactionHandler client, IBinder token) { - Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); - client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, mDontReport, - mLifecycleSeq); - Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + + // ObjectPoolItem implementation + + private PauseActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static PauseActivityItem obtain(boolean finished, boolean userLeaving, int configChanges, + boolean dontReport) { + PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class); + if (instance == null) { + instance = new PauseActivityItem(); + } + instance.mFinished = finished; + instance.mUserLeaving = userLeaving; + instance.mConfigChanges = configChanges; + instance.mDontReport = dontReport; + + return instance; } - @Override - public int getTargetState() { - return PAUSED; + /** Obtain an instance initialized with default params. */ + public static PauseActivityItem obtain() { + PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class); + if (instance == null) { + instance = new PauseActivityItem(); + } + instance.mFinished = false; + instance.mUserLeaving = false; + instance.mConfigChanges = 0; + instance.mDontReport = true; + + return instance; } + @Override + public void recycle() { + mFinished = false; + mUserLeaving = false; + mConfigChanges = 0; + mDontReport = false; + ObjectPool.recycle(this); + } // Parcelable implementation @@ -122,4 +161,10 @@ public class PauseActivityItem extends ActivityLifecycleItem { result = 31 * result + (mDontReport ? 1 : 0); return result; } + + @Override + public String toString() { + return "PauseActivityItem{finished=" + mFinished + ",userLeaving=" + mUserLeaving + + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport + "}"; + } } diff --git a/android/app/servertransaction/PendingTransactionActions.java b/android/app/servertransaction/PendingTransactionActions.java new file mode 100644 index 00000000..8304c1c5 --- /dev/null +++ b/android/app/servertransaction/PendingTransactionActions.java @@ -0,0 +1,145 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +import static android.app.ActivityThread.DEBUG_MEMORY_TRIM; + +import android.app.ActivityManager; +import android.app.ActivityThread.ActivityClientRecord; +import android.os.Build; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.TransactionTooLargeException; +import android.util.Log; +import android.util.LogWriter; +import android.util.Slog; + +import com.android.internal.util.IndentingPrintWriter; + +/** + * Container that has data pending to be used at later stages of + * {@link android.app.servertransaction.ClientTransaction}. + * An instance of this class is passed to each individual transaction item, so it can use some + * information from previous steps or add some for the following steps. + * + * @hide + */ +public class PendingTransactionActions { + private boolean mRestoreInstanceState; + private boolean mCallOnPostCreate; + private Bundle mOldState; + private StopInfo mStopInfo; + + public PendingTransactionActions() { + clear(); + } + + /** Reset the state of the instance to default, non-initialized values. */ + public void clear() { + mRestoreInstanceState = false; + mCallOnPostCreate = false; + mOldState = null; + mStopInfo = null; + } + + /** Getter */ + public boolean shouldRestoreInstanceState() { + return mRestoreInstanceState; + } + + public void setRestoreInstanceState(boolean restoreInstanceState) { + mRestoreInstanceState = restoreInstanceState; + } + + /** Getter */ + public boolean shouldCallOnPostCreate() { + return mCallOnPostCreate; + } + + public void setCallOnPostCreate(boolean callOnPostCreate) { + mCallOnPostCreate = callOnPostCreate; + } + + public Bundle getOldState() { + return mOldState; + } + + public void setOldState(Bundle oldState) { + mOldState = oldState; + } + + public StopInfo getStopInfo() { + return mStopInfo; + } + + public void setStopInfo(StopInfo stopInfo) { + mStopInfo = stopInfo; + } + + /** Reports to server about activity stop. */ + public static class StopInfo implements Runnable { + private static final String TAG = "ActivityStopInfo"; + + private ActivityClientRecord mActivity; + private Bundle mState; + private PersistableBundle mPersistentState; + private CharSequence mDescription; + + public void setActivity(ActivityClientRecord activity) { + mActivity = activity; + } + + public void setState(Bundle state) { + mState = state; + } + + public void setPersistentState(PersistableBundle persistentState) { + mPersistentState = persistentState; + } + + public void setDescription(CharSequence description) { + mDescription = description; + } + + @Override + public void run() { + // Tell activity manager we have been stopped. + try { + if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + mActivity); + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityStopped( + mActivity.token, mState, mPersistentState, mDescription); + } catch (RemoteException ex) { + // Dump statistics about bundle to help developers debug + final LogWriter writer = new LogWriter(Log.WARN, TAG); + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + pw.println("Bundle stats:"); + Bundle.dumpStats(pw, mState); + pw.println("PersistableBundle stats:"); + Bundle.dumpStats(pw, mPersistentState); + + if (ex instanceof TransactionTooLargeException + && mActivity.loadedApk.getTargetSdkVersion() < Build.VERSION_CODES.N) { + Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex); + return; + } + throw ex.rethrowFromSystemServer(); + } + } + } +} diff --git a/android/app/servertransaction/PipModeChangeItem.java b/android/app/servertransaction/PipModeChangeItem.java index 923839ee..b999cd7e 100644 --- a/android/app/servertransaction/PipModeChangeItem.java +++ b/android/app/servertransaction/PipModeChangeItem.java @@ -16,10 +16,13 @@ package android.app.servertransaction; +import android.app.ClientTransactionHandler; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; +import java.util.Objects; + /** * Picture in picture mode change message. * @hide @@ -28,17 +31,37 @@ import android.os.Parcel; // communicate multi-window mode change with WindowConfiguration. public class PipModeChangeItem extends ClientTransactionItem { - private final boolean mIsInPipMode; - private final Configuration mOverrideConfig; + private boolean mIsInPipMode; + private Configuration mOverrideConfig; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handlePictureInPictureModeChanged(token, mIsInPipMode, mOverrideConfig); + } + + + // ObjectPoolItem implementation - public PipModeChangeItem(boolean isInPipMode, Configuration overrideConfig) { - mIsInPipMode = isInPipMode; - mOverrideConfig = overrideConfig; + private PipModeChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static PipModeChangeItem obtain(boolean isInPipMode, Configuration overrideConfig) { + PipModeChangeItem instance = ObjectPool.obtain(PipModeChangeItem.class); + if (instance == null) { + instance = new PipModeChangeItem(); + } + instance.mIsInPipMode = isInPipMode; + instance.mOverrideConfig = overrideConfig; + + return instance; } @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { - client.handlePictureInPictureModeChanged(token, mIsInPipMode, mOverrideConfig); + public void recycle() { + mIsInPipMode = false; + mOverrideConfig = null; + ObjectPool.recycle(this); } @@ -76,7 +99,8 @@ public class PipModeChangeItem extends ClientTransactionItem { return false; } final PipModeChangeItem other = (PipModeChangeItem) o; - return mIsInPipMode == other.mIsInPipMode && mOverrideConfig.equals(other.mOverrideConfig); + return mIsInPipMode == other.mIsInPipMode + && Objects.equals(mOverrideConfig, other.mOverrideConfig); } @Override @@ -86,4 +110,10 @@ public class PipModeChangeItem extends ClientTransactionItem { result = 31 * result + mOverrideConfig.hashCode(); return result; } + + @Override + public String toString() { + return "PipModeChangeItem{isInPipMode=" + mIsInPipMode + + ",overrideConfig=" + mOverrideConfig + "}"; + } } diff --git a/android/app/servertransaction/ResumeActivityItem.java b/android/app/servertransaction/ResumeActivityItem.java index ea31a461..9249c6e8 100644 --- a/android/app/servertransaction/ResumeActivityItem.java +++ b/android/app/servertransaction/ResumeActivityItem.java @@ -18,11 +18,12 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ActivityManager; import android.app.ClientTransactionHandler; import android.os.IBinder; import android.os.Parcel; +import android.os.RemoteException; import android.os.Trace; -import android.util.Slog; /** * Request to move an activity to resumed state. @@ -32,37 +33,78 @@ public class ResumeActivityItem extends ActivityLifecycleItem { private static final String TAG = "ResumeActivityItem"; - private final int mProcState; - private final boolean mIsForward; - - private int mLifecycleSeq; - - public ResumeActivityItem(int procState, boolean isForward) { - mProcState = procState; - mIsForward = isForward; - } + private int mProcState; + private boolean mUpdateProcState; + private boolean mIsForward; @Override - public void prepare(ClientTransactionHandler client, IBinder token) { - mLifecycleSeq = client.getLifecycleSeq(); - if (DEBUG_ORDER) { - Slog.d(TAG, "Resume transaction for " + client + " received seq: " - + mLifecycleSeq); + public void preExecute(ClientTransactionHandler client, IBinder token) { + if (mUpdateProcState) { + client.updateProcessState(mProcState, false); } - client.updateProcessState(mProcState, false); } @Override - public void execute(ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); - client.handleResumeActivity(token, true /* clearHide */, mIsForward, - true /* reallyResume */, mLifecycleSeq, "RESUME_ACTIVITY"); + client.handleResumeActivity(token, true /* clearHide */, mIsForward, "RESUME_ACTIVITY"); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + @Override + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + try { + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityResumed(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + @Override public int getTargetState() { - return RESUMED; + return ON_RESUME; + } + + + // ObjectPoolItem implementation + + private ResumeActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ResumeActivityItem obtain(int procState, boolean isForward) { + ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); + if (instance == null) { + instance = new ResumeActivityItem(); + } + instance.mProcState = procState; + instance.mUpdateProcState = true; + instance.mIsForward = isForward; + + return instance; + } + + /** Obtain an instance initialized with provided params. */ + public static ResumeActivityItem obtain(boolean isForward) { + ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); + if (instance == null) { + instance = new ResumeActivityItem(); + } + instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; + instance.mUpdateProcState = false; + instance.mIsForward = isForward; + + return instance; + } + + @Override + public void recycle() { + mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; + mUpdateProcState = false; + mIsForward = false; + ObjectPool.recycle(this); } @@ -72,12 +114,14 @@ public class ResumeActivityItem extends ActivityLifecycleItem { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mProcState); + dest.writeBoolean(mUpdateProcState); dest.writeBoolean(mIsForward); } /** Read from Parcel. */ private ResumeActivityItem(Parcel in) { mProcState = in.readInt(); + mUpdateProcState = in.readBoolean(); mIsForward = in.readBoolean(); } @@ -101,14 +145,22 @@ public class ResumeActivityItem extends ActivityLifecycleItem { return false; } final ResumeActivityItem other = (ResumeActivityItem) o; - return mProcState == other.mProcState && mIsForward == other.mIsForward; + return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState + && mIsForward == other.mIsForward; } @Override public int hashCode() { int result = 17; result = 31 * result + mProcState; + result = 31 * result + (mUpdateProcState ? 1 : 0); result = 31 * result + (mIsForward ? 1 : 0); return result; } + + @Override + public String toString() { + return "ResumeActivityItem{procState=" + mProcState + + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}"; + } } diff --git a/android/app/servertransaction/StopActivityItem.java b/android/app/servertransaction/StopActivityItem.java index d62c5077..5c5c3041 100644 --- a/android/app/servertransaction/StopActivityItem.java +++ b/android/app/servertransaction/StopActivityItem.java @@ -22,7 +22,6 @@ import android.app.ClientTransactionHandler; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; -import android.util.Slog; /** * Request to move an activity to stopped state. @@ -32,35 +31,50 @@ public class StopActivityItem extends ActivityLifecycleItem { private static final String TAG = "StopActivityItem"; - private final boolean mShowWindow; - private final int mConfigChanges; + private boolean mShowWindow; + private int mConfigChanges; - private int mLifecycleSeq; - - public StopActivityItem(boolean showWindow, int configChanges) { - mShowWindow = showWindow; - mConfigChanges = configChanges; + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop"); + client.handleStopActivity(token, mShowWindow, mConfigChanges, pendingActions); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @Override - public void prepare(ClientTransactionHandler client, IBinder token) { - mLifecycleSeq = client.getLifecycleSeq(); - if (DEBUG_ORDER) { - Slog.d(TAG, "Stop transaction for " + client + " received seq: " - + mLifecycleSeq); - } + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.reportStop(pendingActions); } @Override - public void execute(ClientTransactionHandler client, IBinder token) { - Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop"); - client.handleStopActivity(token, mShowWindow, mConfigChanges, mLifecycleSeq); - Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + public int getTargetState() { + return ON_STOP; + } + + + // ObjectPoolItem implementation + + private StopActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static StopActivityItem obtain(boolean showWindow, int configChanges) { + StopActivityItem instance = ObjectPool.obtain(StopActivityItem.class); + if (instance == null) { + instance = new StopActivityItem(); + } + instance.mShowWindow = showWindow; + instance.mConfigChanges = configChanges; + + return instance; } @Override - public int getTargetState() { - return STOPPED; + public void recycle() { + mShowWindow = false; + mConfigChanges = 0; + ObjectPool.recycle(this); } @@ -109,4 +123,10 @@ public class StopActivityItem extends ActivityLifecycleItem { result = 31 * result + mConfigChanges; return result; } + + @Override + public String toString() { + return "StopActivityItem{showWindow=" + mShowWindow + ",configChanges=" + mConfigChanges + + "}"; + } } diff --git a/android/app/servertransaction/TransactionExecutor.java b/android/app/servertransaction/TransactionExecutor.java new file mode 100644 index 00000000..5b0ea6b1 --- /dev/null +++ b/android/app/servertransaction/TransactionExecutor.java @@ -0,0 +1,248 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESTART; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME; +import static android.app.servertransaction.ActivityLifecycleItem.ON_START; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; + +import android.app.ActivityThread.ActivityClientRecord; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.util.IntArray; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; + +/** + * Class that manages transaction execution in the correct order. + * @hide + */ +public class TransactionExecutor { + + private static final boolean DEBUG_RESOLVER = false; + private static final String TAG = "TransactionExecutor"; + + private ClientTransactionHandler mTransactionHandler; + private PendingTransactionActions mPendingActions = new PendingTransactionActions(); + + // Temp holder for lifecycle path. + // No direct transition between two states should take more than one complete cycle of 6 states. + @ActivityLifecycleItem.LifecycleState + private IntArray mLifecycleSequence = new IntArray(6); + + /** Initialize an instance with transaction handler, that will execute all requested actions. */ + public TransactionExecutor(ClientTransactionHandler clientTransactionHandler) { + mTransactionHandler = clientTransactionHandler; + } + + /** + * Resolve transaction. + * First all callbacks will be executed in the order they appear in the list. If a callback + * requires a certain pre- or post-execution state, the client will be transitioned accordingly. + * Then the client will cycle to the final lifecycle state if provided. Otherwise, it will + * either remain in the initial state, or last state needed by a callback. + */ + public void execute(ClientTransaction transaction) { + final IBinder token = transaction.getActivityToken(); + log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token); + + executeCallbacks(transaction); + + executeLifecycleState(transaction); + mPendingActions.clear(); + log("End resolving transaction"); + } + + /** Cycle through all states requested by callbacks and execute them at proper times. */ + @VisibleForTesting + public void executeCallbacks(ClientTransaction transaction) { + final List callbacks = transaction.getCallbacks(); + if (callbacks == null) { + // No callbacks to execute, return early. + return; + } + log("Resolving callbacks"); + + final IBinder token = transaction.getActivityToken(); + ActivityClientRecord r = mTransactionHandler.getActivityClient(token); + final int size = callbacks.size(); + for (int i = 0; i < size; ++i) { + final ClientTransactionItem item = callbacks.get(i); + log("Resolving callback: " + item); + final int preExecutionState = item.getPreExecutionState(); + if (preExecutionState != UNDEFINED) { + cycleToPath(r, preExecutionState); + } + + item.execute(mTransactionHandler, token, mPendingActions); + item.postExecute(mTransactionHandler, token, mPendingActions); + if (r == null) { + // Launch activity request will create an activity record. + r = mTransactionHandler.getActivityClient(token); + } + + final int postExecutionState = item.getPostExecutionState(); + if (postExecutionState != UNDEFINED) { + cycleToPath(r, postExecutionState); + } + } + } + + /** Transition to the final state if requested by the transaction. */ + private void executeLifecycleState(ClientTransaction transaction) { + final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest(); + if (lifecycleItem == null) { + // No lifecycle request, return early. + return; + } + log("Resolving lifecycle state: " + lifecycleItem); + + final IBinder token = transaction.getActivityToken(); + final ActivityClientRecord r = mTransactionHandler.getActivityClient(token); + + // Cycle to the state right before the final requested state. + cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */); + + // Execute the final transition with proper parameters. + lifecycleItem.execute(mTransactionHandler, token, mPendingActions); + lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions); + } + + /** Transition the client between states. */ + @VisibleForTesting + public void cycleToPath(ActivityClientRecord r, int finish) { + cycleToPath(r, finish, false /* excludeLastState */); + } + + /** + * Transition the client between states with an option not to perform the last hop in the + * sequence. This is used when resolving lifecycle state request, when the last transition must + * be performed with some specific parameters. + */ + private void cycleToPath(ActivityClientRecord r, int finish, + boolean excludeLastState) { + final int start = r.getLifecycleState(); + log("Cycle from: " + start + " to: " + finish + " excludeLastState:" + excludeLastState); + initLifecyclePath(start, finish, excludeLastState); + performLifecycleSequence(r); + } + + /** Transition the client through previously initialized state sequence. */ + private void performLifecycleSequence(ActivityClientRecord r) { + final int size = mLifecycleSequence.size(); + for (int i = 0, state; i < size; i++) { + state = mLifecycleSequence.get(i); + log("Transitioning to state: " + state); + switch (state) { + case ON_CREATE: + mTransactionHandler.handleLaunchActivity(r, mPendingActions); + break; + case ON_START: + mTransactionHandler.handleStartActivity(r, mPendingActions); + break; + case ON_RESUME: + mTransactionHandler.handleResumeActivity(r.token, false /* clearHide */, + r.isForward, "LIFECYCLER_RESUME_ACTIVITY"); + break; + case ON_PAUSE: + mTransactionHandler.handlePauseActivity(r.token, false /* finished */, + false /* userLeaving */, 0 /* configChanges */, + true /* dontReport */, mPendingActions); + break; + case ON_STOP: + mTransactionHandler.handleStopActivity(r.token, false /* show */, + 0 /* configChanges */, mPendingActions); + break; + case ON_DESTROY: + mTransactionHandler.handleDestroyActivity(r.token, false /* finishing */, + 0 /* configChanges */, false /* getNonConfigInstance */); + break; + case ON_RESTART: + mTransactionHandler.performRestartActivity(r.token, false /* start */); + break; + default: + throw new IllegalArgumentException("Unexpected lifecycle state: " + state); + } + } + } + + /** + * Calculate the path through main lifecycle states for an activity and fill + * @link #mLifecycleSequence} with values starting with the state that follows the initial + * state. + */ + public void initLifecyclePath(int start, int finish, boolean excludeLastState) { + mLifecycleSequence.clear(); + if (finish >= start) { + // just go there + for (int i = start + 1; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } else { // finish < start, can't just cycle down + if (start == ON_PAUSE && finish == ON_RESUME) { + // Special case when we can just directly go to resumed state. + mLifecycleSequence.add(ON_RESUME); + } else if (start <= ON_STOP && finish >= ON_START) { + // Restart and go to required state. + + // Go to stopped state first. + for (int i = start + 1; i <= ON_STOP; i++) { + mLifecycleSequence.add(i); + } + // Restart + mLifecycleSequence.add(ON_RESTART); + // Go to required state + for (int i = ON_START; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } else { + // Relaunch and go to required state + + // Go to destroyed state first. + for (int i = start + 1; i <= ON_DESTROY; i++) { + mLifecycleSequence.add(i); + } + // Go to required state + for (int i = ON_CREATE; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } + } + + // Remove last transition in case we want to perform it with some specific params. + if (excludeLastState && mLifecycleSequence.size() != 0) { + mLifecycleSequence.remove(mLifecycleSequence.size() - 1); + } + } + + @VisibleForTesting + public int[] getLifecycleSequence() { + return mLifecycleSequence.toArray(); + } + + private static void log(String message) { + if (DEBUG_RESOLVER) Slog.d(TAG, message); + } +} diff --git a/android/app/servertransaction/WindowVisibilityItem.java b/android/app/servertransaction/WindowVisibilityItem.java index 8e88b38d..d9956b13 100644 --- a/android/app/servertransaction/WindowVisibilityItem.java +++ b/android/app/servertransaction/WindowVisibilityItem.java @@ -18,6 +18,7 @@ package android.app.servertransaction; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import android.app.ClientTransactionHandler; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; @@ -28,20 +29,39 @@ import android.os.Trace; */ public class WindowVisibilityItem extends ClientTransactionItem { - private final boolean mShowWindow; - - public WindowVisibilityItem(boolean showWindow) { - mShowWindow = showWindow; - } + private boolean mShowWindow; @Override - public void execute(android.app.ClientTransactionHandler client, IBinder token) { + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow"); client.handleWindowVisibility(token, mShowWindow); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } + // ObjectPoolItem implementation + + private WindowVisibilityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static WindowVisibilityItem obtain(boolean showWindow) { + WindowVisibilityItem instance = ObjectPool.obtain(WindowVisibilityItem.class); + if (instance == null) { + instance = new WindowVisibilityItem(); + } + instance.mShowWindow = showWindow; + + return instance; + } + + @Override + public void recycle() { + mShowWindow = false; + ObjectPool.recycle(this); + } + + // Parcelable implementation /** Write to Parcel. */ @@ -82,4 +102,9 @@ public class WindowVisibilityItem extends ClientTransactionItem { public int hashCode() { return 17 + 31 * (mShowWindow ? 1 : 0); } + + @Override + public String toString() { + return "WindowVisibilityItem{showWindow=" + mShowWindow + "}"; + } } diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java index ddc5760a..5c7f6741 100644 --- a/android/app/slice/Slice.java +++ b/android/app/slice/Slice.java @@ -37,6 +37,8 @@ import android.os.RemoteException; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -53,9 +55,21 @@ public final class Slice implements Parcelable { /** * @hide */ - @StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED, - HINT_NO_TINT, HINT_PARTIAL}) - public @interface SliceHint{ } + @StringDef(prefix = { "HINT_" }, value = { + HINT_TITLE, + HINT_LIST, + HINT_LIST_ITEM, + HINT_LARGE, + HINT_ACTIONS, + HINT_SELECTED, + HINT_NO_TINT, + HINT_SHORTCUT, + HINT_TOGGLE, + HINT_HORIZONTAL, + HINT_PARTIAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SliceHint {} /** * The meta-data key that allows an activity to easily be linked directly to a slice. @@ -104,12 +118,15 @@ public final class Slice implements Parcelable { */ public static final String HINT_NO_TINT = "no_tint"; /** - * Hint to indicate that this content should not be shown in larger renderings - * of Slices. This content may be used to populate the shortcut/icon - * format of the slice. - * @hide + * Hint to indicate that this content should only be displayed if the slice is presented + * as a shortcut. + */ + public static final String HINT_SHORTCUT = "shortcut"; + /** + * Hint indicating this content should be shown instead of the normal content when the slice + * is in small format. */ - public static final String HINT_HIDDEN = "hidden"; + public static final String HINT_SUMMARY = "summary"; /** * Hint to indicate that this content has a toggle action associated with it. To indicate that * the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the intent @@ -129,10 +146,14 @@ public final class Slice implements Parcelable { * OS and should not be cached by apps. */ public static final String HINT_PARTIAL = "partial"; + /** + * A hint representing that this item is the max value possible for the slice containing this. + * Used to indicate the maximum integer value for a {@link #SUBTYPE_SLIDER}. + */ + public static final String HINT_MAX = "max"; /** * Key to retrieve an extra added to an intent when a control is changed. - * @hide */ public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE"; /** @@ -144,6 +165,25 @@ public final class Slice implements Parcelable { * Subtype to tag the source (i.e. sender) of a {@link #SUBTYPE_MESSAGE}. */ public static final String SUBTYPE_SOURCE = "source"; + /** + * Subtype to tag an item as representing a color. + */ + public static final String SUBTYPE_COLOR = "color"; + /** + * Subtype to tag an item represents a slider. + */ + public static final String SUBTYPE_SLIDER = "slider"; + /** + * Subtype to indicate that this content has a toggle action associated with it. To indicate + * that the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the + * intent associated with it will be sent along with an extra {@link #EXTRA_TOGGLE_STATE} + * which can be retrieved to see the new state of the toggle. + */ + public static final String SUBTYPE_TOGGLE = "toggle"; + /** + * Subtype to tag an item representing priority. + */ + public static final String SUBTYPE_PRIORITY = "priority"; private final SliceItem[] mItems; private final @SliceHint String[] mHints; @@ -375,9 +415,10 @@ public final class Slice implements Parcelable { * Add a color to the slice being constructed * @param subType Optional template-specific type information * @see {@link SliceItem#getSubType()} + * @deprecated will be removed once supportlib updates */ public Builder addColor(int color, @Nullable String subType, @SliceHint String... hints) { - mItems.add(new SliceItem(color, SliceItem.FORMAT_COLOR, subType, hints)); + mItems.add(new SliceItem(color, SliceItem.FORMAT_INT, subType, hints)); return this; } @@ -385,12 +426,33 @@ public final class Slice implements Parcelable { * Add a color to the slice being constructed * @param subType Optional template-specific type information * @see {@link SliceItem#getSubType()} + * @deprecated will be removed once supportlib updates */ public Builder addColor(int color, @Nullable String subType, @SliceHint List 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)); + return this; + } + + /** + * Add a color to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addInt(int value, @Nullable String subType, + @SliceHint List hints) { + return addInt(value, subType, hints.toArray(new String[hints.size()])); + } + /** * Add a timestamp to the slice being constructed * @param subType Optional template-specific type information @@ -413,6 +475,32 @@ public final class Slice implements Parcelable { return addTimestamp(time, subType, hints.toArray(new String[hints.size()])); } + /** + * Add a bundle to the slice being constructed. + *

Expected to be used for support library extension, should not be used for general + * development + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addBundle(Bundle bundle, @Nullable String subType, + @SliceHint String... hints) { + mItems.add(new SliceItem(bundle, SliceItem.FORMAT_BUNDLE, subType, + hints)); + return this; + } + + /** + * Add a bundle to the slice being constructed. + *

Expected to be used for support library extension, should not be used for general + * development + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addBundle(Bundle bundle, @Nullable String subType, + @SliceHint List hints) { + return addBundle(bundle, subType, hints.toArray(new String[hints.size()])); + } + /** * Construct the slice. */ diff --git a/android/app/slice/SliceItem.java b/android/app/slice/SliceItem.java index cdeee357..bcfd413f 100644 --- a/android/app/slice/SliceItem.java +++ b/android/app/slice/SliceItem.java @@ -21,6 +21,7 @@ import android.annotation.StringDef; import android.app.PendingIntent; import android.app.RemoteInput; import android.graphics.drawable.Icon; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -29,6 +30,8 @@ import android.widget.RemoteViews; import com.android.internal.util.ArrayUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.List; @@ -42,9 +45,10 @@ import java.util.List; *

  • {@link #FORMAT_TEXT}
  • *
  • {@link #FORMAT_IMAGE}
  • *
  • {@link #FORMAT_ACTION}
  • - *
  • {@link #FORMAT_COLOR}
  • + *
  • {@link #FORMAT_INT}
  • *
  • {@link #FORMAT_TIMESTAMP}
  • *
  • {@link #FORMAT_REMOTE_INPUT}
  • + *
  • {@link #FORMAT_BUNDLE}
  • * * The hints that a {@link SliceItem} are a set of strings which annotate * the content. The hints that are guaranteed to be understood by the system @@ -52,11 +56,22 @@ import java.util.List; */ public final class SliceItem implements Parcelable { + private static final String TAG = "SliceItem"; + /** * @hide */ - @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_COLOR, - FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT}) + @StringDef(prefix = { "FORMAT_" }, value = { + FORMAT_SLICE, + FORMAT_TEXT, + FORMAT_IMAGE, + FORMAT_ACTION, + FORMAT_INT, + FORMAT_TIMESTAMP, + FORMAT_REMOTE_INPUT, + FORMAT_BUNDLE, + }) + @Retention(RetentionPolicy.SOURCE) public @interface SliceType {} /** @@ -79,7 +94,12 @@ public final class SliceItem implements Parcelable { */ public static final String FORMAT_ACTION = "action"; /** - * A {@link SliceItem} that contains a Color int. + * A {@link SliceItem} that contains an int. + */ + public static final String FORMAT_INT = "int"; + /** + * A {@link SliceItem} that contains an int. + * @deprecated to be removed */ public static final String FORMAT_COLOR = "color"; /** @@ -90,6 +110,10 @@ public final class SliceItem implements Parcelable { * A {@link SliceItem} that contains a {@link RemoteInput}. */ public static final String FORMAT_REMOTE_INPUT = "input"; + /** + * A {@link SliceItem} that contains a {@link Bundle}. + */ + public static final String FORMAT_BUNDLE = "bundle"; /** * @hide @@ -127,20 +151,6 @@ public final class SliceItem implements Parcelable { return Arrays.asList(mHints); } - /** - * @hide - */ - public void addHint(@Slice.SliceHint String hint) { - mHints = ArrayUtils.appendElement(String.class, mHints, hint); - } - - /** - * @hide - */ - public void removeHint(String hint) { - ArrayUtils.removeElement(String.class, mHints, hint); - } - /** * Get the format of this SliceItem. *

    @@ -149,9 +159,10 @@ public final class SliceItem implements Parcelable { *

  • {@link #FORMAT_TEXT}
  • *
  • {@link #FORMAT_IMAGE}
  • *
  • {@link #FORMAT_ACTION}
  • - *
  • {@link #FORMAT_COLOR}
  • + *
  • {@link #FORMAT_INT}
  • *
  • {@link #FORMAT_TIMESTAMP}
  • *
  • {@link #FORMAT_REMOTE_INPUT}
  • + *
  • {@link #FORMAT_BUNDLE}
  • * @see #getSubType() () */ public String getFormat() { @@ -177,6 +188,13 @@ public final class SliceItem implements Parcelable { return (CharSequence) mObj; } + /** + * @return The parcelable held by this {@link #FORMAT_BUNDLE} SliceItem + */ + public Bundle getBundle() { + return (Bundle) mObj; + } + /** * @return The icon held by this {@link #FORMAT_IMAGE} SliceItem */ @@ -206,7 +224,14 @@ public final class SliceItem implements Parcelable { } /** - * @return The color held by this {@link #FORMAT_COLOR} SliceItem + * @return The color held by this {@link #FORMAT_INT} SliceItem + */ + public int getInt() { + return (Integer) mObj; + } + + /** + * @deprecated to be removed. */ public int getColor() { return (Integer) mObj; @@ -299,6 +324,7 @@ public final class SliceItem implements Parcelable { case FORMAT_SLICE: case FORMAT_IMAGE: case FORMAT_REMOTE_INPUT: + case FORMAT_BUNDLE: ((Parcelable) obj).writeToParcel(dest, flags); break; case FORMAT_ACTION: @@ -308,7 +334,7 @@ public final class SliceItem implements Parcelable { case FORMAT_TEXT: TextUtils.writeToParcel((CharSequence) obj, dest, flags); break; - case FORMAT_COLOR: + case FORMAT_INT: dest.writeInt((Integer) obj); break; case FORMAT_TIMESTAMP: @@ -329,12 +355,14 @@ public final class SliceItem implements Parcelable { return new Pair<>( PendingIntent.CREATOR.createFromParcel(in), Slice.CREATOR.createFromParcel(in)); - case FORMAT_COLOR: + case FORMAT_INT: return in.readInt(); case FORMAT_TIMESTAMP: return in.readLong(); case FORMAT_REMOTE_INPUT: return RemoteInput.CREATOR.createFromParcel(in); + case FORMAT_BUNDLE: + return Bundle.CREATOR.createFromParcel(in); } throw new RuntimeException("Unsupported type " + type); } diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java new file mode 100644 index 00000000..0c5f225d --- /dev/null +++ b/android/app/slice/SliceManager.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.slice; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; +import android.util.ArrayMap; +import android.util.Pair; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Class to handle interactions with {@link Slice}s. + *

    + * The SliceManager manages permissions and pinned state for slices. + */ +@SystemService(Context.SLICE_SERVICE) +public class SliceManager { + + private final ISliceManager mService; + private final Context mContext; + private final ArrayMap, ISliceListener> mListenerLookup = + new ArrayMap<>(); + + /** + * @hide + */ + public SliceManager(Context context, Handler handler) throws ServiceNotFoundException { + mContext = context; + mService = ISliceManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE)); + } + + /** + * Adds a callback to a specific slice uri. + *

    + * This is a convenience that performs a few slice actions at once. It will put + * the slice in a pinned state since there is a callback attached. It will also + * listen for content changes, when a content change observes, the android system + * will bind the new slice and provide it to all registered {@link SliceCallback}s. + * + * @param uri The uri of the slice being listened to. + * @param callback The listener that should receive the callbacks. + * @param specs The list of supported {@link SliceSpec}s of the callback. + * @see SliceProvider#onSlicePinned(Uri) + */ + public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback, + @NonNull List specs) { + registerSliceCallback(uri, callback, specs, Handler.getMain()); + } + + /** + * Adds a callback to a specific slice uri. + *

    + * This is a convenience that performs a few slice actions at once. It will put + * the slice in a pinned state since there is a callback attached. It will also + * listen for content changes, when a content change observes, the android system + * will bind the new slice and provide it to all registered {@link SliceCallback}s. + * + * @param uri The uri of the slice being listened to. + * @param callback The listener that should receive the callbacks. + * @param specs The list of supported {@link SliceSpec}s of the callback. + * @see SliceProvider#onSlicePinned(Uri) + */ + public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback, + @NonNull List specs, Handler handler) { + try { + mService.addSliceListener(uri, mContext.getPackageName(), + getListener(uri, callback, new ISliceListener.Stub() { + @Override + public void onSliceUpdated(Slice s) throws RemoteException { + handler.post(() -> callback.onSliceUpdated(s)); + } + }), specs.toArray(new SliceSpec[specs.size()])); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds a callback to a specific slice uri. + *

    + * This is a convenience that performs a few slice actions at once. It will put + * the slice in a pinned state since there is a callback attached. It will also + * listen for content changes, when a content change observes, the android system + * will bind the new slice and provide it to all registered {@link SliceCallback}s. + * + * @param uri The uri of the slice being listened to. + * @param callback The listener that should receive the callbacks. + * @param specs The list of supported {@link SliceSpec}s of the callback. + * @see SliceProvider#onSlicePinned(Uri) + */ + public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback, + @NonNull List specs, Executor executor) { + try { + mService.addSliceListener(uri, mContext.getPackageName(), + getListener(uri, callback, new ISliceListener.Stub() { + @Override + public void onSliceUpdated(Slice s) throws RemoteException { + executor.execute(() -> callback.onSliceUpdated(s)); + } + }), specs.toArray(new SliceSpec[specs.size()])); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private ISliceListener getListener(Uri uri, SliceCallback callback, + ISliceListener listener) { + Pair key = new Pair<>(uri, callback); + if (mListenerLookup.containsKey(key)) { + try { + mService.removeSliceListener(uri, mContext.getPackageName(), + mListenerLookup.get(key)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + mListenerLookup.put(key, listener); + return listener; + } + + /** + * Removes a callback for a specific slice uri. + *

    + * Removes the app from the pinned state (if there are no other apps/callbacks pinning it) + * in addition to removing the callback. + * + * @param uri The uri of the slice being listened to + * @param callback The listener that should no longer receive callbacks. + * @see #registerSliceCallback + */ + public void unregisterSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback) { + try { + mService.removeSliceListener(uri, mContext.getPackageName(), + mListenerLookup.remove(new Pair<>(uri, callback))); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Ensures that a slice is in a pinned state. + *

    + * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices + * they still care about after a reboot. + * + * @param uri The uri of the slice being pinned. + * @param specs The list of supported {@link SliceSpec}s of the callback. + * @see SliceProvider#onSlicePinned(Uri) + */ + public void pinSlice(@NonNull Uri uri, @NonNull List specs) { + try { + mService.pinSlice(mContext.getPackageName(), uri, + specs.toArray(new SliceSpec[specs.size()])); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove a pin for a slice. + *

    + * If the slice has no other pins/callbacks then the slice will be unpinned. + * + * @param uri The uri of the slice being unpinned. + * @see #pinSlice + * @see SliceProvider#onSliceUnpinned(Uri) + */ + public void unpinSlice(@NonNull Uri uri) { + try { + mService.unpinSlice(mContext.getPackageName(), uri); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public boolean hasSliceAccess() { + try { + return mService.hasSliceAccess(mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the current set of specs for a pinned slice. + *

    + * This is the set of specs supported for a specific pinned slice. It will take + * into account all clients and returns only specs supported by all. + * @see SliceSpec + */ + public @NonNull List getPinnedSpecs(Uri uri) { + try { + return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Class that listens to changes in {@link Slice}s. + */ + public interface SliceCallback { + + /** + * Called when slice is updated. + * + * @param s The updated slice. + * @see #registerSliceCallback + */ + void onSliceUpdated(Slice s); + } +} diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java index ac5365c3..8483931c 100644 --- a/android/app/slice/SliceProvider.java +++ b/android/app/slice/SliceProvider.java @@ -102,6 +102,14 @@ public abstract class SliceProvider extends ContentProvider { * @hide */ public static final String METHOD_MAP_INTENT = "map_slice"; + /** + * @hide + */ + public static final String METHOD_PIN = "pin"; + /** + * @hide + */ + public static final String METHOD_UNPIN = "unpin"; /** * @hide */ @@ -142,6 +150,38 @@ public abstract class SliceProvider extends ContentProvider { return null; } + /** + * Called to inform an app that a slice has been pinned. + *

    + * 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. + *

    + * 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. + *

    + * Pinned state is not persisted through a reboot, and apps can expect a + * new call to onSlicePinned for any slices that should remain pinned + * after a reboot occurs. + * + * @param sliceUri The uri of the slice being unpinned. + * @see #onSliceUnpinned(Uri) + */ + public void onSlicePinned(Uri sliceUri) { + } + + /** + * Called to inform an app that a slices is no longer pinned. + *

    + * This means that no other apps on the device care about updates to this + * slice anymore and therefore it is not important to be updated. Any syncs + * or jobs related to this slice should be cancelled. + * @see #onSlicePinned(Uri) + */ + public void onSliceUnpinned(Uri sliceUri) { + } + /** * This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider. * In that case, this method can be called and is expected to return a non-null Uri representing @@ -221,6 +261,7 @@ public abstract class SliceProvider extends ContentProvider { getContext().enforceCallingPermission(permission.BIND_SLICE, "Slice binding requires the permission BIND_SLICE"); Intent intent = extras.getParcelable(EXTRA_INTENT); + if (intent == null) return null; Uri uri = onMapIntentToUri(intent); List supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); Bundle b = new Bundle(); @@ -231,10 +272,62 @@ public abstract class SliceProvider extends ContentProvider { b.putParcelable(EXTRA_SLICE, null); } return b; + } else if (method.equals(METHOD_PIN)) { + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + } + handlePinSlice(uri); + } else if (method.equals(METHOD_UNPIN)) { + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + } + handleUnpinSlice(uri); } return super.call(method, arg, extras); } + private void handlePinSlice(Uri sliceUri) { + if (Looper.myLooper() == Looper.getMainLooper()) { + onSlicePinned(sliceUri); + } else { + CountDownLatch latch = new CountDownLatch(1); + Handler.getMain().post(() -> { + onSlicePinned(sliceUri); + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void handleUnpinSlice(Uri sliceUri) { + if (Looper.myLooper() == Looper.getMainLooper()) { + onSliceUnpinned(sliceUri); + } else { + CountDownLatch latch = new CountDownLatch(1); + Handler.getMain().post(() -> { + onSliceUnpinned(sliceUri); + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + private Slice handleBindSlice(Uri sliceUri, List supportedSpecs) { if (Looper.myLooper() == Looper.getMainLooper()) { return onBindSliceStrict(sliceUri, supportedSpecs); diff --git a/android/app/slice/SliceSpec.java b/android/app/slice/SliceSpec.java index 433b67e9..8cc0384c 100644 --- a/android/app/slice/SliceSpec.java +++ b/android/app/slice/SliceSpec.java @@ -103,6 +103,11 @@ public final class SliceSpec implements Parcelable { return mType.equals(other.mType) && mRevision == other.mRevision; } + @Override + public String toString() { + return String.format("SliceSpec{%s,%d}", mType, mRevision); + } + public static final Creator CREATOR = new Creator() { @Override public SliceSpec createFromParcel(Parcel source) { diff --git a/android/app/timezone/Callback.java b/android/app/timezone/Callback.java index aea80380..e3840be6 100644 --- a/android/app/timezone/Callback.java +++ b/android/app/timezone/Callback.java @@ -30,9 +30,14 @@ import java.lang.annotation.RetentionPolicy; public abstract class Callback { @Retention(RetentionPolicy.SOURCE) - @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_INSTALL_BAD_DISTRO_STRUCTURE, - ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION, ERROR_INSTALL_RULES_TOO_OLD, - ERROR_INSTALL_VALIDATION_ERROR}) + @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = { + SUCCESS, + ERROR_UNKNOWN_FAILURE, + ERROR_INSTALL_BAD_DISTRO_STRUCTURE, + ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION, + ERROR_INSTALL_RULES_TOO_OLD, + ERROR_INSTALL_VALIDATION_ERROR + }) public @interface AsyncResultCode {} /** diff --git a/android/app/timezone/RulesManager.java b/android/app/timezone/RulesManager.java index ad9b698a..0a38eb9a 100644 --- a/android/app/timezone/RulesManager.java +++ b/android/app/timezone/RulesManager.java @@ -69,7 +69,11 @@ public final class RulesManager { private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) - @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_OPERATION_IN_PROGRESS}) + @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = { + SUCCESS, + ERROR_UNKNOWN_FAILURE, + ERROR_OPERATION_IN_PROGRESS + }) public @interface ResultCode {} /** @@ -105,9 +109,9 @@ public final class RulesManager { */ public RulesState getRulesState() { try { - logDebug("sIRulesManager.getRulesState()"); + logDebug("mIRulesManager.getRulesState()"); RulesState rulesState = mIRulesManager.getRulesState(); - logDebug("sIRulesManager.getRulesState() returned " + rulesState); + logDebug("mIRulesManager.getRulesState() returned " + rulesState); return rulesState; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -131,7 +135,7 @@ public final class RulesManager { ICallback iCallback = new CallbackWrapper(mContext, callback); try { - logDebug("sIRulesManager.requestInstall()"); + logDebug("mIRulesManager.requestInstall()"); return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -151,7 +155,7 @@ public final class RulesManager { public int requestUninstall(byte[] checkToken, Callback callback) { ICallback iCallback = new CallbackWrapper(mContext, callback); try { - logDebug("sIRulesManager.requestUninstall()"); + logDebug("mIRulesManager.requestUninstall()"); return mIRulesManager.requestUninstall(checkToken, iCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -196,7 +200,7 @@ public final class RulesManager { */ public void requestNothing(byte[] checkToken, boolean succeeded) { try { - logDebug("sIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken)); + logDebug("mIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken)); mIRulesManager.requestNothing(checkToken, succeeded); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/android/app/timezone/RulesState.java b/android/app/timezone/RulesState.java index ec247ebf..16309fab 100644 --- a/android/app/timezone/RulesState.java +++ b/android/app/timezone/RulesState.java @@ -63,11 +63,12 @@ import java.lang.annotation.RetentionPolicy; public final class RulesState implements Parcelable { @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "STAGED_OPERATION_" }, value = { STAGED_OPERATION_UNKNOWN, STAGED_OPERATION_NONE, STAGED_OPERATION_UNINSTALL, - STAGED_OPERATION_INSTALL }) + STAGED_OPERATION_INSTALL + }) private @interface StagedOperationType {} /** Staged state could not be determined. */ @@ -80,10 +81,11 @@ public final class RulesState implements Parcelable { public static final int STAGED_OPERATION_INSTALL = 3; @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "DISTRO_STATUS_" }, value = { DISTRO_STATUS_UNKNOWN, DISTRO_STATUS_NONE, - DISTRO_STATUS_INSTALLED }) + DISTRO_STATUS_INSTALLED + }) private @interface DistroStatus {} /** The current distro status could not be determined. */ diff --git a/android/app/usage/AppStandby.java b/android/app/usage/AppStandby.java deleted file mode 100644 index 6f9fc2fa..00000000 --- a/android/app/usage/AppStandby.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app.usage; - -import android.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Set of constants for app standby buckets and reasons. Apps will be moved into different buckets - * that affect how frequently they can run in the background or perform other battery-consuming - * actions. Buckets will be assigned based on how frequently or when the system thinks the user - * is likely to use the app. - * @hide - */ -public class AppStandby { - - /** The app was used very recently, currently in use or likely to be used very soon. */ - public static final int STANDBY_BUCKET_ACTIVE = 0; - - // Leave some gap in case we want to increase the number of buckets - - /** The app was used recently and/or likely to be used in the next few hours */ - public static final int STANDBY_BUCKET_WORKING_SET = 3; - - // Leave some gap in case we want to increase the number of buckets - - /** The app was used in the last few days and/or likely to be used in the next few days */ - public static final int STANDBY_BUCKET_FREQUENT = 6; - - // Leave some gap in case we want to increase the number of buckets - - /** The app has not be used for several days and/or is unlikely to be used for several days */ - public static final int STANDBY_BUCKET_RARE = 9; - - // Leave some gap in case we want to increase the number of buckets - - /** The app has never been used. */ - public static final int STANDBY_BUCKET_NEVER = 12; - - /** Reason for bucketing -- default initial state */ - public static final String REASON_DEFAULT = "default"; - - /** Reason for bucketing -- timeout */ - public static final String REASON_TIMEOUT = "timeout"; - - /** Reason for bucketing -- usage */ - public static final String REASON_USAGE = "usage"; - - /** Reason for bucketing -- forced by user / shell command */ - public static final String REASON_FORCED = "forced"; - - /** - * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will - * be appended. - */ - public static final String REASON_PREDICTED = "predicted"; - - @IntDef(flag = false, value = { - STANDBY_BUCKET_ACTIVE, - STANDBY_BUCKET_WORKING_SET, - STANDBY_BUCKET_FREQUENT, - STANDBY_BUCKET_RARE, - STANDBY_BUCKET_NEVER, - }) - @Retention(RetentionPolicy.SOURCE) - public @interface StandbyBuckets {} -} diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java index 222e9a0e..2e44a630 100644 --- a/android/app/usage/NetworkStats.java +++ b/android/app/usage/NetworkStats.java @@ -129,7 +129,11 @@ public final class NetworkStats implements AutoCloseable { */ public static class Bucket { /** @hide */ - @IntDef({STATE_ALL, STATE_DEFAULT, STATE_FOREGROUND}) + @IntDef(prefix = { "STATE_" }, value = { + STATE_ALL, + STATE_DEFAULT, + STATE_FOREGROUND + }) @Retention(RetentionPolicy.SOURCE) public @interface State {} @@ -164,7 +168,11 @@ public final class NetworkStats implements AutoCloseable { public static final int UID_TETHERING = TrafficStats.UID_TETHERING; /** @hide */ - @IntDef({METERED_ALL, METERED_NO, METERED_YES}) + @IntDef(prefix = { "METERED_" }, value = { + METERED_ALL, + METERED_NO, + METERED_YES + }) @Retention(RetentionPolicy.SOURCE) public @interface Metered {} @@ -187,7 +195,11 @@ public final class NetworkStats implements AutoCloseable { public static final int METERED_YES = 0x2; /** @hide */ - @IntDef({ROAMING_ALL, ROAMING_NO, ROAMING_YES}) + @IntDef(prefix = { "ROAMING_" }, value = { + ROAMING_ALL, + ROAMING_NO, + ROAMING_YES + }) @Retention(RetentionPolicy.SOURCE) public @interface Roaming {} diff --git a/android/app/usage/StorageStatsManager.java b/android/app/usage/StorageStatsManager.java index 3d187ec7..a86c27a0 100644 --- a/android/app/usage/StorageStatsManager.java +++ b/android/app/usage/StorageStatsManager.java @@ -78,6 +78,16 @@ public class StorageStatsManager { return isQuotaSupported(convert(uuid)); } + /** {@hide} */ + @TestApi + public boolean isReservedSupported(@NonNull UUID storageUuid) { + try { + return mService.isReservedSupported(convert(storageUuid), mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Return the total size of the underlying physical media that is hosting * this storage volume. diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java index 8200414f..f04e9074 100644 --- a/android/app/usage/UsageEvents.java +++ b/android/app/usage/UsageEvents.java @@ -110,10 +110,9 @@ public final class UsageEvents implements Parcelable { public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0; /** @hide */ - @IntDef(flag = true, - value = { - FLAG_IS_PACKAGE_INSTANT_APP, - }) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_IS_PACKAGE_INSTANT_APP, + }) @Retention(RetentionPolicy.SOURCE) public @interface EventFlags {} diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java index 3a3e16e0..edb6a74b 100644 --- a/android/app/usage/UsageStatsManager.java +++ b/android/app/usage/UsageStatsManager.java @@ -16,16 +16,18 @@ package android.app.usage; +import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; -import android.app.usage.AppStandby.StandbyBuckets; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; import java.util.Map; @@ -90,6 +92,76 @@ public final class UsageStatsManager { */ public static final int INTERVAL_COUNT = 4; + + /** + * The app is whitelisted for some reason and the bucket cannot be changed. + * {@hide} + */ + @SystemApi + public static final int STANDBY_BUCKET_EXEMPTED = 5; + + /** + * The app was used very recently, currently in use or likely to be used very soon. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_ACTIVE = 10; + + /** + * The app was used recently and/or likely to be used in the next few hours. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_WORKING_SET = 20; + + /** + * The app was used in the last few days and/or likely to be used in the next few days. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_FREQUENT = 30; + + /** + * The app has not be used for several days and/or is unlikely to be used for several days. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_RARE = 40; + + /** + * The app has never been used. + * {@hide} + */ + @SystemApi + public static final int STANDBY_BUCKET_NEVER = 50; + + /** {@hide} Reason for bucketing -- default initial state */ + public static final String REASON_DEFAULT = "default"; + + /** {@hide} Reason for bucketing -- timeout */ + public static final String REASON_TIMEOUT = "timeout"; + + /** {@hide} Reason for bucketing -- usage */ + public static final String REASON_USAGE = "usage"; + + /** {@hide} Reason for bucketing -- forced by user / shell command */ + public static final String REASON_FORCED = "forced"; + + /** + * {@hide} + * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will + * be appended. + */ + public static final String REASON_PREDICTED = "predicted"; + + /** @hide */ + @IntDef(flag = false, prefix = { "STANDBY_BUCKET_" }, value = { + STANDBY_BUCKET_EXEMPTED, + STANDBY_BUCKET_ACTIVE, + STANDBY_BUCKET_WORKING_SET, + STANDBY_BUCKET_FREQUENT, + STANDBY_BUCKET_RARE, + STANDBY_BUCKET_NEVER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StandbyBuckets {} + private static final UsageEvents sEmptyResults = new UsageEvents(); private final Context mContext; @@ -237,7 +309,7 @@ public final class UsageStatsManager { } /** - * @hide + * {@hide} */ public void setAppInactive(String packageName, boolean inactive) { try { @@ -248,20 +320,52 @@ public final class UsageStatsManager { } /** - * @hide + * Returns the current standby bucket of the calling app. The system determines the standby + * state of the app based on app usage patterns. Standby buckets determine how much an app will + * be restricted from running background tasks such as jobs, alarms and certain PendingIntent + * callbacks. + *

    Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to + * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least + * restrictive. The battery level of the device might also affect the restrictions. + * + * @return the current standby bucket of the calling app. One of STANDBY_BUCKET_* constants. */ + public @StandbyBuckets int getAppStandbyBucket() { + try { + return mService.getAppStandbyBucket(mContext.getOpPackageName(), + mContext.getOpPackageName(), + mContext.getUserId()); + } catch (RemoteException e) { + } + return STANDBY_BUCKET_ACTIVE; + } + + /** + * {@hide} + * Returns the current standby bucket of the specified app. The caller must hold the permission + * android.permission.PACKAGE_USAGE_STATS. + * @param packageName the package for which to fetch the current standby bucket. + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public @StandbyBuckets int getAppStandbyBucket(String packageName) { try { return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { } - return AppStandby.STANDBY_BUCKET_ACTIVE; + return STANDBY_BUCKET_ACTIVE; } /** - * @hide - * Changes the app standby state to the provided bucket. + * {@hide} + * Changes an app's standby bucket to the provided value. The caller can only set the standby + * bucket for a different app than itself. + * @param packageName the package name of the app to set the bucket for. A SecurityException + * will be thrown if the package name is that of the caller. + * @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*. + * Setting a standby bucket outside of the range of STANDBY_BUCKET_ACTIVE to + * STANDBY_BUCKET_NEVER will result in a SecurityException. */ @SystemApi @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE) @@ -273,6 +377,39 @@ public final class UsageStatsManager { } } + /** + * {@hide} + * Returns the current standby bucket of every app that has a bucket assigned to it. + * The caller must hold the permission android.permission.PACKAGE_USAGE_STATS. The key of the + * returned Map is the package name and the value is the bucket assigned to the package. + * @see #getAppStandbyBucket() + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + public Map getAppStandbyBuckets() { + try { + return (Map) mService.getAppStandbyBuckets( + mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException e) { + } + return Collections.EMPTY_MAP; + } + + /** + * {@hide} + * Changes the app standby bucket for multiple apps at once. The Map is keyed by the package + * name and the value is one of STANDBY_BUCKET_*. + * @param appBuckets a map of package name to bucket value. + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE) + public void setAppStandbyBuckets(Map appBuckets) { + try { + mService.setAppStandbyBuckets(appBuckets, mContext.getUserId()); + } catch (RemoteException e) { + } + } + /** * {@hide} * Temporarily whitelist the specified app for a short duration. This is to allow an app diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java index 9954484f..4b4fe72f 100644 --- a/android/app/usage/UsageStatsManagerInternal.java +++ b/android/app/usage/UsageStatsManagerInternal.java @@ -16,7 +16,7 @@ package android.app.usage; -import android.app.usage.AppStandby.StandbyBuckets; +import android.app.usage.UsageStatsManager.StandbyBuckets; import android.content.ComponentName; import android.content.res.Configuration; diff --git a/android/appwidget/AppWidgetManagerInternal.java b/android/appwidget/AppWidgetManagerInternal.java new file mode 100644 index 00000000..7ab3d8bd --- /dev/null +++ b/android/appwidget/AppWidgetManagerInternal.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.appwidget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArraySet; + +import java.util.Set; + +/** + * App widget manager local system service interface. + * + * @hide Only for use within the system server. + */ +public abstract class AppWidgetManagerInternal { + + /** + * Gets the packages from which the uid hosts widgets. + * + * @param uid The potential host UID. + * @return Whether the UID hosts widgets from the package. + */ + public abstract @Nullable ArraySet getHostedWidgetPackages(int uid); +} diff --git a/android/appwidget/AppWidgetProviderInfo.java b/android/appwidget/AppWidgetProviderInfo.java index fd1b0e02..75ce4fbb 100644 --- a/android/appwidget/AppWidgetProviderInfo.java +++ b/android/appwidget/AppWidgetProviderInfo.java @@ -17,15 +17,17 @@ package android.appwidget; import android.annotation.NonNull; +import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.ResourceId; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.content.ComponentName; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -68,6 +70,23 @@ public class AppWidgetProviderInfo implements Parcelable { */ public static final int WIDGET_CATEGORY_SEARCHBOX = 4; + /** + * The widget can be reconfigured anytime after it is bound by starting the + * {@link #configure} activity. + * + * @see #widgetFeatures + */ + public static final int WIDGET_FEATURE_RECONFIGURABLE = 1; + + /** + * The widget is added directly by the app, and the host may hide this widget when providing + * the user with the list of available widgets to choose from. + * + * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent) + * @see #widgetFeatures + */ + public static final int WIDGET_FEATURE_HIDE_FROM_PICKER = 2; + /** * Identity of this AppWidget component. This component should be a {@link * android.content.BroadcastReceiver}, and it will be sent the AppWidget intents @@ -209,6 +228,15 @@ public class AppWidgetProviderInfo implements Parcelable { */ public int widgetCategory; + /** + * Flags indicating various features supported by the widget. These are hints to the widget + * host, and do not actually change the behavior of the widget. + * + * @see #WIDGET_FEATURE_RECONFIGURABLE + * @see #WIDGET_FEATURE_HIDE_FROM_PICKER + */ + public int widgetFeatures; + /** @hide */ public ActivityInfo providerInfo; @@ -221,9 +249,7 @@ public class AppWidgetProviderInfo implements Parcelable { */ @SuppressWarnings("deprecation") public AppWidgetProviderInfo(Parcel in) { - if (0 != in.readInt()) { - this.provider = new ComponentName(in); - } + this.provider = in.readTypedObject(ComponentName.CREATOR); this.minWidth = in.readInt(); this.minHeight = in.readInt(); this.minResizeWidth = in.readInt(); @@ -231,16 +257,15 @@ public class AppWidgetProviderInfo implements Parcelable { this.updatePeriodMillis = in.readInt(); this.initialLayout = in.readInt(); this.initialKeyguardLayout = in.readInt(); - if (0 != in.readInt()) { - this.configure = new ComponentName(in); - } + this.configure = in.readTypedObject(ComponentName.CREATOR); this.label = in.readString(); this.icon = in.readInt(); this.previewImage = in.readInt(); this.autoAdvanceViewId = in.readInt(); this.resizeMode = in.readInt(); this.widgetCategory = in.readInt(); - this.providerInfo = in.readParcelable(null); + this.providerInfo = in.readTypedObject(ActivityInfo.CREATOR); + this.widgetFeatures = in.readInt(); } /** @@ -308,13 +333,8 @@ public class AppWidgetProviderInfo implements Parcelable { @Override @SuppressWarnings("deprecation") - public void writeToParcel(android.os.Parcel out, int flags) { - if (this.provider != null) { - out.writeInt(1); - this.provider.writeToParcel(out, flags); - } else { - out.writeInt(0); - } + public void writeToParcel(Parcel out, int flags) { + out.writeTypedObject(this.provider, flags); out.writeInt(this.minWidth); out.writeInt(this.minHeight); out.writeInt(this.minResizeWidth); @@ -322,19 +342,15 @@ public class AppWidgetProviderInfo implements Parcelable { out.writeInt(this.updatePeriodMillis); out.writeInt(this.initialLayout); out.writeInt(this.initialKeyguardLayout); - if (this.configure != null) { - out.writeInt(1); - this.configure.writeToParcel(out, flags); - } else { - out.writeInt(0); - } + out.writeTypedObject(this.configure, flags); out.writeString(this.label); out.writeInt(this.icon); out.writeInt(this.previewImage); out.writeInt(this.autoAdvanceViewId); out.writeInt(this.resizeMode); out.writeInt(this.widgetCategory); - out.writeParcelable(this.providerInfo, flags); + out.writeTypedObject(this.providerInfo, flags); + out.writeInt(this.widgetFeatures); } @Override @@ -357,6 +373,7 @@ public class AppWidgetProviderInfo implements Parcelable { that.resizeMode = this.resizeMode; that.widgetCategory = this.widgetCategory; that.providerInfo = this.providerInfo; + that.widgetFeatures = this.widgetFeatures; return that; } diff --git a/android/arch/core/executor/ArchTaskExecutor.java b/android/arch/core/executor/ArchTaskExecutor.java index 2401a730..6276ee34 100644 --- a/android/arch/core/executor/ArchTaskExecutor.java +++ b/android/arch/core/executor/ArchTaskExecutor.java @@ -64,6 +64,7 @@ public class ArchTaskExecutor extends TaskExecutor { * * @return The singleton ArchTaskExecutor. */ + @NonNull public static ArchTaskExecutor getInstance() { if (sInstance != null) { return sInstance; diff --git a/android/arch/core/executor/TaskExecutor.java b/android/arch/core/executor/TaskExecutor.java index 055b4763..71758019 100644 --- a/android/arch/core/executor/TaskExecutor.java +++ b/android/arch/core/executor/TaskExecutor.java @@ -16,6 +16,7 @@ package android.arch.core.executor; +import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; /** @@ -33,14 +34,14 @@ public abstract class TaskExecutor { * * @param runnable The runnable to run in the disk IO thread pool. */ - public abstract void executeOnDiskIO(Runnable runnable); + public abstract void executeOnDiskIO(@NonNull Runnable runnable); /** * Posts the given task to the main thread. * * @param runnable The runnable to run on the main thread. */ - public abstract void postToMainThread(Runnable runnable); + public abstract void postToMainThread(@NonNull Runnable runnable); /** * Executes the given task on the main thread. @@ -49,7 +50,7 @@ public abstract class TaskExecutor { * * @param runnable The runnable to run on the main thread. */ - public void executeOnMainThread(Runnable runnable) { + public void executeOnMainThread(@NonNull Runnable runnable) { if (isMainThread()) { runnable.run(); } else { diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java index f1352446..1ddcb1a9 100644 --- a/android/arch/lifecycle/ComputableLiveData.java +++ b/android/arch/lifecycle/ComputableLiveData.java @@ -1,9 +1,136 @@ -//ComputableLiveData interface for tests +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.arch.lifecycle; -import android.arch.lifecycle.LiveData; + +import android.arch.core.executor.ArchTaskExecutor; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A LiveData class that can be invalidated & computed on demand. + *

    + * This is an internal class for now, might be public if we see the necessity. + * + * @param The type of the live data + * @hide internal + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class ComputableLiveData { - public ComputableLiveData(){} - abstract protected T compute(); - public LiveData getLiveData() {return null;} - public void invalidate() {} + + private final LiveData 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. + *

    + * 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() { + @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 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. + *

    + * When there are active observers, this will trigger a call to {@link #compute()}. + */ + public void invalidate() { + ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); + } + + @SuppressWarnings("WeakerAccess") + @WorkerThread + protected abstract T compute(); } diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java index c0a2090c..e04cdb4c 100644 --- a/android/arch/lifecycle/Lifecycle.java +++ b/android/arch/lifecycle/Lifecycle.java @@ -108,6 +108,7 @@ public abstract class Lifecycle { * @return The current state of the Lifecycle. */ @MainThread + @NonNull public abstract State getCurrentState(); @SuppressWarnings("WeakerAccess") diff --git a/android/arch/lifecycle/LifecycleRegistry.java b/android/arch/lifecycle/LifecycleRegistry.java index bf8aff79..eff946b2 100644 --- a/android/arch/lifecycle/LifecycleRegistry.java +++ b/android/arch/lifecycle/LifecycleRegistry.java @@ -225,6 +225,7 @@ public class LifecycleRegistry extends Lifecycle { return mObserverMap.size(); } + @NonNull @Override public State getCurrentState() { return mState; diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java index 3aea6acb..5b09c32f 100644 --- a/android/arch/lifecycle/LiveData.java +++ b/android/arch/lifecycle/LiveData.java @@ -1,4 +1,410 @@ -//LiveData interface for tests +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package android.arch.lifecycle; -public class LiveData { + +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)}. + * + *

    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. + * + *

    + * 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. + *

    + * 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 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 { + 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, 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, 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. + *

    + * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} + * or {@link Lifecycle.State#RESUMED} state (active). + *

    + * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will + * automatically be removed. + *

    + * 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. + *

    + * 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 & the owner. + *

    + * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData + * ignores the call. + *

    + * 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 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. + *

    + * 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 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 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, 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: + *

    +     * liveData.postValue("a");
    +     * liveData.setValue("b");
    +     * 
    + * The value "b" would be set at first and later the main thread would override it with + * the value "a". + *

    + * 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. + *

    + * 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. + *

    + * 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. + *

    + * 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). + *

    + * 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 observer; + public boolean active; + public int lastVersion = START_VERSION; + + LifecycleBoundObserver(LifecycleOwner owner, Observer observer) { + this.owner = owner; + this.observer = observer; + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + removeObserver(observer); + return; + } + // immediately set active state, so we'd never dispatch anything to inactive + // owner + activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); + } + + void activeStateChanged(boolean newActive) { + if (newActive == active) { + return; + } + active = newActive; + boolean wasInactive = LiveData.this.mActiveCount == 0; + LiveData.this.mActiveCount += active ? 1 : -1; + if (wasInactive && active) { + onActive(); + } + if (LiveData.this.mActiveCount == 0 && !active) { + onInactive(); + } + if (active) { + dispatchingValue(this); + } + } + } + + static boolean isActiveState(State state) { + return state.isAtLeast(STARTED); + } + + private void assertMainThread(String methodName) { + if (!ArchTaskExecutor.getInstance().isMainThread()) { + throw new IllegalStateException("Cannot invoke " + methodName + " on a background" + + " thread"); + } + } } diff --git a/android/arch/lifecycle/LiveDataReactiveStreams.java b/android/arch/lifecycle/LiveDataReactiveStreams.java index ba76f8e8..ed3c57c3 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreams.java +++ b/android/arch/lifecycle/LiveDataReactiveStreams.java @@ -50,8 +50,9 @@ public final class LiveDataReactiveStreams { * will buffer the latest item and emit it to the subscriber when data is again requested. Any * other items emitted during the time there was no backpressure requested will be dropped. */ + @NonNull public static Publisher toPublisher( - final LifecycleOwner lifecycle, final LiveData liveData) { + @NonNull LifecycleOwner lifecycle, @NonNull LiveData liveData) { return new LiveDataPublisher<>(lifecycle, liveData); } @@ -60,7 +61,7 @@ public final class LiveDataReactiveStreams { final LifecycleOwner mLifecycle; final LiveData mLiveData; - LiveDataPublisher(final LifecycleOwner lifecycle, final LiveData liveData) { + LiveDataPublisher(LifecycleOwner lifecycle, LiveData liveData) { this.mLifecycle = lifecycle; this.mLiveData = liveData; } @@ -91,7 +92,7 @@ public final class LiveDataReactiveStreams { } @Override - public void onChanged(T t) { + public void onChanged(@Nullable T t) { if (mCanceled) { return; } @@ -183,7 +184,8 @@ public final class LiveDataReactiveStreams { * * @param The type of data hold by this instance. */ - public static LiveData fromPublisher(final Publisher publisher) { + @NonNull + public static LiveData fromPublisher(@NonNull Publisher publisher) { return new PublisherLiveData<>(publisher); } @@ -209,10 +211,10 @@ public final class LiveDataReactiveStreams { * @param The type of data hold by this instance. */ private static class PublisherLiveData extends LiveData { - private final Publisher mPublisher; + private final Publisher mPublisher; final AtomicReference mSubscriber; - PublisherLiveData(@NonNull final Publisher publisher) { + PublisherLiveData(@NonNull Publisher publisher) { mPublisher = publisher; mSubscriber = new AtomicReference<>(); } diff --git a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java index 83e543c3..163cff00 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java +++ b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java @@ -16,11 +16,10 @@ package android.arch.lifecycle; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.fail; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java index 8877357a..37d2020a 100644 --- a/android/arch/lifecycle/ViewModelProviderTest.java +++ b/android/arch/lifecycle/ViewModelProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; import org.junit.Assert; import org.junit.Before; @@ -84,18 +82,6 @@ public class ViewModelProviderTest { assertThat(viewModel, is(provider.get(ViewModel1.class))); } - @Test(expected = IllegalStateException.class) - public void testNotAttachedActivity() { - // This is similar to call ViewModelProviders.of in Activity's constructor - ViewModelProviders.of(new FragmentActivity()); - } - - @Test(expected = IllegalStateException.class) - public void testNotAttachedFragment() { - // This is similar to call ViewModelProviders.of in Activity's constructor - ViewModelProviders.of(new Fragment()); - } - public static class ViewModel1 extends ViewModel { boolean mCleared; diff --git a/android/arch/lifecycle/ViewModelProvidersTest.java b/android/arch/lifecycle/ViewModelProvidersTest.java new file mode 100644 index 00000000..f37c9a29 --- /dev/null +++ b/android/arch/lifecycle/ViewModelProvidersTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ViewModelProvidersTest { + + @Test(expected = IllegalStateException.class) + public void testNotAttachedActivity() { + // This is similar to call ViewModelProviders.of in Activity's constructor + ViewModelProviders.of(new FragmentActivity()); + } + + @Test(expected = IllegalStateException.class) + public void testNotAttachedFragment() { + // This is similar to call ViewModelProviders.of in Activity's constructor + ViewModelProviders.of(new Fragment()); + } +} diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java index d7d769d6..e79c934a 100644 --- a/android/arch/lifecycle/ViewModelStores.java +++ b/android/arch/lifecycle/ViewModelStores.java @@ -40,6 +40,9 @@ public class ViewModelStores { */ @MainThread public static ViewModelStore of(@NonNull FragmentActivity activity) { + if (activity instanceof ViewModelStoreOwner) { + return ((ViewModelStoreOwner) activity).getViewModelStore(); + } return holderFragmentFor(activity).getViewModelStore(); } @@ -51,6 +54,9 @@ public class ViewModelStores { */ @MainThread public static ViewModelStore of(@NonNull Fragment fragment) { + if (fragment instanceof ViewModelStoreOwner) { + return ((ViewModelStoreOwner) fragment).getViewModelStore(); + } return holderFragmentFor(fragment).getViewModelStore(); } } diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java index 38b7cc04..b2e389ff 100644 --- a/android/arch/paging/ContiguousDataSource.java +++ b/android/arch/paging/ContiguousDataSource.java @@ -27,15 +27,27 @@ abstract class ContiguousDataSource extends DataSource { return true; } - abstract void loadInitial(@Nullable Key key, int initialLoadSize, - int pageSize, boolean enablePlaceholders, - @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver); - - abstract void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, - @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver); - - abstract void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize, - @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver); + abstract void dispatchLoadInitial( + @Nullable Key key, + int initialLoadSize, + int pageSize, + boolean enablePlaceholders, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); + + abstract void dispatchLoadAfter( + int currentEndIndex, + @NonNull Value currentEndItem, + int pageSize, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); + + abstract void dispatchLoadBefore( + int currentBeginIndex, + @NonNull Value currentBeginItem, + int pageSize, + @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver); /** * Get the key from either the position, or item, or null if position/item invalid. diff --git a/android/arch/paging/ContiguousPagedList.java b/android/arch/paging/ContiguousPagedList.java index a134e440..42eb320d 100644 --- a/android/arch/paging/ContiguousPagedList.java +++ b/android/arch/paging/ContiguousPagedList.java @@ -87,7 +87,7 @@ class ContiguousPagedList extends PagedList implements PagedStorage.Cal if (mDataSource.isInvalid()) { detach(); } else { - mDataSource.loadInitial(key, + mDataSource.dispatchLoadInitial(key, mConfig.initialLoadSizeHint, mConfig.pageSize, mConfig.enablePlaceholders, @@ -184,7 +184,7 @@ class ContiguousPagedList extends PagedList implements PagedStorage.Cal if (mDataSource.isInvalid()) { detach(); } else { - mDataSource.loadBefore(position, item, mConfig.pageSize, + mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize, mMainThreadExecutor, mReceiver); } @@ -213,7 +213,7 @@ class ContiguousPagedList extends PagedList implements PagedStorage.Cal if (mDataSource.isInvalid()) { detach(); } else { - mDataSource.loadAfter(position, item, mConfig.pageSize, + mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize, mMainThreadExecutor, mReceiver); } } diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java index b82d4e6d..bbf7ccb3 100644 --- a/android/arch/paging/DataSource.java +++ b/android/arch/paging/DataSource.java @@ -30,11 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean; * Base class for loading pages of snapshot data into a {@link PagedList}. *

    * 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. - *

    - * A PagedList / DataSource pair serve as a snapshot of the data set being loaded. If the - * underlying data set is modified, a new PagedList / DataSource pair must be created to represent - * the new data. + * it loads more data, but the data loaded cannot be updated. If the underlying data set is + * modified, a new PagedList / DataSource pair must be created to represent the new data. *

    Loading Pages

    * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter} * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView. @@ -68,18 +65,23 @@ import java.util.concurrent.atomic.AtomicBoolean; * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the * snapshot can be created. *

    Implementing a DataSource

    - * To implement, extend either the {@link KeyedDataSource}, or {@link PositionalDataSource} - * subclass. Choose based on whether each load operation is based on the position of the data in the - * list. + * To implement, extend one of the subclasses: {@link PageKeyedDataSource}, + * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}. + *

    + * 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. *

    - * Use {@link KeyedDataSource} if you need to use data from item {@code N-1} to load item + * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item * {@code N}. For example, if requesting the backend for the next comments in the list * requires the ID or timestamp of the most recent loaded comment, or if querying the next users * from a name-sorted database query requires the name and unique ID of the previous. *

    - * Use {@link PositionalDataSource} if you can load arbitrary pages based solely on position - * information, and can provide a fixed item count. PositionalDataSource supports querying pages at - * arbitrary positions, so can provide data to PagedLists in arbitrary order. + * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary + * positions, and provide a fixed item count. PositionalDataSource supports querying pages at + * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that + * PositionalDataSource is required to respect page size for efficient tiling. If you want to + * override page size (e.g. when network page size constraints are only known at runtime), use one + * of the other DataSource classes. *

    * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not * return {@code null} items in lists that it loads. This is so that users of the PagedList @@ -115,8 +117,13 @@ public abstract class DataSource { /** * Create a DataSource. *

    - * The DataSource should invalidate itself if the snapshot is no longer valid, and a new - * DataSource should be queried from the Factory. + * The DataSource should invalidate itself if the snapshot is no longer valid. If a + * DataSource becomes invalid, the only way to query more data is to create a new DataSource + * from the Factory. + *

    + * {@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} to observers. * * @return the new DataSource. */ @@ -159,11 +166,11 @@ public abstract class DataSource { private Executor mPostExecutor = null; private boolean mHasSignalled = false; - BaseLoadCallback(@PageResult.ResultType int resultType, @NonNull DataSource dataSource, + BaseLoadCallback(@NonNull DataSource dataSource, @PageResult.ResultType int resultType, @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { + mDataSource = dataSource; mResultType = resultType; mPostExecutor = mainThreadExecutor; - mDataSource = dataSource; mReceiver = receiver; } @@ -173,20 +180,30 @@ public abstract class DataSource { } } + /** + * 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.getInvalidResult()); + return true; + } + return false; + } + void dispatchResultToReceiver(final @NonNull PageResult result) { Executor executor; synchronized (mSignalLock) { if (mHasSignalled) { throw new IllegalStateException( - "LoadCallback already dispatched, cannot dispatch again."); + "callback.onResult already called, cannot call again."); } mHasSignalled = true; executor = mPostExecutor; } - final PageResult resolvedResult = - mDataSource.isInvalid() ? PageResult.getInvalidResult() : result; - if (executor != null) { executor.execute(new Runnable() { @Override diff --git a/android/arch/paging/ItemKeyedDataSource.java b/android/arch/paging/ItemKeyedDataSource.java new file mode 100644 index 00000000..cb8247bd --- /dev/null +++ b/android/arch/paging/ItemKeyedDataSource.java @@ -0,0 +1,337 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.paging; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Incremental data loader for paging keyed content, where loaded content uses previously loaded + * items as input to future loads. + *

    + * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1} + * to load item {@code N}. This is common, for example, in sorted database queries where + * attributes of the item such just before the next query define how to execute it. + *

    + * The {@code InMemoryByItemRepository} in the + * PagingWithNetworkSample + * shows how to implement a network ItemKeyedDataSource using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. + * + * @param Type of data used to query Value types out of the DataSource. + * @param Type of items being loaded by the DataSource. + */ +public abstract class ItemKeyedDataSource extends ContiguousDataSource { + + /** + * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. + * + * @param Type of data used to query Value types out of the DataSource. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadInitialParams { + /** + * Load items around this key, or at the beginning of the data set if {@code null} is + * passed. + *

    + * Note that this key is generally a hint, and may be ignored if you want to always load + * from the beginning. + */ + @Nullable + public final Key requestedInitialKey; + + /** + * Requested number of items to load. + *

    + * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored. + */ + public final boolean placeholdersEnabled; + + + LoadInitialParams(@Nullable Key requestedInitialKey, int requestedLoadSize, + boolean placeholdersEnabled) { + this.requestedInitialKey = requestedInitialKey; + this.requestedLoadSize = requestedLoadSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} + * and {@link #loadAfter(LoadParams, LoadCallback)}. + * + * @param Type of data used to query Value types out of the DataSource. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadParams { + /** + * Load items before/after this key. + *

    + * Returned data must begin directly adjacent to this position. + */ + public final Key key; + /** + * Requested number of items to load. + *

    + * Returned page can be of this size, but it may be altered if that is easier, e.g. a + * network data source where the backend defines page size. + */ + public final int requestedLoadSize; + + LoadParams(Key key, int requestedLoadSize) { + this.key = key; + this.requestedLoadSize = requestedLoadSize; + } + } + + /** + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data and, optionally, position/count information. + *

    + * A callback can be called only once, and will throw if called again. + *

    + * If you can compute the number of items in the data set before and after the loaded range, + * call the three parameter {@link #onResult(List, int, int)} to pass that information. You + * can skip passing this information by calling the single parameter {@link #onResult(List)}, + * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is + * {@code false}, so the positioning information will be ignored. + *

    + * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public static class LoadInitialCallback extends LoadCallback { + private final boolean mCountingEnabled; + LoadInitialCallback(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled, + @NonNull PageResult.Receiver receiver) { + super(dataSource, PageResult.INIT, null, receiver); + mCountingEnabled = countingEnabled; + } + + /** + * Called to pass initial load state from a DataSource. + *

    + * Call this method from your DataSource's {@code loadInitial} function to return data, + * and inform how many placeholders should be shown before and after. If counting is cheap + * to compute (for example, if a network load returns the information regardless), it's + * recommended to pass data back through this method. + *

    + * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be loaded from this DataSource, + * pass {@code N}. + * @param totalCount Total number of items that may be returned from this DataSource. + * Includes the number in the initial {@code data} parameter + * as well as any items that can be loaded in front or behind of + * {@code data}. + */ + public void onResult(@NonNull List data, int position, int totalCount) { + if (!dispatchInvalidResultIfInvalid()) { + validateInitialLoadParams(data, position, totalCount); + + int trailingUnloadedCount = totalCount - position - data.size(); + if (mCountingEnabled) { + dispatchResultToReceiver(new PageResult<>( + data, position, trailingUnloadedCount, 0)); + } else { + dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + } + + /** + * Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} + * and {@link #loadAfter(LoadParams, LoadCallback)} to return data. + *

    + * A callback can be called only once, and will throw if called again. + *

    + * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of items being loaded. + */ + public static class LoadCallback extends BaseLoadCallback { + LoadCallback(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type, + @Nullable Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + super(dataSource, type, mainThreadExecutor, receiver); + } + + /** + * Called to pass loaded data from a DataSource. + *

    + * Call this method from your ItemKeyedDataSource's + * {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data. + *

    + * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to + * initialize without counting available data, or supporting placeholders. + *

    + * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the ItemKeyedDataSource. + */ + public void onResult(@NonNull List data) { + if (!dispatchInvalidResultIfInvalid()) { + dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + @Nullable + @Override + final Key getKey(int position, Value item) { + if (item == null) { + return null; + } + + return getKey(item); + } + + @Override + final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize, + boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + LoadInitialCallback callback = + new LoadInitialCallback<>(this, enablePlaceholders, receiver); + loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback); + + // If initialLoad's callback is not called within the body, we force any following calls + // to post to the UI thread. This constructor may be run on a background thread, but + // after constructor, mutation must happen on UI thread. + callback.setPostExecutor(mainThreadExecutor); + } + + @Override + final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize), + new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver)); + } + + @Override + final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize), + new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver)); + } + + /** + * Load initial data. + *

    + * This method is called first to initialize a PagedList with data. If it's possible to count + * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to + * the callback via the three-parameter + * {@link LoadInitialCallback#onResult(List, int, int)}. This enables PagedLists + * presenting data from this source to display placeholders to represent unloaded items. + *

    + * {@link LoadInitialParams#requestedInitialKey} and {@link LoadInitialParams#requestedLoadSize} + * are hints, not requirements, so they may be altered or ignored. Note that ignoring the + * {@code requestedInitialKey} can prevent subsequent PagedList/DataSource pairs from + * initializing at the same location. If your data source never invalidates (for example, + * loading from the network without the network ever signalling that old data must be reloaded), + * it's fine to ignore the {@code initialLoadKey} and always start from the beginning of the + * data set. + * + * @param params Parameters for initial load, including initial key and requested size. + * @param callback Callback that receives initial load data. + */ + public abstract void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); + + /** + * Load list data after the key specified in {@link LoadParams#key LoadParams.key}. + *

    + * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

    + * Data may be passed synchronously during the loadAfter method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

    + * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key to load after, and requested size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadAfter(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Load list data before the key specified in {@link LoadParams#key LoadParams.key}. + *

    + * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

    + *

    Note: Data returned will be prepended just before the key + * passed, so if you vary size, ensure that the last item is adjacent to the passed key. + *

    + * Data may be passed synchronously during the loadBefore method, or deferred and called at a + * later time. Further loads going up will be blocked until the callback is called. + *

    + * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key to load before, and requested size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadBefore(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Return a key associated with the given item. + *

    + * If your ItemKeyedDataSource is loading from a source that is sorted and loaded by a unique + * integer ID, you would return {@code item.getID()} here. This key can then be passed to + * {@link #loadBefore(LoadParams, LoadCallback)} or + * {@link #loadAfter(LoadParams, LoadCallback)} to load additional items adjacent to the item + * passed to this function. + *

    + * If your key is more complex, such as when you're sorting by name, then resolving collisions + * with integer ID, you'll need to return both. In such a case you would use a wrapper class, + * such as {@code Pair} or, in Kotlin, + * {@code data class Key(val name: String, val id: Int)} + * + * @param item Item to get the key from. + * @return Key associated with given item. + */ + @NonNull + public abstract Key getKey(@NonNull Value item); +} diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java deleted file mode 100644 index 4f62692b..00000000 --- a/android/arch/paging/KeyedDataSource.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.List; -import java.util.concurrent.Executor; - -/** - * Incremental data loader for paging keyed content, where loaded content uses previously loaded - * items as input to future loads. - *

    - * Implement a DataSource using KeyedDataSource if you need to use data from item {@code N - 1} - * to load item {@code N}. This is common, for example, in sorted database queries where - * attributes of the item such just before the next query define how to execute it. - * - * @param Type of data used to query Value types out of the DataSource. - * @param Type of items being loaded by the DataSource. - */ -public abstract class KeyedDataSource extends ContiguousDataSource { - - /** - * Callback for KeyedDataSource initial loading methods to return data and (optionally) - * position/count information. - *

    - * A callback can be called only once, and will throw if called again. - *

    - * It is always valid for a DataSource loading method that takes a callback to stash the - * callback and call it later. This enables DataSources to be fully asynchronous, and to handle - * temporary, recoverable error states (such as a network error that can be retried). - * - * @param Type of items being loaded. - */ - public static class InitialLoadCallback extends LoadCallback { - private final boolean mCountingEnabled; - InitialLoadCallback(@NonNull KeyedDataSource dataSource, boolean countingEnabled, - @NonNull PageResult.Receiver receiver) { - super(dataSource, PageResult.INIT, null, receiver); - mCountingEnabled = countingEnabled; - } - - /** - * Called to pass initial load state from a DataSource. - *

    - * Call this method from your DataSource's {@code loadInitial} function to return data, - * and inform how many placeholders should be shown before and after. If counting is cheap - * to compute (for example, if a network load returns the information regardless), it's - * recommended to pass data back through this method. - *

    - * It is always valid to pass a different amount of data than what is requested. Pass an - * empty list if there is no more data to load. - * - * @param data List of items loaded from the DataSource. If this is empty, the DataSource - * is treated as empty, and no further loads will occur. - * @param position Position of the item at the front of the list. If there are {@code N} - * items before the items in data that can be loaded from this DataSource, - * pass {@code N}. - * @param totalCount Total number of items that may be returned from this DataSource. - * Includes the number in the initial {@code data} parameter - * as well as any items that can be loaded in front or behind of - * {@code data}. - */ - public void onResult(@NonNull List data, int position, int totalCount) { - validateInitialLoadParams(data, position, totalCount); - - int trailingUnloadedCount = totalCount - position - data.size(); - if (mCountingEnabled) { - dispatchResultToReceiver(new PageResult<>( - data, position, trailingUnloadedCount, 0)); - } else { - dispatchResultToReceiver(new PageResult<>(data, position)); - } - } - } - - /** - * Callback for KeyedDataSource {@link #loadBefore(Object, int, LoadCallback)} - * and {@link #loadAfter(Object, int, LoadCallback)} methods to return data. - *

    - * A callback can be called only once, and will throw if called again. - *

    - * It is always valid for a DataSource loading method that takes a callback to stash the - * callback and call it later. This enables DataSources to be fully asynchronous, and to handle - * temporary, recoverable error states (such as a network error that can be retried). - * - * @param Type of items being loaded. - */ - public static class LoadCallback extends BaseLoadCallback { - LoadCallback(@NonNull KeyedDataSource dataSource, @PageResult.ResultType int type, - @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { - super(type, dataSource, mainThreadExecutor, receiver); - } - - /** - * Called to pass loaded data from a DataSource. - *

    - * Call this method from your KeyedDataSource's - * {@link #loadBefore(Object, int, LoadCallback)} and - * {@link #loadAfter(Object, int, LoadCallback)} methods to return data. - *

    - * Call this from {@link #loadInitial(Object, int, boolean, InitialLoadCallback)} to - * initialize without counting available data, or supporting placeholders. - *

    - * It is always valid to pass a different amount of data than what is requested. Pass an - * empty list if there is no more data to load. - * - * @param data List of items loaded from the KeyedDataSource. - */ - public void onResult(@NonNull List data) { - dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); - } - } - - @Nullable - @Override - final Key getKey(int position, Value item) { - if (item == null) { - return null; - } - - return getKey(item); - } - - @Override - public void loadInitial(@Nullable Key key, int initialLoadSize, int pageSize, - boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, - @NonNull PageResult.Receiver receiver) { - InitialLoadCallback callback = - new InitialLoadCallback<>(this, enablePlaceholders, receiver); - loadInitial(key, initialLoadSize, enablePlaceholders, callback); - - // If initialLoad's callback is not called within the body, we force any following calls - // to post to the UI thread. This constructor may be run on a background thread, but - // after constructor, mutation must happen on UI thread. - callback.setPostExecutor(mainThreadExecutor); - } - - @Override - void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, - @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { - loadAfter(getKey(currentEndItem), pageSize, - new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver)); - } - - @Override - void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize, - @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { - loadBefore(getKey(currentBeginItem), pageSize, - new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver)); - } - - /** - * Load initial data. - *

    - * This method is called first to initialize a PagedList with data. If it's possible to count - * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to - * the callback via the three-parameter - * {@link InitialLoadCallback#onResult(List, int, int)}. This enables PagedLists - * presenting data from this source to display placeholders to represent unloaded items. - *

    - * {@code initialLoadKey} and {@code requestedLoadSize} are hints, not requirements, so if it is - * difficult or impossible to respect them, they may be altered. Note that ignoring the - * {@code initialLoadKey} can prevent subsequent PagedList/DataSource pairs from initializing at - * the same location. If your data source never invalidates (for example, loading from the - * network without the network ever signalling that old data must be reloaded), it's fine to - * ignore the {@code initialLoadKey} and always start from the beginning of the data set. - * - * @param initialLoadKey Load items around this key, or at the beginning of the data set if null - * is passed. - * @param requestedLoadSize Suggested number of items to load. - * @param enablePlaceholders Signals whether counting is requested. If false, you can - * potentially save work by calling the single-parameter variant of - * {@link LoadCallback#onResult(List)} and not counting the - * number of items in the data set. - * @param callback DataSource.LoadCallback that receives initial load data. - */ - public abstract void loadInitial(@Nullable Key initialLoadKey, int requestedLoadSize, - boolean enablePlaceholders, @NonNull InitialLoadCallback callback); - - /** - * Load list data after the specified item. - *

    - * It's valid to return a different list size than the page size, if it's easier for this data - * source. It is generally safer to increase the number loaded than reduce. - *

    - * Data may be passed synchronously during the loadAfter method, or deferred and called at a - * later time. Further loads going down will be blocked until the callback is called. - *

    - * If data cannot be loaded (for example, if the request is invalid, or the data would be stale - * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, - * and prevent further loading. - * - * @param currentEndKey Load items after this key. May be null on initial load, to indicate load - * from beginning. - * @param pageSize Suggested number of items to load. - * @param callback DataSource.LoadCallback that receives loaded data. - */ - public abstract void loadAfter(@NonNull Key currentEndKey, int pageSize, - @NonNull LoadCallback callback); - - /** - * Load data before the currently loaded content. - *

    - * It's valid to return a different list size than the page size, if it's easier for this data - * source. It is generally safer to increase the number loaded than reduce. Note that the last - * item returned must be directly adjacent to the key passed, so varying size from the pageSize - * requested should effectively grow or shrink the list by modifying the beginning, not the end. - *

    - * Data may be passed synchronously during the loadBefore method, or deferred and called at a - * later time. Further loads going up will be blocked until the callback is called. - *

    - * If data cannot be loaded (for example, if the request is invalid, or the data would be stale - * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, - * and prevent further loading. - *

    Note: Data must be returned in the order it will be - * presented in the list. - * - * @param currentBeginKey Load items before this key. - * @param pageSize Suggested number of items to load. - * @param callback DataSource.LoadCallback that receives loaded data. - */ - public abstract void loadBefore(@NonNull Key currentBeginKey, int pageSize, - @NonNull LoadCallback callback); - - /** - * Return a key associated with the given item. - *

    - * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique - * integer ID, you would return {@code item.getID()} here. This key can then be passed to - * {@link #loadBefore(Object, int, LoadCallback)} or - * {@link #loadAfter(Object, int, LoadCallback)} to load additional items adjacent to the item - * passed to this function. - *

    - * If your key is more complex, such as when you're sorting by name, then resolving collisions - * with integer ID, you'll need to return both. In such a case you would use a wrapper class, - * such as {@code Pair} or, in Kotlin, - * {@code data class Key(val name: String, val id: Int)} - * - * @param item Item to get the key from. - * @return Key associated with given item. - */ - @NonNull - public abstract Key getKey(@NonNull Value item); -} diff --git a/android/arch/paging/ListDataSource.java b/android/arch/paging/ListDataSource.java index b6f366a3..1482a91f 100644 --- a/android/arch/paging/ListDataSource.java +++ b/android/arch/paging/ListDataSource.java @@ -29,22 +29,23 @@ class ListDataSource extends PositionalDataSource { } @Override - public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize, - @NonNull InitialLoadCallback callback) { + public void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback) { final int totalCount = mList.size(); - final int firstLoadPosition = computeFirstLoadPosition( - requestedStartPosition, requestedLoadSize, pageSize, totalCount); - final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize); + final int position = computeInitialLoadPosition(params, totalCount); + final int loadSize = computeInitialLoadSize(params, position, totalCount); // for simplicity, we could return everything immediately, // but we tile here since it's expected behavior - List sublist = mList.subList(firstLoadPosition, firstLoadPosition + firstLoadSize); - callback.onResult(sublist, firstLoadPosition, totalCount); + List sublist = mList.subList(position, position + loadSize); + callback.onResult(sublist, position, totalCount); } @Override - public void loadRange(int startPosition, int count, @NonNull LoadCallback callback) { - callback.onResult(mList.subList(startPosition, startPosition + count)); + public void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback) { + callback.onResult(mList.subList(params.startPosition, + params.startPosition + params.loadSize)); } } diff --git a/android/arch/paging/LivePagedListBuilder.java b/android/arch/paging/LivePagedListBuilder.java index b0fddba2..f2d09cc7 100644 --- a/android/arch/paging/LivePagedListBuilder.java +++ b/android/arch/paging/LivePagedListBuilder.java @@ -88,9 +88,21 @@ public class LivePagedListBuilder { } /** - * Sets a {@link PagedList.BoundaryCallback} on each PagedList created. + * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load + * additional data from network when paging from local storage. *

    - * This can be used to + * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this + * method is not called, or {@code null} is passed, you will not be notified when each + * DataSource runs out of data to provide to its PagedList. + *

    + * If you are paging from a DataSource.Factory backed by local storage, you can set a + * BoundaryCallback to know when there is no more information to page from local storage. + * This is useful to page from the network when local storage is a cache of network data. + *

    + * Note that when using a BoundaryCallback with a {@code LiveData}, method calls + * on the callback may be dispatched multiple times - one for each PagedList/DataSource + * pair. If loading network data from a BoundaryCallback, you should prevent multiple + * dispatches of the same method from triggering multiple simultaneous network loads. * * @param boundaryCallback The boundary callback for listening to PagedList load state. * @return this @@ -106,6 +118,8 @@ public class LivePagedListBuilder { /** * Sets executor which will be used for background loading of pages. *

    + * If not set, defaults to the Arch components I/O thread. + *

    * Does not affect initial load, which will be always be done on done on the Arch components * I/O thread. * diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java index b7c68dd6..44b71a82 100644 --- a/android/arch/paging/LivePagedListProvider.java +++ b/android/arch/paging/LivePagedListProvider.java @@ -16,5 +16,91 @@ package android.arch.paging; -abstract public class LivePagedListProvider { -} \ No newline at end of file +import android.arch.lifecycle.LiveData; +import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +// NOTE: Room 1.0 depends on this class, so it should not be removed +// until Room switches to using DataSource.Factory directly +/** + * Provides a {@code LiveData}, given a means to construct a DataSource. + *

    + * Return type for data-loading system of an application or library to produce a + * {@code LiveData}, while leaving the details of the paging mechanism up to the + * consumer. + * + * @param Type of input valued used to load data from the DataSource. Must be integer if + * you're using PositionalDataSource. + * @param Data type produced by the DataSource, and held by the PagedLists. + * + * @see PagedListAdapter + * @see DataSource + * @see PagedList + * + * @deprecated use {@link LivePagedListBuilder} to construct a {@code LiveData}. It + * provides the same construction capability with more customization, and simpler defaults. The role + * of DataSource construction has been separated out to {@link DataSource.Factory} to access or + * provide a self-invalidating sequence of DataSources. If you were acquiring this from Room, you + * can switch to having your Dao return a {@link DataSource.Factory} instead, and create a + * {@code LiveData} with a {@link LivePagedListBuilder}. + */ +@Deprecated +public abstract class LivePagedListProvider implements DataSource.Factory { + + @Override + public DataSource create() { + return createDataSource(); + } + + /** + * Construct a new data source to be wrapped in a new PagedList, which will be returned + * through the LiveData. + * + * @return The data source. + */ + @WorkerThread + protected abstract DataSource createDataSource(); + + /** + * Creates a LiveData of PagedLists, given the page size. + *

    + * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a + * {@link android.support.v7.widget.RecyclerView}. + * + * @param initialLoadKey Initial key used to load initial data from the data source. + * @param pageSize Page size defining how many items are loaded from a data source at a time. + * Recommended to be multiple times the size of item displayed at once. + * + * @return The LiveData of PagedLists. + */ + @AnyThread + @NonNull + public LiveData> create(@Nullable Key initialLoadKey, int pageSize) { + return new LivePagedListBuilder<>(this, pageSize) + .setInitialLoadKey(initialLoadKey) + .build(); + } + + /** + * Creates a LiveData of PagedLists, given the PagedList.Config. + *

    + * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a + * {@link android.support.v7.widget.RecyclerView}. + * + * @param initialLoadKey Initial key to pass to the data source to initialize data with. + * @param config PagedList.Config to use with created PagedLists. This specifies how the + * lists will load data. + * + * @return The LiveData of PagedLists. + */ + @AnyThread + @NonNull + public LiveData> create(@Nullable Key initialLoadKey, + @NonNull PagedList.Config config) { + return new LivePagedListBuilder<>(this, config) + .setInitialLoadKey(initialLoadKey) + .build(); + } +} diff --git a/android/arch/paging/PageKeyedDataSource.java b/android/arch/paging/PageKeyedDataSource.java new file mode 100644 index 00000000..a10eceeb --- /dev/null +++ b/android/arch/paging/PageKeyedDataSource.java @@ -0,0 +1,391 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.paging; + +import android.support.annotation.GuardedBy; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Incremental data loader for page-keyed content, where requests return keys for next/previous + * pages. + *

    + * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1} + * to load page {@code N}. This is common, for example, in network APIs that include a next/previous + * link or key with each page load. + *

    + * The {@code InMemoryByPageRepository} in the + * PagingWithNetworkSample + * shows how to implement a network PageKeyedDataSource using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. + * + * @param Type of data used to query Value types out of the DataSource. + * @param Type of items being loaded by the DataSource. + */ +public abstract class PageKeyedDataSource extends ContiguousDataSource { + private final Object mKeyLock = new Object(); + + @Nullable + @GuardedBy("mKeyLock") + private Key mNextKey = null; + + @Nullable + @GuardedBy("mKeyLock") + private Key mPreviousKey = null; + + private void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) { + synchronized (mKeyLock) { + mPreviousKey = previousKey; + mNextKey = nextKey; + } + } + + private void setPreviousKey(@Nullable Key previousKey) { + synchronized (mKeyLock) { + mPreviousKey = previousKey; + } + } + + private void setNextKey(@Nullable Key nextKey) { + synchronized (mKeyLock) { + mNextKey = nextKey; + } + } + + private @Nullable Key getPreviousKey() { + synchronized (mKeyLock) { + return mPreviousKey; + } + } + + private @Nullable Key getNextKey() { + synchronized (mKeyLock) { + return mNextKey; + } + } + + /** + * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. + * + * @param Type of data used to query pages. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadInitialParams { + /** + * Requested number of items to load. + *

    + * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored. + */ + public final boolean placeholdersEnabled; + + + LoadInitialParams(int requestedLoadSize, + boolean placeholdersEnabled) { + this.requestedLoadSize = requestedLoadSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)}. + * + * @param Type of data used to query pages. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadParams { + /** + * Load items before/after this key. + *

    + * Returned data must begin directly adjacent to this position. + */ + public final Key key; + + /** + * Requested number of items to load. + *

    + * Returned page can be of this size, but it may be altered if that is easier, e.g. a + * network data source where the backend defines page size. + */ + public final int requestedLoadSize; + + LoadParams(Key key, int requestedLoadSize) { + this.key = key; + this.requestedLoadSize = requestedLoadSize; + } + } + + /** + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data and, optionally, position/count information. + *

    + * A callback can be called only once, and will throw if called again. + *

    + * If you can compute the number of items in the data set before and after the loaded range, + * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that + * information. You can skip passing this information by calling the three parameter + * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if + * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning + * information will be ignored. + *

    + * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of data used to query pages. + * @param Type of items being loaded. + */ + public static class LoadInitialCallback extends BaseLoadCallback { + private final PageKeyedDataSource mDataSource; + private final boolean mCountingEnabled; + LoadInitialCallback(@NonNull PageKeyedDataSource dataSource, + boolean countingEnabled, @NonNull PageResult.Receiver receiver) { + super(dataSource, PageResult.INIT, null, receiver); + mDataSource = dataSource; + mCountingEnabled = countingEnabled; + } + + /** + * Called to pass initial load state from a DataSource. + *

    + * Call this method from your DataSource's {@code loadInitial} function to return data, + * and inform how many placeholders should be shown before and after. If counting is cheap + * to compute (for example, if a network load returns the information regardless), it's + * recommended to pass data back through this method. + *

    + * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the DataSource. If this is empty, the DataSource + * is treated as empty, and no further loads will occur. + * @param position Position of the item at the front of the list. If there are {@code N} + * items before the items in data that can be loaded from this DataSource, + * pass {@code N}. + * @param totalCount Total number of items that may be returned from this DataSource. + * Includes the number in the initial {@code data} parameter + * as well as any items that can be loaded in front or behind of + * {@code data}. + */ + public void onResult(@NonNull List data, int position, int totalCount, + @Nullable Key previousPageKey, @Nullable Key nextPageKey) { + if (!dispatchInvalidResultIfInvalid()) { + validateInitialLoadParams(data, position, totalCount); + + // setup keys before dispatching data, so guaranteed to be ready + mDataSource.initKeys(previousPageKey, nextPageKey); + + int trailingUnloadedCount = totalCount - position - data.size(); + if (mCountingEnabled) { + dispatchResultToReceiver(new PageResult<>( + data, position, trailingUnloadedCount, 0)); + } else { + dispatchResultToReceiver(new PageResult<>(data, position)); + } + } + } + + /** + * Called to pass loaded data from a DataSource. + *

    + * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to + * initialize without counting available data, or supporting placeholders. + *

    + * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + * + * @param data List of items loaded from the PageKeyedDataSource. + * @param previousPageKey Key for page before the initial load result, or {@code null} if no + * more data can be loaded before. + * @param nextPageKey Key for page after the initial load result, or {@code null} if no + * more data can be loaded after. + */ + public void onResult(@NonNull List data, @Nullable Key previousPageKey, + @Nullable Key nextPageKey) { + if (!dispatchInvalidResultIfInvalid()) { + mDataSource.initKeys(previousPageKey, nextPageKey); + dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + /** + * Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} to return data. + *

    + * A callback can be called only once, and will throw if called again. + *

    + * It is always valid for a DataSource loading method that takes a callback to stash the + * callback and call it later. This enables DataSources to be fully asynchronous, and to handle + * temporary, recoverable error states (such as a network error that can be retried). + * + * @param Type of data used to query pages. + * @param Type of items being loaded. + */ + public static class LoadCallback extends BaseLoadCallback { + private final PageKeyedDataSource mDataSource; + LoadCallback(@NonNull PageKeyedDataSource dataSource, + @PageResult.ResultType int type, @Nullable Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + super(dataSource, type, mainThreadExecutor, receiver); + mDataSource = dataSource; + } + + /** + * Called to pass loaded data from a DataSource. + *

    + * Call this method from your PageKeyedDataSource's + * {@link #loadBefore(LoadParams, LoadCallback)} and + * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data. + *

    + * It is always valid to pass a different amount of data than what is requested. Pass an + * empty list if there is no more data to load. + *

    + * Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've + * loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the + * previous page, or {@code null} if the loaded page is the first. If in + * {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or + * {@code null} if the loaded page is the last. + * + * @param data List of items loaded from the PageKeyedDataSource. + * @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore} + * / next page in {@link #loadAfter}), or {@code null} if there are + * no more pages to load in the current load direction. + */ + public void onResult(@NonNull List data, @Nullable Key adjacentPageKey) { + if (!dispatchInvalidResultIfInvalid()) { + if (mResultType == PageResult.APPEND) { + mDataSource.setNextKey(adjacentPageKey); + } else { + mDataSource.setPreviousKey(adjacentPageKey); + } + dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0)); + } + } + } + + @Nullable + @Override + final Key getKey(int position, Value item) { + // don't attempt to persist keys, since we currently don't pass them to initial load + return null; + } + + @Override + final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize, + boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + LoadInitialCallback callback = + new LoadInitialCallback<>(this, enablePlaceholders, receiver); + loadInitial(new LoadInitialParams(initialLoadSize, enablePlaceholders), callback); + + // If initialLoad's callback is not called within the body, we force any following calls + // to post to the UI thread. This constructor may be run on a background thread, but + // after constructor, mutation must happen on UI thread. + callback.setPostExecutor(mainThreadExecutor); + } + + + @Override + final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + @Nullable Key key = getNextKey(); + if (key != null) { + loadAfter(new LoadParams<>(key, pageSize), + new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver)); + } + } + + @Override + final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, + @NonNull PageResult.Receiver receiver) { + @Nullable Key key = getPreviousKey(); + if (key != null) { + loadBefore(new LoadParams<>(key, pageSize), + new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver)); + } + } + + /** + * Load initial data. + *

    + * This method is called first to initialize a PagedList with data. If it's possible to count + * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to + * the callback via the three-parameter + * {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists + * presenting data from this source to display placeholders to represent unloaded items. + *

    + * {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be + * altered or ignored. + * + * @param params Parameters for initial load, including requested load size. + * @param callback Callback that receives initial load data. + */ + public abstract void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); + + /** + * Prepend page with the key specified by {@link LoadParams#key LoadParams.key}. + *

    + * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

    + * Data may be passed synchronously during the load method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

    + * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key for the new page, and requested load + * size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadBefore(@NonNull LoadParams params, + @NonNull LoadCallback callback); + + /** + * Append page with the key specified by {@link LoadParams#key LoadParams.key}. + *

    + * It's valid to return a different list size than the page size if it's easier, e.g. if your + * backend defines page sizes. It is generally safer to increase the number loaded than reduce. + *

    + * Data may be passed synchronously during the load method, or deferred and called at a + * later time. Further loads going down will be blocked until the callback is called. + *

    + * If data cannot be loaded (for example, if the request is invalid, or the data would be stale + * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source, + * and prevent further loading. + * + * @param params Parameters for the load, including the key for the new page, and requested load + * size. + * @param callback Callback that receives loaded data. + */ + public abstract void loadAfter(@NonNull LoadParams params, + @NonNull LoadCallback callback); +} diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java index 4e17a151..c6de5c52 100644 --- a/android/arch/paging/PagedList.java +++ b/android/arch/paging/PagedList.java @@ -578,7 +578,8 @@ public abstract class PagedList extends AbstractList { * If data is supplied by a {@link PositionalDataSource}, the item returned from * get(i) has a position of i + getPositionOffset(). *

    - * If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0. + * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it + * doesn't use positions, returns 0. */ public int getPositionOffset() { return mStorage.getPositionOffset(); @@ -602,7 +603,8 @@ public abstract class PagedList extends AbstractList { * GC'd. * * @param previousSnapshot Snapshot previously captured from this List, or null. - * @param callback LoadCallback to dispatch to. + * @param callback Callback to dispatch to. + * * @see #removeWeakCallback(Callback) */ @SuppressWarnings("WeakerAccess") @@ -637,7 +639,7 @@ public abstract class PagedList extends AbstractList { /** * Removes a previously added callback. * - * @param callback LoadCallback, previously added. + * @param callback Callback, previously added. * @see #addWeakCallback(List, Callback) */ @SuppressWarnings("WeakerAccess") @@ -680,7 +682,7 @@ public abstract class PagedList extends AbstractList { * Dispatch updates since the non-empty snapshot was taken. * * @param snapshot Non-empty snapshot. - * @param callback LoadCallback for updates that have occurred since snapshot. + * @param callback Callback for updates that have occurred since snapshot. */ abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList snapshot, @NonNull Callback callback); @@ -857,12 +859,14 @@ public abstract class PagedList extends AbstractList { } /** - * Defines how many items to load when first load occurs, if you are using a - * {@link KeyedDataSource}. + * Defines how many items to load when first load occurs. *

    * This value is typically larger than page size, so on first load data there's a large * enough range of content loaded to cover small scrolls. *

    + * When using a {@link PositionalDataSource}, the initial load size will be coerced to + * an integer multiple of pageSize, to enable efficient tiling. + *

    * If not set, defaults to three times page size. * * @param initialLoadSizeHint Number of items to load while initializing the PagedList. @@ -874,7 +878,6 @@ public abstract class PagedList extends AbstractList { return this; } - /** * Creates a {@link Config} with the given parameters. * @@ -905,13 +908,32 @@ public abstract class PagedList extends AbstractList { /** * Signals when a PagedList has reached the end of available data. *

    - * This can be used to implement paging from the network into a local database - when the - * database has no more data to present, a BoundaryCallback can be used to fetch more data. + * When local storage is a cache of network data, it's common to set up a streaming pipeline: + * Network data is paged into the database, database is paged into UI. Paging from the database + * to UI can be done with a {@code LiveData}, but it's still necessary to know when + * to trigger network loads. *

    - * If an instance is shared across multiple PagedLists (e.g. when passed to + * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of + * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network + * load that will write the result directly to the database. Because the database is being + * observed, the UI bound to the {@code LiveData} will update automatically to + * account for the new items. + *

    + * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should * avoid triggering it again while the load is ongoing. + *

    + * BoundaryCallback only passes the item at front or end of the list. Number of items is not + * passed, since it may not be fully computed by the DataSource if placeholders are not + * supplied. Keys are not known because the BoundaryCallback is independent of the + * DataSource-specific keys, which may be different for local vs remote storage. + *

    + * The database + network Repository in the + * PagingWithNetworkSample + * shows how to implement a network BoundaryCallback using + * Retrofit, while + * handling swipe-to-refresh, network errors, and retry. * * @param Type loaded by the PagedList. */ diff --git a/android/arch/paging/PositionalDataSource.java b/android/arch/paging/PositionalDataSource.java index d3946370..780bcf6d 100644 --- a/android/arch/paging/PositionalDataSource.java +++ b/android/arch/paging/PositionalDataSource.java @@ -25,15 +25,19 @@ import java.util.List; import java.util.concurrent.Executor; /** - * Position-based data loader for a fixed-size, countable data set, supporting loads at arbitrary - * positions. + * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at + * arbitrary page positions. *

    - * Extend PositionalDataSource if you can support counting your data set, and loading based on - * position information. + * Extend PositionalDataSource if you can load pages of a requested size at arbitrary + * positions, and provide a fixed item count. If your data source can't support loading arbitrary + * requested page sizes (e.g. when network page size constraints are only known at runtime), use + * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead. *

    * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled} - * PositionalDataSource requires counting the size of the dataset. This allows pages to be tiled in + * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}. + * If placeholders are disabled, initialize with the two parameter + * {@link LoadInitialCallback#onResult(List, int)}. *

    * Room can generate a Factory of PositionalDataSources for you: *

    @@ -46,9 +50,79 @@ import java.util.concurrent.Executor;
      * @param  Type of items being loaded by the PositionalDataSource.
      */
     public abstract class PositionalDataSource extends DataSource {
    +
    +    /**
    +     * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
    +     */
    +    @SuppressWarnings("WeakerAccess")
    +    public static class LoadInitialParams {
    +        /**
    +         * Initial load position requested.
    +         * 

    + * Note that this may not be within the bounds of your data set, it may need to be adjusted + * before you execute your load. + */ + public final int requestedStartPosition; + + /** + * Requested number of items to load. + *

    + * Note that this may be larger than available data. + */ + public final int requestedLoadSize; + + /** + * Defines page size acceptable for return values. + *

    + * List of items passed to the callback must be an integer multiple of page size. + */ + public final int pageSize; + + /** + * Defines whether placeholders are enabled, and whether the total count passed to + * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored. + */ + public final boolean placeholdersEnabled; + + LoadInitialParams( + int requestedStartPosition, + int requestedLoadSize, + int pageSize, + boolean placeholdersEnabled) { + this.requestedStartPosition = requestedStartPosition; + this.requestedLoadSize = requestedLoadSize; + this.pageSize = pageSize; + this.placeholdersEnabled = placeholdersEnabled; + } + } + + /** + * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. + */ + @SuppressWarnings("WeakerAccess") + public static class LoadRangeParams { + /** + * Start position of data to load. + *

    + * Returned data must start at this position. + */ + public final int startPosition; + /** + * Number of items to load. + *

    + * Returned data must be of this size, unless at end of the list. + */ + public final int loadSize; + + LoadRangeParams(int startPosition, int loadSize) { + this.startPosition = startPosition; + this.loadSize = loadSize; + } + } + /** - * Callback for PositionalDataSource initial loading methods to return data, position, and - * (optionally) count information. + * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} + * to return data, position, and count. *

    * A callback can be called only once, and will throw if called again. *

    @@ -58,13 +132,13 @@ public abstract class PositionalDataSource extends DataSource { * * @param Type of items being loaded. */ - public static class InitialLoadCallback extends BaseLoadCallback { + public static class LoadInitialCallback extends BaseLoadCallback { private final boolean mCountingEnabled; private final int mPageSize; - InitialLoadCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled, + LoadInitialCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled, int pageSize, PageResult.Receiver receiver) { - super(PageResult.INIT, dataSource, null, receiver); + super(dataSource, PageResult.INIT, null, receiver); mCountingEnabled = countingEnabled; mPageSize = pageSize; if (mPageSize < 1) { @@ -78,7 +152,9 @@ public abstract class PositionalDataSource extends DataSource { * Call this method from your DataSource's {@code loadInitial} function to return data, * and inform how many placeholders should be shown before and after. If counting is cheap * to compute (for example, if a network load returns the information regardless), it's - * recommended to pass data back through this method. + * recommended to pass the total size to the totalCount parameter. If placeholders are not + * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead + * call {@link #onResult(List, int)}. * * @param data List of items loaded from the DataSource. If this is empty, the DataSource * is treated as empty, and no further loads will occur. @@ -91,29 +167,34 @@ public abstract class PositionalDataSource extends DataSource { * {@code data}. */ public void onResult(@NonNull List data, int position, int totalCount) { - validateInitialLoadParams(data, position, totalCount); - if (position + data.size() != totalCount - && data.size() % mPageSize != 0) { - throw new IllegalArgumentException("PositionalDataSource requires initial load size" - + " to be a multiple of page size to support internal tiling."); - } + if (!dispatchInvalidResultIfInvalid()) { + validateInitialLoadParams(data, position, totalCount); + if (position + data.size() != totalCount + && data.size() % mPageSize != 0) { + throw new IllegalArgumentException("PositionalDataSource requires initial load" + + " size to be a multiple of page size to support internal tiling."); + } - if (mCountingEnabled) { - int trailingUnloadedCount = totalCount - position - data.size(); - dispatchResultToReceiver( - new PageResult<>(data, position, trailingUnloadedCount, 0)); - } else { - // Only occurs when wrapped as contiguous - dispatchResultToReceiver(new PageResult<>(data, position)); + if (mCountingEnabled) { + int trailingUnloadedCount = totalCount - position - data.size(); + dispatchResultToReceiver( + new PageResult<>(data, position, trailingUnloadedCount, 0)); + } else { + // Only occurs when wrapped as contiguous + dispatchResultToReceiver(new PageResult<>(data, position)); + } } } /** - * Called to pass initial load state from a DataSource without supporting placeholders. + * Called to pass initial load state from a DataSource without total count, + * when placeholders aren't requested. + *

    Note: This method can only be called when placeholders + * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false). *

    * Call this method from your DataSource's {@code loadInitial} function to return data, - * if position is known but total size is not. If counting is not expensive, consider - * calling the three parameter variant: {@link #onResult(List, int, int)}. + * if position is known but total size is not. If placeholders are requested, call the three + * parameter variant: {@link #onResult(List, int, int)}. * * @param data List of items loaded from the DataSource. If this is empty, the DataSource * is treated as empty, and no further loads will occur. @@ -121,15 +202,28 @@ public abstract class PositionalDataSource extends DataSource { * items before the items in data that can be provided by this DataSource, * pass {@code N}. */ - void onResult(@NonNull List data, int position) { - // not counting, don't need to check mAcceptCount - dispatchResultToReceiver(new PageResult<>( - data, 0, 0, position)); + @SuppressWarnings("WeakerAccess") + public void onResult(@NonNull List data, int position) { + if (!dispatchInvalidResultIfInvalid()) { + if (position < 0) { + throw new IllegalArgumentException("Position must be non-negative"); + } + if (data.isEmpty() && position != 0) { + throw new IllegalArgumentException( + "Initial result cannot be empty if items are present in data set."); + } + if (mCountingEnabled) { + throw new IllegalStateException("Placeholders requested, but totalCount not" + + " provided. Please call the three-parameter onResult method, or" + + " disable placeholders in the PagedList.Config"); + } + dispatchResultToReceiver(new PageResult<>(data, position)); + } } } /** - * Callback for PositionalDataSource {@link #loadRange(int, int, LoadCallback)} methods + * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)} * to return data. *

    * A callback can be called only once, and will throw if called again. @@ -140,33 +234,37 @@ public abstract class PositionalDataSource extends DataSource { * * @param Type of items being loaded. */ - public static class LoadCallback extends BaseLoadCallback { + public static class LoadRangeCallback extends BaseLoadCallback { private final int mPositionOffset; - LoadCallback(@NonNull PositionalDataSource dataSource, int positionOffset, + LoadRangeCallback(@NonNull PositionalDataSource dataSource, int positionOffset, Executor mainThreadExecutor, PageResult.Receiver receiver) { - super(PageResult.TILE, dataSource, mainThreadExecutor, receiver); + super(dataSource, PageResult.TILE, mainThreadExecutor, receiver); mPositionOffset = positionOffset; } /** - * Called to pass loaded data from a DataSource. - *

    - * Call this method from your DataSource's {@code load} methods to return data. + * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. * - * @param data List of items loaded from the DataSource. + * @param data List of items loaded from the DataSource. Must be same size as requested, + * unless at end of list. */ public void onResult(@NonNull List data) { - dispatchResultToReceiver(new PageResult<>( - data, 0, 0, mPositionOffset)); + if (!dispatchInvalidResultIfInvalid()) { + dispatchResultToReceiver(new PageResult<>( + data, 0, 0, mPositionOffset)); + } } } - void loadInitial(boolean acceptCount, + final void dispatchLoadInitial(boolean acceptCount, int requestedStartPosition, int requestedLoadSize, int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { - InitialLoadCallback callback = - new InitialLoadCallback<>(this, acceptCount, pageSize, receiver); - loadInitial(requestedStartPosition, requestedLoadSize, pageSize, callback); + LoadInitialCallback callback = + new LoadInitialCallback<>(this, acceptCount, pageSize, receiver); + + LoadInitialParams params = new LoadInitialParams( + requestedStartPosition, requestedLoadSize, pageSize, acceptCount); + loadInitial(params, callback); // If initialLoad's callback is not called within the body, we force any following calls // to post to the UI thread. This constructor may be run on a background thread, but @@ -174,14 +272,14 @@ public abstract class PositionalDataSource extends DataSource { callback.setPostExecutor(mainThreadExecutor); } - void loadRange(int startPosition, int count, + final void dispatchLoadRange(int startPosition, int count, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { - LoadCallback callback = - new LoadCallback<>(this, startPosition, mainThreadExecutor, receiver); + LoadRangeCallback callback = + new LoadRangeCallback<>(this, startPosition, mainThreadExecutor, receiver); if (count == 0) { callback.onResult(Collections.emptyList()); } else { - loadRange(startPosition, count, callback); + loadRange(new LoadRangeParams(startPosition, count), callback); } } @@ -192,52 +290,94 @@ public abstract class PositionalDataSource extends DataSource { *

    * Result list must be a multiple of pageSize to enable efficient tiling. * - * @param requestedStartPosition Initial load position requested. Note that this may not be - * within the bounds of your data set, it should be corrected - * before you make your query. - * @param requestedLoadSize Requested number of items to load. Note that this may be larger than - * available data. - * @param pageSize Defines page size acceptable for return values. List of items passed to the - * callback must be an integer multiple of page size. - * @param callback DataSource.InitialLoadCallback that receives initial load data, including + * @param params Parameters for initial load, including requested start position, load size, and + * page size. + * @param callback Callback that receives initial load data, including * position and total data set size. */ @WorkerThread - public abstract void loadInitial(int requestedStartPosition, int requestedLoadSize, - int pageSize, @NonNull InitialLoadCallback callback); + public abstract void loadInitial( + @NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback); /** * Called to load a range of data from the DataSource. *

    * This method is called to load additional pages from the DataSource after the - * InitialLoadCallback passed to loadInitial has initialized a PagedList. + * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList. *

    - * Unlike {@link #loadInitial(int, int, int, InitialLoadCallback)}, this method must return the - * number of items requested, at the position requested. + * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return + * the number of items requested, at the position requested. * - * @param startPosition Initial load position. - * @param count Number of items to load. - * @param callback DataSource.LoadCallback that receives loaded data. + * @param params Parameters for load, including start position and load size. + * @param callback Callback that receives loaded data. */ @WorkerThread - public abstract void loadRange(int startPosition, int count, @NonNull LoadCallback callback); + public abstract void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback); @Override boolean isContiguous() { return false; } - @NonNull ContiguousDataSource wrapAsContiguousWithoutPlaceholders() { return new ContiguousWithoutPlaceholdersWrapper<>(this); } - static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) { + /** + * Helper for computing an initial position in + * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be + * computed ahead of loading. + *

    + * The value computed by this function will do bounds checking, page alignment, and positioning + * based on initial load size requested. + *

    + * Example usage in a PositionalDataSource subclass: + *

    +     * class ItemDataSource extends PositionalDataSource<Item> {
    +     *     private int computeCount() {
    +     *         // actual count code here
    +     *     }
    +     *
    +     *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
    +     *         // actual load code here
    +     *     }
    +     *
    +     *     {@literal @}Override
    +     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
    +     *             {@literal @}NonNull LoadInitialCallback<Item> callback) {
    +     *         int totalCount = computeCount();
    +     *         int position = computeInitialLoadPosition(params, totalCount);
    +     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
    +     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
    +     *     }
    +     *
    +     *     {@literal @}Override
    +     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
    +     *             {@literal @}NonNull LoadRangeCallback<Item> callback) {
    +     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
    +     *     }
    +     * }
    + * + * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, + * including page size, and requested start/loadSize. + * @param totalCount Total size of the data set. + * @return Position to start loading at. + * + * @see #computeInitialLoadSize(LoadInitialParams, int, int) + */ + public static int computeInitialLoadPosition(@NonNull LoadInitialParams params, + int totalCount) { + int position = params.requestedStartPosition; + int initialLoadSize = params.requestedLoadSize; + int pageSize = params.pageSize; + int roundedPageStart = Math.round(position / pageSize) * pageSize; // maximum start pos is that which will encompass end of list - int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize; + int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize; roundedPageStart = Math.min(maximumLoadPage, roundedPageStart); // minimum start position is 0 @@ -246,6 +386,56 @@ public abstract class PositionalDataSource extends DataSource { return roundedPageStart; } + /** + * Helper for computing an initial load size in + * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be + * computed ahead of loading. + *

    + * This function takes the requested load size, and bounds checks it against the value returned + * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}. + *

    + * Example usage in a PositionalDataSource subclass: + *

    +     * class ItemDataSource extends PositionalDataSource<Item> {
    +     *     private int computeCount() {
    +     *         // actual count code here
    +     *     }
    +     *
    +     *     private List<Item> loadRangeInternal(int startPosition, int loadCount) {
    +     *         // actual load code here
    +     *     }
    +     *
    +     *     {@literal @}Override
    +     *     public void loadInitial({@literal @}NonNull LoadInitialParams params,
    +     *             {@literal @}NonNull LoadInitialCallback<Item> callback) {
    +     *         int totalCount = computeCount();
    +     *         int position = computeInitialLoadPosition(params, totalCount);
    +     *         int loadSize = computeInitialLoadSize(params, position, totalCount);
    +     *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
    +     *     }
    +     *
    +     *     {@literal @}Override
    +     *     public void loadRange({@literal @}NonNull LoadRangeParams params,
    +     *             {@literal @}NonNull LoadRangeCallback<Item> callback) {
    +     *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
    +     *     }
    +     * }
    + * + * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, + * including page size, and requested start/loadSize. + * @param initialLoadPosition Value returned by + * {@link #computeInitialLoadPosition(LoadInitialParams, int)} + * @param totalCount Total size of the data set. + * @return Number of items to load. + * + * @see #computeInitialLoadPosition(LoadInitialParams, int) + */ + @SuppressWarnings("WeakerAccess") + public static int computeInitialLoadSize(@NonNull LoadInitialParams params, + int initialLoadPosition, int totalCount) { + return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize); + } + @SuppressWarnings("deprecation") static class ContiguousWithoutPlaceholdersWrapper extends ContiguousDataSource { @@ -259,39 +449,42 @@ public abstract class PositionalDataSource extends DataSource { } @Override - void loadInitial(@Nullable Integer position, int initialLoadSize, int pageSize, + void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize, boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { final int convertPosition = position == null ? 0 : position; // Note enablePlaceholders will be false here, but we don't have a way to communicate // this to PositionalDataSource. This is fine, because only the list and its position - // offset will be consumed by the InitialLoadCallback. - mPositionalDataSource.loadInitial(false, convertPosition, initialLoadSize, + // offset will be consumed by the LoadInitialCallback. + mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize, pageSize, mainThreadExecutor, receiver); } @Override - void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, + void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { int startIndex = currentEndIndex + 1; - mPositionalDataSource.loadRange(startIndex, pageSize, mainThreadExecutor, receiver); + mPositionalDataSource.dispatchLoadRange( + startIndex, pageSize, mainThreadExecutor, receiver); } @Override - void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize, - @NonNull Executor mainThreadExecutor, + void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, + int pageSize, @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver receiver) { int startIndex = currentBeginIndex - 1; if (startIndex < 0) { // trigger empty list load - mPositionalDataSource.loadRange(startIndex, 0, mainThreadExecutor, receiver); + mPositionalDataSource.dispatchLoadRange( + startIndex, 0, mainThreadExecutor, receiver); } else { int loadSize = Math.min(pageSize, startIndex + 1); startIndex = startIndex - loadSize + 1; - mPositionalDataSource.loadRange(startIndex, loadSize, mainThreadExecutor, receiver); + mPositionalDataSource.dispatchLoadRange( + startIndex, loadSize, mainThreadExecutor, receiver); } } diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java index 34d0091e..77695e5c 100644 --- a/android/arch/paging/TiledDataSource.java +++ b/android/arch/paging/TiledDataSource.java @@ -46,8 +46,8 @@ public abstract class TiledDataSource extends PositionalDataSource { public abstract List loadRange(int startPosition, int count); @Override - public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize, - @NonNull InitialLoadCallback callback) { + public void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback) { int totalCount = countItems(); if (totalCount == 0) { callback.onResult(Collections.emptyList(), 0, 0); @@ -55,9 +55,8 @@ public abstract class TiledDataSource extends PositionalDataSource { } // bound the size requested, based on known count - final int firstLoadPosition = computeFirstLoadPosition( - requestedStartPosition, requestedLoadSize, pageSize, totalCount); - final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize); + final int firstLoadPosition = computeInitialLoadPosition(params, totalCount); + final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount); // convert from legacy behavior List list = loadRange(firstLoadPosition, firstLoadSize); @@ -69,8 +68,9 @@ public abstract class TiledDataSource extends PositionalDataSource { } @Override - public void loadRange(int startPosition, int count, @NonNull LoadCallback callback) { - List list = loadRange(startPosition, count); + public void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback) { + List list = loadRange(params.startPosition, params.loadSize); if (list != null) { callback.onResult(list); } else { diff --git a/android/arch/paging/TiledPagedList.java b/android/arch/paging/TiledPagedList.java index 6c189cdb..f7aae980 100644 --- a/android/arch/paging/TiledPagedList.java +++ b/android/arch/paging/TiledPagedList.java @@ -92,7 +92,7 @@ class TiledPagedList extends PagedList final int idealStart = position - firstLoadSize / 2; final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize); - mDataSource.loadInitial(true, roundedPageStart, firstLoadSize, + mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize, pageSize, mMainThreadExecutor, mReceiver); } } @@ -178,7 +178,8 @@ class TiledPagedList extends PagedList } else { int startPosition = pageIndex * pageSize; int count = Math.min(pageSize, mStorage.size() - startPosition); - mDataSource.loadRange(startPosition, count, mMainThreadExecutor, mReceiver); + mDataSource.dispatchLoadRange( + startPosition, count, mMainThreadExecutor, mReceiver); } } }); diff --git a/android/arch/paging/integration/testapp/ItemDataSource.java b/android/arch/paging/integration/testapp/ItemDataSource.java index bbbfabb8..c53d3614 100644 --- a/android/arch/paging/integration/testapp/ItemDataSource.java +++ b/android/arch/paging/integration/testapp/ItemDataSource.java @@ -56,35 +56,19 @@ class ItemDataSource extends PositionalDataSource { return items; } - // TODO: open up this API in PositionalDataSource? - private static int computeFirstLoadPosition(int position, int firstLoadSize, - int pageSize, int size) { - int roundedPageStart = Math.round(position / pageSize) * pageSize; - - // minimum start position is 0 - roundedPageStart = Math.max(0, roundedPageStart); - - // maximum start pos is that which will encompass end of list - int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize; - roundedPageStart = Math.min(maximumLoadPage, roundedPageStart); - - return roundedPageStart; - } - @Override - public void loadInitial(int requestedStartPosition, int requestedLoadSize, - int pageSize, @NonNull InitialLoadCallback callback) { - requestedStartPosition = computeFirstLoadPosition( - requestedStartPosition, requestedLoadSize, pageSize, COUNT); - - requestedLoadSize = Math.min(COUNT - requestedStartPosition, requestedLoadSize); - List data = loadRangeInternal(requestedStartPosition, requestedLoadSize); - callback.onResult(data, requestedStartPosition, COUNT); + public void loadInitial(@NonNull LoadInitialParams params, + @NonNull LoadInitialCallback callback) { + int position = computeInitialLoadPosition(params, COUNT); + int loadSize = computeInitialLoadSize(params, position, COUNT); + List data = loadRangeInternal(position, loadSize); + callback.onResult(data, position, COUNT); } @Override - public void loadRange(int startPosition, int count, @NonNull LoadCallback callback) { - List data = loadRangeInternal(startPosition, count); + public void loadRange(@NonNull LoadRangeParams params, + @NonNull LoadRangeCallback callback) { + List data = loadRangeInternal(params.startPosition, params.loadSize); callback.onResult(data); } } diff --git a/android/arch/persistence/db/SupportSQLiteProgram.java b/android/arch/persistence/db/SupportSQLiteProgram.java index c6d43cca..38c1ac16 100644 --- a/android/arch/persistence/db/SupportSQLiteProgram.java +++ b/android/arch/persistence/db/SupportSQLiteProgram.java @@ -16,16 +16,14 @@ package android.arch.persistence.db; -import android.annotation.TargetApi; -import android.os.Build; +import java.io.Closeable; /** * An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}. */ -@TargetApi(Build.VERSION_CODES.KITKAT) @SuppressWarnings("unused") -public interface SupportSQLiteProgram extends AutoCloseable { +public interface SupportSQLiteProgram extends Closeable { /** * Bind a NULL value to this statement. The value remains bound until * {@link #clearBindings} is called. diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java index e9c2b741..d564a031 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java @@ -16,6 +16,8 @@ package android.arch.persistence.db.framework; +import static android.text.TextUtils.isEmpty; + import android.arch.persistence.db.SimpleSQLiteQuery; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteQuery; @@ -312,8 +314,4 @@ class FrameworkSQLiteDatabase implements SupportSQLiteDatabase { public void close() throws IOException { mDelegate.close(); } - - private static boolean isEmpty(String input) { - return input == null || input.length() == 0; - } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java b/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java index 6c2bb721..73c98c61 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java @@ -60,7 +60,7 @@ class FrameworkSQLiteProgram implements SupportSQLiteProgram { } @Override - public void close() throws Exception { + public void close() { mDelegate.close(); } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java index 53a04bd6..7f07865d 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java @@ -22,7 +22,7 @@ import android.database.sqlite.SQLiteStatement; /** * Delegates all calls to a {@link SQLiteStatement}. */ -class FrameworkSQLiteStatement implements SupportSQLiteStatement { +class FrameworkSQLiteStatement extends FrameworkSQLiteProgram implements SupportSQLiteStatement { private final SQLiteStatement mDelegate; /** @@ -31,39 +31,10 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement { * @param delegate The SQLiteStatement to delegate calls to. */ FrameworkSQLiteStatement(SQLiteStatement delegate) { + super(delegate); mDelegate = delegate; } - @Override - public void bindNull(int index) { - mDelegate.bindNull(index); - } - - @Override - public void bindLong(int index, long value) { - mDelegate.bindLong(index, value); - } - - @Override - public void bindDouble(int index, double value) { - mDelegate.bindDouble(index, value); - } - - @Override - public void bindString(int index, String value) { - mDelegate.bindString(index, value); - } - - @Override - public void bindBlob(int index, byte[] value) { - mDelegate.bindBlob(index, value); - } - - @Override - public void clearBindings() { - mDelegate.clearBindings(); - } - @Override public void execute() { mDelegate.execute(); @@ -88,9 +59,4 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement { public String simpleQueryForString() { return mDelegate.simpleQueryForString(); } - - @Override - public void close() throws Exception { - mDelegate.close(); - } } diff --git a/android/arch/persistence/room/Database.java b/android/arch/persistence/room/Database.java index f12d1b94..14e722fd 100644 --- a/android/arch/persistence/room/Database.java +++ b/android/arch/persistence/room/Database.java @@ -34,7 +34,7 @@ import java.lang.annotation.Target; *
      * // User and Book are classes annotated with {@literal @}Entity.
      * {@literal @}Database(version = 1, entities = {User.class, Book.class})
    - * abstract class AppDatabase extends RoomDatabase() {
    + * abstract class AppDatabase extends RoomDatabase {
      *     // BookDao is a class annotated with {@literal @}Dao.
      *     abstract public BookDao bookDao();
      *     // UserDao is a class annotated with {@literal @}Dao.
    diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java
    index 8c940246..70d832b0 100644
    --- a/android/arch/persistence/room/RoomDatabase.java
    +++ b/android/arch/persistence/room/RoomDatabase.java
    @@ -52,6 +52,13 @@ import java.util.concurrent.locks.ReentrantLock;
     //@SuppressWarnings({"unused", "WeakerAccess"})
     public abstract class RoomDatabase {
         private static final String DB_IMPL_SUFFIX = "_Impl";
    +    /**
    +     * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
    +     *
    +     * @hide
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    public static final int MAX_BIND_PARAMETER_CNT = 999;
         // set by the generated open helper.
         protected volatile SupportSQLiteDatabase mDatabase;
         private SupportSQLiteOpenHelper mOpenHelper;
    @@ -90,7 +97,7 @@ public abstract class RoomDatabase {
          * @param configuration The database configuration.
          */
         @CallSuper
    -    public void init(DatabaseConfiguration configuration) {
    +    public void init(@NonNull DatabaseConfiguration configuration) {
             mOpenHelper = createOpenHelper(configuration);
             mCallbacks = configuration.callbacks;
             mAllowMainThreadQueries = configuration.allowMainThreadQueries;
    @@ -101,6 +108,7 @@ public abstract class RoomDatabase {
          *
          * @return The SQLite open helper used by this database.
          */
    +    @NonNull
         public SupportSQLiteOpenHelper getOpenHelper() {
             return mOpenHelper;
         }
    @@ -113,6 +121,7 @@ public abstract class RoomDatabase {
          * @param config The configuration of the Room database.
          * @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
          */
    +    @NonNull
         protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
     
         /**
    @@ -122,6 +131,7 @@ public abstract class RoomDatabase {
          *
          * @return Creates a new InvalidationTracker.
          */
    +    @NonNull
         protected abstract InvalidationTracker createInvalidationTracker();
     
         /**
    @@ -199,7 +209,7 @@ public abstract class RoomDatabase {
          * @param sql The query to compile.
          * @return The compiled query.
          */
    -    public SupportSQLiteStatement compileStatement(String sql) {
    +    public SupportSQLiteStatement compileStatement(@NonNull String sql) {
             assertNotMainThread();
             return mOpenHelper.getWritableDatabase().compileStatement(sql);
         }
    @@ -238,7 +248,7 @@ public abstract class RoomDatabase {
          *
          * @param body The piece of code to execute.
          */
    -    public void runInTransaction(Runnable body) {
    +    public void runInTransaction(@NonNull Runnable body) {
             beginTransaction();
             try {
                 body.run();
    @@ -256,7 +266,7 @@ public abstract class RoomDatabase {
          * @param   The type of the return value.
          * @return The value returned from the {@link Callable}.
          */
    -    public  V runInTransaction(Callable body) {
    +    public  V runInTransaction(@NonNull Callable body) {
             beginTransaction();
             try {
                 V result = body.call();
    @@ -278,7 +288,7 @@ public abstract class RoomDatabase {
          *
          * @param db The database instance.
          */
    -    protected void internalInitInvalidationTracker(SupportSQLiteDatabase db) {
    +    protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
             mInvalidationTracker.internalInit(db);
         }
     
    @@ -290,6 +300,7 @@ public abstract class RoomDatabase {
          *
          * @return The invalidation tracker for the database.
          */
    +    @NonNull
         public InvalidationTracker getInvalidationTracker() {
             return mInvalidationTracker;
         }
    @@ -365,7 +376,7 @@ public abstract class RoomDatabase {
              * @return this
              */
             @NonNull
    -        public Builder addMigrations(Migration... migrations) {
    +        public Builder addMigrations(@NonNull Migration... migrations) {
                 mMigrationContainer.addMigrations(migrations);
                 return this;
             }
    @@ -471,7 +482,7 @@ public abstract class RoomDatabase {
              *
              * @param migrations List of available migrations.
              */
    -        public void addMigrations(Migration... migrations) {
    +        public void addMigrations(@NonNull Migration... migrations) {
                 for (Migration migration : migrations) {
                     addMigration(migration);
                 }
    diff --git a/android/arch/persistence/room/RoomSQLiteQuery.java b/android/arch/persistence/room/RoomSQLiteQuery.java
    index a8defd48..a10cc525 100644
    --- a/android/arch/persistence/room/RoomSQLiteQuery.java
    +++ b/android/arch/persistence/room/RoomSQLiteQuery.java
    @@ -209,7 +209,7 @@ public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram
         }
     
         @Override
    -    public void close() throws Exception {
    +    public void close() {
             // no-op. not calling release because it is internal API.
         }
     
    diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java
    index 2fad7b1f..610afb26 100644
    --- a/android/arch/persistence/room/integration/testapp/TestDatabase.java
    +++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java
    @@ -32,6 +32,7 @@ import android.arch.persistence.room.integration.testapp.dao.UserDao;
     import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
     import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
     import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
    +import android.arch.persistence.room.integration.testapp.vo.Day;
     import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
     import android.arch.persistence.room.integration.testapp.vo.Pet;
     import android.arch.persistence.room.integration.testapp.vo.PetCouple;
    @@ -41,6 +42,8 @@ import android.arch.persistence.room.integration.testapp.vo.Toy;
     import android.arch.persistence.room.integration.testapp.vo.User;
     
     import java.util.Date;
    +import java.util.HashSet;
    +import java.util.Set;
     
     @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
             BlobEntity.class, Product.class, FunnyNamedEntity.class},
    @@ -74,5 +77,25 @@ public abstract class TestDatabase extends RoomDatabase {
                     return date.getTime();
                 }
             }
    +
    +        @TypeConverter
    +        public Set decomposeDays(int flags) {
    +            Set result = new HashSet<>();
    +            for (Day day : Day.values()) {
    +                if ((flags & (1 << day.ordinal())) != 0) {
    +                    result.add(day);
    +                }
    +            }
    +            return result;
    +        }
    +
    +        @TypeConverter
    +        public int composeDays(Set days) {
    +            int result = 0;
    +            for (Day day : days) {
    +                result |= 1 << day.ordinal();
    +            }
    +            return result;
    +        }
         }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/dao/PetDao.java b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
    index 5179655a..5d060f41 100644
    --- a/android/arch/persistence/room/integration/testapp/dao/PetDao.java
    +++ b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
    @@ -21,6 +21,9 @@ import android.arch.persistence.room.Insert;
     import android.arch.persistence.room.OnConflictStrategy;
     import android.arch.persistence.room.Query;
     import android.arch.persistence.room.integration.testapp.vo.Pet;
    +import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
    +
    +import java.util.List;
     
     @Dao
     public interface PetDao {
    @@ -32,4 +35,7 @@ public interface PetDao {
     
         @Query("SELECT COUNT(*) FROM Pet")
         int count();
    +
    +    @Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
    +    List allPetsWithToyIds();
     }
    diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
    index 0b184a9c..1a2a4689 100644
    --- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java
    +++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
    @@ -29,6 +29,8 @@ import android.arch.persistence.room.Transaction;
     import android.arch.persistence.room.Update;
     import android.arch.persistence.room.integration.testapp.TestDatabase;
     import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
    +import android.arch.persistence.room.integration.testapp.vo.Day;
    +import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
     import android.arch.persistence.room.integration.testapp.vo.User;
     import android.database.Cursor;
     
    @@ -36,6 +38,7 @@ import org.reactivestreams.Publisher;
     
     import java.util.Date;
     import java.util.List;
    +import java.util.Set;
     import java.util.concurrent.Callable;
     
     import io.reactivex.Flowable;
    @@ -203,11 +206,17 @@ public abstract class UserDao {
         @Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
         public abstract int updateByAgeAndIds(float weight, int age, List ids);
     
    +    @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
    +    public abstract List findUsersByWorkDays(Set days);
    +
         // QueryLoader
     
         @Query("SELECT COUNT(*) from user")
         public abstract Integer getUserCount();
     
    +    @Query("SELECT u.mName, u.mLastName from user u where mId = :id")
    +    public abstract NameAndLastName getNameAndLastName(int id);
    +
         @Transaction
         public void insertBothByAnnotation(final User a, final User b) {
             insert(a);
    diff --git a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
    index eb159014..263417e4 100644
    --- a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
    +++ b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
    @@ -27,6 +27,7 @@ import android.arch.persistence.room.integration.testapp.vo.Pet;
     import android.arch.persistence.room.integration.testapp.vo.User;
     import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
     import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
    +import android.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
     import android.arch.persistence.room.integration.testapp.vo.UserAndPetNonNull;
     import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
     import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
    @@ -61,6 +62,9 @@ public interface UserPetDao {
         @Query("SELECT * FROM User UNION ALL SELECT * FROM USER")
         List unionByItself();
     
    +    @Query("SELECT * FROM User")
    +    List loadUserWithPetAdoptionDates();
    +
         @Query("SELECT * FROM User u where u.mId = :userId")
         LiveData liveUserWithPets(int userId);
     
    diff --git a/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java b/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
    index 2db543b3..6278bc28 100644
    --- a/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
    +++ b/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
    @@ -16,10 +16,9 @@
     package android.arch.persistence.room.integration.testapp.database;
     
     import android.arch.paging.DataSource;
    -import android.arch.paging.KeyedDataSource;
    +import android.arch.paging.ItemKeyedDataSource;
     import android.arch.persistence.room.InvalidationTracker;
     import android.support.annotation.NonNull;
    -import android.support.annotation.Nullable;
     
     import java.util.Collections;
     import java.util.List;
    @@ -28,7 +27,7 @@ import java.util.Set;
     /**
      * Sample Room keyed data source.
      */
    -public class LastNameAscCustomerDataSource extends KeyedDataSource {
    +public class LastNameAscCustomerDataSource extends ItemKeyedDataSource {
         private final CustomerDao mCustomerDao;
         @SuppressWarnings("FieldCanBeLocal")
         private final InvalidationTracker.Observer mObserver;
    @@ -76,13 +75,14 @@ public class LastNameAscCustomerDataSource extends KeyedDataSource callback) {
    +    public void loadInitial(@NonNull LoadInitialParams params,
    +            @NonNull LoadInitialCallback callback) {
    +        String customerName = params.requestedInitialKey;
             List list;
             if (customerName != null) {
                 // initial keyed load - load before 'customerName',
                 // and load after last item in before list
    -            int pageSize = initialLoadSize / 2;
    +            int pageSize = params.requestedLoadSize / 2;
                 String key = customerName;
                 list = mCustomerDao.customerNameLoadBefore(key, pageSize);
                 Collections.reverse(list);
    @@ -91,10 +91,10 @@ public class LastNameAscCustomerDataSource extends KeyedDataSource params,
                 @NonNull LoadCallback callback) {
    -        callback.onResult(mCustomerDao.customerNameLoadAfter(currentEndKey, pageSize));
    +        callback.onResult(mCustomerDao.customerNameLoadAfter(params.key, params.requestedLoadSize));
         }
     
         @Override
    -    public void loadBefore(@NonNull String currentBeginKey, int pageSize,
    +    public void loadBefore(@NonNull LoadParams params,
                 @NonNull LoadCallback callback) {
    -        List list = mCustomerDao.customerNameLoadBefore(currentBeginKey, pageSize);
    +        List list = mCustomerDao.customerNameLoadBefore(
    +                params.key, params.requestedLoadSize);
             Collections.reverse(list);
             callback.onResult(list);
         }
     }
    +
    diff --git a/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java b/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
    index 7a21d989..46c875ce 100644
    --- a/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
    +++ b/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
    @@ -28,6 +28,8 @@ import android.arch.persistence.room.PrimaryKey;
     import android.arch.persistence.room.Query;
     import android.arch.persistence.room.Room;
     import android.arch.persistence.room.RoomDatabase;
    +import android.support.annotation.NonNull;
    +import android.support.annotation.Nullable;
     import android.support.test.InstrumentationRegistry;
     import android.support.test.filters.SmallTest;
     import android.support.test.runner.AndroidJUnit4;
    @@ -40,7 +42,8 @@ import org.junit.runner.RunWith;
     @SuppressWarnings("SqlNoDataSourceInspection")
     @SmallTest
     public class ConstructorTest {
    -    @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class},
    +    @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class,
    +            EntityWithAnnotations.class},
                 exportSchema = false)
         abstract static class MyDb extends RoomDatabase {
             abstract MyDao dao();
    @@ -59,6 +62,12 @@ public class ConstructorTest {
     
             @Query("SELECT * FROM pc WHERE a = :a")
             PartialConstructor loadPartial(int a);
    +
    +        @Insert
    +        void insertEntityWithAnnotations(EntityWithAnnotations... items);
    +
    +        @Query("SELECT * FROM EntityWithAnnotations")
    +        EntityWithAnnotations getEntitiWithAnnotations();
         }
     
         @SuppressWarnings("WeakerAccess")
    @@ -167,6 +176,50 @@ public class ConstructorTest {
             }
         }
     
    +    @SuppressWarnings("WeakerAccess")
    +    @Entity
    +    static class EntityWithAnnotations {
    +        @PrimaryKey
    +        @NonNull
    +        public final String id;
    +
    +        @NonNull
    +        public final String username;
    +
    +        @Nullable
    +        public final String displayName;
    +
    +        EntityWithAnnotations(
    +                @NonNull String id,
    +                @NonNull String username,
    +                @Nullable String displayName) {
    +            this.id = id;
    +            this.username = username;
    +            this.displayName = displayName;
    +        }
    +
    +        @Override
    +        public boolean equals(Object o) {
    +            if (this == o) return true;
    +            if (o == null || getClass() != o.getClass()) return false;
    +
    +            EntityWithAnnotations that = (EntityWithAnnotations) o;
    +
    +            if (!id.equals(that.id)) return false;
    +            if (!username.equals(that.username)) return false;
    +            return displayName != null ? displayName.equals(that.displayName)
    +                    : that.displayName == null;
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            int result = id.hashCode();
    +            result = 31 * result + username.hashCode();
    +            result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
    +            return result;
    +        }
    +    }
    +
         private MyDb mDb;
         private MyDao mDao;
     
    @@ -193,4 +246,11 @@ public class ConstructorTest {
             PartialConstructor load = mDao.loadPartial(3);
             assertThat(load, is(item));
         }
    +
    +    @Test // for bug b/69562125
    +    public void entityWithAnnotations() {
    +        EntityWithAnnotations item = new EntityWithAnnotations("a", "b", null);
    +        mDao.insertEntityWithAnnotations(item);
    +        assertThat(mDao.getEntitiWithAnnotations(), is(item));
    +    }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java b/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
    index 45233f37..c3ebfe94 100644
    --- a/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
    +++ b/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
    @@ -22,9 +22,11 @@ import static org.hamcrest.MatcherAssert.assertThat;
     
     import android.arch.persistence.room.integration.testapp.vo.EmbeddedUserAndAllPets;
     import android.arch.persistence.room.integration.testapp.vo.Pet;
    +import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
     import android.arch.persistence.room.integration.testapp.vo.Toy;
     import android.arch.persistence.room.integration.testapp.vo.User;
     import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
    +import android.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
     import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
     import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
     import android.support.test.filters.SmallTest;
    @@ -33,8 +35,10 @@ import android.support.test.runner.AndroidJUnit4;
     import org.junit.Test;
     import org.junit.runner.RunWith;
     
    +import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collections;
    +import java.util.Date;
     import java.util.List;
     
     @RunWith(AndroidJUnit4.class)
    @@ -141,4 +145,90 @@ public class PojoWithRelationTest extends TestDatabaseTest {
             assertThat(relationContainer.getUserAndAllPets().user.getId(), is(1));
             assertThat(relationContainer.getUserAndAllPets().pets.size(), is(2));
         }
    +
    +    @Test
    +    public void boxedPrimitiveList() {
    +        Pet pet1 = TestUtil.createPet(3);
    +        Pet pet2 = TestUtil.createPet(5);
    +
    +        Toy pet1_toy1 = TestUtil.createToyForPet(pet1, 10);
    +        Toy pet1_toy2 = TestUtil.createToyForPet(pet1, 20);
    +        Toy pet2_toy1 = TestUtil.createToyForPet(pet2, 30);
    +
    +        mPetDao.insertOrReplace(pet1, pet2);
    +        mToyDao.insert(pet1_toy1, pet1_toy2, pet2_toy1);
    +
    +        List petWithToyIds = mPetDao.allPetsWithToyIds();
    +        //noinspection ArraysAsListWithZeroOrOneArgument
    +        assertThat(petWithToyIds, is(
    +                Arrays.asList(
    +                        new PetWithToyIds(pet1, Arrays.asList(10, 20)),
    +                        new PetWithToyIds(pet2, Arrays.asList(30)))
    +        ));
    +    }
    +
    +    @Test
    +    public void viaTypeConverter() {
    +        User user = TestUtil.createUser(3);
    +        Pet pet1 = TestUtil.createPet(3);
    +        Date date1 = new Date(300);
    +        pet1.setAdoptionDate(date1);
    +        Pet pet2 = TestUtil.createPet(5);
    +        Date date2 = new Date(700);
    +        pet2.setAdoptionDate(date2);
    +
    +        pet1.setUserId(3);
    +        pet2.setUserId(3);
    +        mUserDao.insert(user);
    +        mPetDao.insertOrReplace(pet1, pet2);
    +
    +        List adoptions =
    +                mUserPetDao.loadUserWithPetAdoptionDates();
    +
    +        assertThat(adoptions, is(Arrays.asList(
    +                new UserAndPetAdoptionDates(user, Arrays.asList(new Date(300), new Date(700)))
    +        )));
    +    }
    +
    +    @Test
    +    public void largeRelation_child() {
    +        User user = TestUtil.createUser(3);
    +        List pets = new ArrayList<>();
    +        for (int i = 0; i < 2000; i++) {
    +            Pet pet = TestUtil.createPet(i + 1);
    +            pet.setUserId(3);
    +        }
    +        mUserDao.insert(user);
    +        mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
    +        List result = mUserPetDao.loadAllUsersWithTheirPets();
    +        assertThat(result.size(), is(1));
    +        assertThat(result.get(0).user, is(user));
    +        assertThat(result.get(0).pets, is(pets));
    +    }
    +
    +    @Test
    +    public void largeRelation_parent() {
    +        final List users = new ArrayList<>();
    +        final List pets = new ArrayList<>();
    +        for (int i = 0; i < 2000; i++) {
    +            User user = TestUtil.createUser(i + 1);
    +            users.add(user);
    +            Pet pet = TestUtil.createPet(i + 1);
    +            pet.setUserId(user.getId());
    +            pets.add(pet);
    +        }
    +        mDatabase.runInTransaction(new Runnable() {
    +            @Override
    +            public void run() {
    +                mUserDao.insertAll(users.toArray(new User[users.size()]));
    +                mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
    +            }
    +        });
    +        List result = mUserPetDao.loadAllUsersWithTheirPets();
    +        assertThat(result.size(), is(2000));
    +        for (int i = 0; i < 2000; i++) {
    +            assertThat(result.get(i).user, is(users.get(i)));
    +            assertThat(result.get(i).pets, is(Collections.singletonList(pets.get(i))));
    +        }
    +    }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
    index f8049f35..de45ebb3 100644
    --- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
    +++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
    @@ -35,6 +35,8 @@ import android.arch.persistence.room.integration.testapp.dao.ProductDao;
     import android.arch.persistence.room.integration.testapp.dao.UserDao;
     import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
     import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
    +import android.arch.persistence.room.integration.testapp.vo.Day;
    +import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
     import android.arch.persistence.room.integration.testapp.vo.Pet;
     import android.arch.persistence.room.integration.testapp.vo.Product;
     import android.arch.persistence.room.integration.testapp.vo.User;
    @@ -47,8 +49,6 @@ import android.support.test.InstrumentationRegistry;
     import android.support.test.filters.SmallTest;
     import android.support.test.runner.AndroidJUnit4;
     
    -import junit.framework.AssertionFailedError;
    -
     import org.junit.Before;
     import org.junit.Test;
     import org.junit.runner.RunWith;
    @@ -57,7 +57,9 @@ import java.util.Arrays;
     import java.util.Calendar;
     import java.util.Collections;
     import java.util.Date;
    +import java.util.HashSet;
     import java.util.List;
    +import java.util.Set;
     
     @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
     @SmallTest
    @@ -158,7 +160,7 @@ public class SimpleEntityReadWriteTest {
             User user2 = TestUtil.createUser(3);
             try {
                 mUserDao.insert(user2);
    -            throw new AssertionFailedError("didn't throw in conflicting insertion");
    +            throw new AssertionError("didn't throw in conflicting insertion");
             } catch (SQLiteException ignored) {
             }
         }
    @@ -524,4 +526,44 @@ public class SimpleEntityReadWriteTest {
             assertTrue("SQLiteConstraintException expected", caught);
             assertThat(mUserDao.count(), is(0));
         }
    +
    +    @Test
    +    public void tablePrefix_simpleSelect() {
    +        User user = TestUtil.createUser(3);
    +        mUserDao.insert(user);
    +        NameAndLastName result = mUserDao.getNameAndLastName(3);
    +        assertThat(result.getName(), is(user.getName()));
    +        assertThat(result.getLastName(), is(user.getLastName()));
    +    }
    +
    +    @Test
    +    public void enumSet_simpleLoad() {
    +        User a = TestUtil.createUser(3);
    +        Set expected = toSet(Day.MONDAY, Day.TUESDAY);
    +        a.setWorkDays(expected);
    +        mUserDao.insert(a);
    +        User loaded = mUserDao.load(3);
    +        assertThat(loaded.getWorkDays(), is(expected));
    +    }
    +
    +    @Test
    +    public void enumSet_query() {
    +        User user1 = TestUtil.createUser(3);
    +        user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
    +        User user2 = TestUtil.createUser(5);
    +        user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
    +        mUserDao.insert(user1);
    +        mUserDao.insert(user2);
    +        List empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
    +        assertThat(empty.size(), is(0));
    +        List friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
    +        assertThat(friday, is(Arrays.asList(user1)));
    +        List monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
    +        assertThat(monday, is(Arrays.asList(user1, user2)));
    +
    +    }
    +
    +    private Set toSet(Day... days) {
    +        return new HashSet<>(Arrays.asList(days));
    +    }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/test/TestUtil.java b/android/arch/persistence/room/integration/testapp/test/TestUtil.java
    index 0a35b2f0..d5309b3b 100644
    --- a/android/arch/persistence/room/integration/testapp/test/TestUtil.java
    +++ b/android/arch/persistence/room/integration/testapp/test/TestUtil.java
    @@ -20,6 +20,7 @@ import android.arch.persistence.room.integration.testapp.vo.Address;
     import android.arch.persistence.room.integration.testapp.vo.Coordinates;
     import android.arch.persistence.room.integration.testapp.vo.Pet;
     import android.arch.persistence.room.integration.testapp.vo.School;
    +import android.arch.persistence.room.integration.testapp.vo.Toy;
     import android.arch.persistence.room.integration.testapp.vo.User;
     
     import java.util.ArrayList;
    @@ -59,9 +60,18 @@ public class TestUtil {
             Pet pet = new Pet();
             pet.setPetId(id);
             pet.setName(UUID.randomUUID().toString());
    +        pet.setAdoptionDate(new Date());
             return pet;
         }
     
    +    public static Toy createToyForPet(Pet pet, int toyId) {
    +        Toy toy = new Toy();
    +        toy.setName("toy " + toyId);
    +        toy.setId(toyId);
    +        toy.setPetId(pet.getPetId());
    +        return toy;
    +    }
    +
         public static Pet[] createPetsForUser(int uid, int petStartId, int count) {
             Pet[] pets = new Pet[count];
             for (int i = 0; i < count; i++) {
    diff --git a/android/arch/persistence/room/integration/testapp/vo/Day.java b/android/arch/persistence/room/integration/testapp/vo/Day.java
    new file mode 100644
    index 00000000..e02b91c4
    --- /dev/null
    +++ b/android/arch/persistence/room/integration/testapp/vo/Day.java
    @@ -0,0 +1,27 @@
    +/*
    + * Copyright 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.arch.persistence.room.integration.testapp.vo;
    +
    +public enum Day {
    +    MONDAY,
    +    TUESDAY,
    +    WEDNESDAY,
    +    THURSDAY,
    +    FRIDAY,
    +    SATURDAY,
    +    SUNDAY
    +}
    diff --git a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
    new file mode 100644
    index 00000000..29e25548
    --- /dev/null
    +++ b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
    @@ -0,0 +1,36 @@
    +/*
    + * Copyright 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.arch.persistence.room.integration.testapp.vo;
    +
    +
    +public class NameAndLastName {
    +    private String mName;
    +    private String mLastName;
    +
    +    public NameAndLastName(String name, String lastName) {
    +        mName = name;
    +        mLastName = lastName;
    +    }
    +
    +    public String getName() {
    +        return mName;
    +    }
    +
    +    public String getLastName() {
    +        return mLastName;
    +    }
    +}
    diff --git a/android/arch/persistence/room/integration/testapp/vo/Pet.java b/android/arch/persistence/room/integration/testapp/vo/Pet.java
    index 8806e10c..cc7549fa 100644
    --- a/android/arch/persistence/room/integration/testapp/vo/Pet.java
    +++ b/android/arch/persistence/room/integration/testapp/vo/Pet.java
    @@ -20,6 +20,8 @@ import android.arch.persistence.room.ColumnInfo;
     import android.arch.persistence.room.Entity;
     import android.arch.persistence.room.PrimaryKey;
     
    +import java.util.Date;
    +
     @Entity
     public class Pet {
         @PrimaryKey
    @@ -28,6 +30,8 @@ public class Pet {
         @ColumnInfo(name = "mPetName")
         private String mName;
     
    +    private Date mAdoptionDate;
    +
         public int getPetId() {
             return mPetId;
         }
    @@ -52,6 +56,14 @@ public class Pet {
             mUserId = userId;
         }
     
    +    public Date getAdoptionDate() {
    +        return mAdoptionDate;
    +    }
    +
    +    public void setAdoptionDate(Date adoptionDate) {
    +        mAdoptionDate = adoptionDate;
    +    }
    +
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
    @@ -61,7 +73,9 @@ public class Pet {
     
             if (mPetId != pet.mPetId) return false;
             if (mUserId != pet.mUserId) return false;
    -        return mName != null ? mName.equals(pet.mName) : pet.mName == null;
    +        if (mName != null ? !mName.equals(pet.mName) : pet.mName != null) return false;
    +        return mAdoptionDate != null ? mAdoptionDate.equals(pet.mAdoptionDate)
    +                : pet.mAdoptionDate == null;
         }
     
         @Override
    @@ -69,6 +83,17 @@ public class Pet {
             int result = mPetId;
             result = 31 * result + mUserId;
             result = 31 * result + (mName != null ? mName.hashCode() : 0);
    +        result = 31 * result + (mAdoptionDate != null ? mAdoptionDate.hashCode() : 0);
             return result;
         }
    +
    +    @Override
    +    public String toString() {
    +        return "Pet{"
    +                + "mPetId=" + mPetId
    +                + ", mUserId=" + mUserId
    +                + ", mName='" + mName + '\''
    +                + ", mAdoptionDate=" + mAdoptionDate
    +                + '}';
    +    }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java b/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
    new file mode 100644
    index 00000000..005afb1c
    --- /dev/null
    +++ b/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
    @@ -0,0 +1,68 @@
    +/*
    + * Copyright 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.arch.persistence.room.integration.testapp.vo;
    +
    +import android.arch.persistence.room.Embedded;
    +import android.arch.persistence.room.Ignore;
    +import android.arch.persistence.room.Relation;
    +
    +import java.util.List;
    +
    +public class PetWithToyIds {
    +    @Embedded
    +    public final Pet pet;
    +    @Relation(parentColumn = "mPetId", entityColumn = "mPetId", projection = "mId",
    +            entity = Toy.class)
    +    public List toyIds;
    +
    +    // for the relation
    +    public PetWithToyIds(Pet pet) {
    +        this.pet = pet;
    +    }
    +
    +    @Ignore
    +    public PetWithToyIds(Pet pet, List toyIds) {
    +        this.pet = pet;
    +        this.toyIds = toyIds;
    +    }
    +
    +    @Override
    +    public boolean equals(Object o) {
    +        if (this == o) return true;
    +        if (o == null || getClass() != o.getClass()) return false;
    +
    +        PetWithToyIds that = (PetWithToyIds) o;
    +
    +        if (pet != null ? !pet.equals(that.pet) : that.pet != null) return false;
    +        return toyIds != null ? toyIds.equals(that.toyIds) : that.toyIds == null;
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        int result = pet != null ? pet.hashCode() : 0;
    +        result = 31 * result + (toyIds != null ? toyIds.hashCode() : 0);
    +        return result;
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "PetWithToyIds{"
    +                + "pet=" + pet
    +                + ", toyIds=" + toyIds
    +                + '}';
    +    }
    +}
    diff --git a/android/arch/persistence/room/integration/testapp/vo/User.java b/android/arch/persistence/room/integration/testapp/vo/User.java
    index a5b88394..a615819b 100644
    --- a/android/arch/persistence/room/integration/testapp/vo/User.java
    +++ b/android/arch/persistence/room/integration/testapp/vo/User.java
    @@ -23,6 +23,8 @@ import android.arch.persistence.room.TypeConverters;
     import android.arch.persistence.room.integration.testapp.TestDatabase;
     
     import java.util.Date;
    +import java.util.HashSet;
    +import java.util.Set;
     
     @Entity
     @TypeConverters({TestDatabase.Converters.class})
    @@ -46,6 +48,9 @@ public class User {
         @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
         private String mCustomField;
     
    +    // bit flags
    +    private Set mWorkDays = new HashSet<>();
    +
         public int getId() {
             return mId;
         }
    @@ -110,6 +115,15 @@ public class User {
             mCustomField = customField;
         }
     
    +    public Set getWorkDays() {
    +        return mWorkDays;
    +    }
    +
    +    public void setWorkDays(
    +            Set workDays) {
    +        mWorkDays = workDays;
    +    }
    +
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
    @@ -128,8 +142,11 @@ public class User {
             if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
                 return false;
             }
    -        return mCustomField != null ? mCustomField.equals(user.mCustomField)
    -                : user.mCustomField == null;
    +        if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
    +                : user.mCustomField != null) {
    +            return false;
    +        }
    +        return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
         }
     
         @Override
    @@ -142,6 +159,7 @@ public class User {
             result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
             result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
             result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
    +        result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
             return result;
         }
     
    @@ -155,7 +173,8 @@ public class User {
                     + ", mAdmin=" + mAdmin
                     + ", mWeight=" + mWeight
                     + ", mBirthday=" + mBirthday
    -                + ", mCustom=" + mCustomField
    +                + ", mCustomField='" + mCustomField + '\''
    +                + ", mWorkDays=" + mWorkDays
                     + '}';
         }
     }
    diff --git a/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java b/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
    new file mode 100644
    index 00000000..28d24959
    --- /dev/null
    +++ b/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
    @@ -0,0 +1,71 @@
    +/*
    + * Copyright 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.arch.persistence.room.integration.testapp.vo;
    +
    +import android.arch.persistence.room.Embedded;
    +import android.arch.persistence.room.Ignore;
    +import android.arch.persistence.room.Relation;
    +
    +import java.util.Date;
    +import java.util.List;
    +
    +public class UserAndPetAdoptionDates {
    +    @Embedded
    +    public final User user;
    +    @Relation(entity = Pet.class,
    +            parentColumn = "mId",
    +            entityColumn = "mUserId",
    +            projection = "mAdoptionDate")
    +    public List petAdoptionDates;
    +
    +    public UserAndPetAdoptionDates(User user) {
    +        this.user = user;
    +    }
    +
    +    @Ignore
    +    public UserAndPetAdoptionDates(User user, List petAdoptionDates) {
    +        this.user = user;
    +        this.petAdoptionDates = petAdoptionDates;
    +    }
    +
    +    @Override
    +    public boolean equals(Object o) {
    +        if (this == o) return true;
    +        if (o == null || getClass() != o.getClass()) return false;
    +
    +        UserAndPetAdoptionDates that = (UserAndPetAdoptionDates) o;
    +
    +        if (user != null ? !user.equals(that.user) : that.user != null) return false;
    +        return petAdoptionDates != null ? petAdoptionDates.equals(that.petAdoptionDates)
    +                : that.petAdoptionDates == null;
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        int result = user != null ? user.hashCode() : 0;
    +        result = 31 * result + (petAdoptionDates != null ? petAdoptionDates.hashCode() : 0);
    +        return result;
    +    }
    +
    +    @Override
    +    public String toString() {
    +        return "UserAndPetAdoptionDates{"
    +                + "user=" + user
    +                + ", petAdoptionDates=" + petAdoptionDates
    +                + '}';
    +    }
    +}
    diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java
    index 578a5b8b..3290d57f 100644
    --- a/android/bluetooth/BluetoothAdapter.java
    +++ b/android/bluetooth/BluetoothAdapter.java
    @@ -1,6 +1,6 @@
     /*
    - * Copyright (C) 2009-2016 The Android Open Source Project
    - * Copyright (C) 2015 Samsung LSI
    + * Copyright 2009-2016 The Android Open Source Project
    + * Copyright 2015 Samsung LSI
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -132,9 +132,8 @@ public final class BluetoothAdapter {
          * respectively.
          * 

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_STATE_CHANGED = - "android.bluetooth.adapter.action.STATE_CHANGED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_STATE_CHANGED = "android.bluetooth.adapter.action.STATE_CHANGED"; /** * Used as an int extra field in {@link #ACTION_STATE_CHANGED} @@ -144,8 +143,7 @@ public final class BluetoothAdapter { * {@link #STATE_ON}, * {@link #STATE_TURNING_OFF}, */ - public static final String EXTRA_STATE = - "android.bluetooth.adapter.extra.STATE"; + public static final String EXTRA_STATE = "android.bluetooth.adapter.extra.STATE"; /** * Used as an int extra field in {@link #ACTION_STATE_CHANGED} * intents to request the previous power state. Possible values are: @@ -158,11 +156,17 @@ public final class BluetoothAdapter { "android.bluetooth.adapter.extra.PREVIOUS_STATE"; /** @hide */ - @IntDef({STATE_OFF, STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, STATE_BLE_TURNING_ON, - STATE_BLE_ON, STATE_BLE_TURNING_OFF}) + @IntDef(prefix = { "STATE_" }, value = { + STATE_OFF, + STATE_TURNING_ON, + STATE_ON, + STATE_TURNING_OFF, + STATE_BLE_TURNING_ON, + STATE_BLE_ON, + STATE_BLE_TURNING_OFF + }) @Retention(RetentionPolicy.SOURCE) - public @interface AdapterState { - } + public @interface AdapterState {} /** * Indicates the local Bluetooth adapter is off. @@ -254,9 +258,8 @@ public final class BluetoothAdapter { * application can be notified when the device has ended discoverability. *

    Requires {@link android.Manifest.permission#BLUETOOTH} */ - @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_REQUEST_DISCOVERABLE = - "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE"; + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String + ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE"; /** * Used as an optional int extra field in {@link @@ -282,9 +285,8 @@ public final class BluetoothAdapter { * for global notification whenever Bluetooth is turned on or off. *

    Requires {@link android.Manifest.permission#BLUETOOTH} */ - @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_REQUEST_ENABLE = - "android.bluetooth.adapter.action.REQUEST_ENABLE"; + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String + ACTION_REQUEST_ENABLE = "android.bluetooth.adapter.action.REQUEST_ENABLE"; /** * Activity Action: Show a system activity that allows the user to turn off @@ -305,9 +307,8 @@ public final class BluetoothAdapter { * * @hide */ - @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_REQUEST_DISABLE = - "android.bluetooth.adapter.action.REQUEST_DISABLE"; + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String + ACTION_REQUEST_DISABLE = "android.bluetooth.adapter.action.REQUEST_DISABLE"; /** * Activity Action: Show a system activity that allows user to enable BLE scans even when @@ -334,9 +335,8 @@ public final class BluetoothAdapter { * respectively. *

    Requires {@link android.Manifest.permission#BLUETOOTH} */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_SCAN_MODE_CHANGED = - "android.bluetooth.adapter.action.SCAN_MODE_CHANGED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_SCAN_MODE_CHANGED = "android.bluetooth.adapter.action.SCAN_MODE_CHANGED"; /** * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED} @@ -357,10 +357,13 @@ public final class BluetoothAdapter { "android.bluetooth.adapter.extra.PREVIOUS_SCAN_MODE"; /** @hide */ - @IntDef({SCAN_MODE_NONE, SCAN_MODE_CONNECTABLE, SCAN_MODE_CONNECTABLE_DISCOVERABLE}) + @IntDef(prefix = { "SCAN_" }, value = { + SCAN_MODE_NONE, + SCAN_MODE_CONNECTABLE, + SCAN_MODE_CONNECTABLE_DISCOVERABLE + }) @Retention(RetentionPolicy.SOURCE) - public @interface ScanMode { - } + public @interface ScanMode {} /** * Indicates that both inquiry scan and page scan are disabled on the local @@ -396,17 +399,15 @@ public final class BluetoothAdapter { * discovery. *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_DISCOVERY_STARTED = - "android.bluetooth.adapter.action.DISCOVERY_STARTED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_DISCOVERY_STARTED = "android.bluetooth.adapter.action.DISCOVERY_STARTED"; /** * Broadcast Action: The local Bluetooth adapter has finished the device * discovery process. *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_DISCOVERY_FINISHED = - "android.bluetooth.adapter.action.DISCOVERY_FINISHED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED"; /** * Broadcast Action: The local Bluetooth adapter has changed its friendly @@ -416,9 +417,8 @@ public final class BluetoothAdapter { * the name. *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_LOCAL_NAME_CHANGED = - "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED"; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_LOCAL_NAME_CHANGED = "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED"; /** * Used as a String extra field in {@link #ACTION_LOCAL_NAME_CHANGED} * intents to request the local Bluetooth name. @@ -451,8 +451,8 @@ public final class BluetoothAdapter { * *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_CONNECTION_STATE_CHANGED = + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String + ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED"; /** @@ -476,8 +476,7 @@ public final class BluetoothAdapter { * * @hide */ - @SystemApi - public static final String ACTION_BLE_STATE_CHANGED = + @SystemApi public static final String ACTION_BLE_STATE_CHANGED = "android.bluetooth.adapter.action.BLE_STATE_CHANGED"; /** @@ -574,8 +573,7 @@ public final class BluetoothAdapter { private final IBluetoothManager mManagerService; private IBluetooth mService; - private final ReentrantReadWriteLock mServiceLock = - new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock mServiceLock = new ReentrantReadWriteLock(); private final Object mLock = new Object(); private final Map mLeScanClients; @@ -655,8 +653,9 @@ public final class BluetoothAdapter { if (address == null || address.length != 6) { throw new IllegalArgumentException("Bluetooth address must have 6 bytes"); } - return new BluetoothDevice(String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", - address[0], address[1], address[2], address[3], address[4], address[5])); + return new BluetoothDevice( + String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", address[0], address[1], + address[2], address[3], address[4], address[5])); } /** @@ -668,7 +667,9 @@ public final class BluetoothAdapter { * on this device before calling this method. */ public BluetoothLeAdvertiser getBluetoothLeAdvertiser() { - if (!getLeAccess()) return null; + if (!getLeAccess()) { + return null; + } synchronized (mLock) { if (sBluetoothLeAdvertiser == null) { sBluetoothLeAdvertiser = new BluetoothLeAdvertiser(mManagerService); @@ -698,8 +699,7 @@ public final class BluetoothAdapter { synchronized (mLock) { if (sPeriodicAdvertisingManager == null) { - sPeriodicAdvertisingManager = - new PeriodicAdvertisingManager(mManagerService); + sPeriodicAdvertisingManager = new PeriodicAdvertisingManager(mManagerService); } } return sPeriodicAdvertisingManager; @@ -709,7 +709,9 @@ public final class BluetoothAdapter { * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations. */ public BluetoothLeScanner getBluetoothLeScanner() { - if (!getLeAccess()) return null; + if (!getLeAccess()) { + return null; + } synchronized (mLock) { if (sBluetoothLeScanner == null) { sBluetoothLeScanner = new BluetoothLeScanner(mManagerService); @@ -729,7 +731,9 @@ public final class BluetoothAdapter { public boolean isEnabled() { try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isEnabled(); + if (mService != null) { + return mService.isEnabled(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -750,7 +754,9 @@ public final class BluetoothAdapter { @SystemApi public boolean isLeEnabled() { final int state = getLeState(); - if (DBG) Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state)); + if (DBG) { + Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state)); + } return (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON); } @@ -781,12 +787,16 @@ public final class BluetoothAdapter { */ @SystemApi public boolean disableBLE() { - if (!isBleScanAlwaysAvailable()) return false; + if (!isBleScanAlwaysAvailable()) { + return false; + } int state = getLeState(); if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON) { String packageName = ActivityThread.currentPackageName(); - if (DBG) Log.d(TAG, "disableBLE(): de-registering " + packageName); + if (DBG) { + Log.d(TAG, "disableBLE(): de-registering " + packageName); + } try { mManagerService.updateBleAppCount(mToken, false, packageName); } catch (RemoteException e) { @@ -795,7 +805,9 @@ public final class BluetoothAdapter { return true; } - if (DBG) Log.d(TAG, "disableBLE(): Already disabled"); + if (DBG) { + Log.d(TAG, "disableBLE(): Already disabled"); + } return false; } @@ -832,16 +844,22 @@ public final class BluetoothAdapter { */ @SystemApi public boolean enableBLE() { - if (!isBleScanAlwaysAvailable()) return false; + if (!isBleScanAlwaysAvailable()) { + return false; + } try { String packageName = ActivityThread.currentPackageName(); mManagerService.updateBleAppCount(mToken, true, packageName); if (isLeEnabled()) { - if (DBG) Log.d(TAG, "enableBLE(): Bluetooth already enabled"); + if (DBG) { + Log.d(TAG, "enableBLE(): Bluetooth already enabled"); + } return true; } - if (DBG) Log.d(TAG, "enableBLE(): Calling enable"); + if (DBG) { + Log.d(TAG, "enableBLE(): Calling enable"); + } return mManagerService.enable(packageName); } catch (RemoteException e) { Log.e(TAG, "", e); @@ -877,19 +895,16 @@ public final class BluetoothAdapter { } // Consider all internal states as OFF - if (state == BluetoothAdapter.STATE_BLE_ON - || state == BluetoothAdapter.STATE_BLE_TURNING_ON + if (state == BluetoothAdapter.STATE_BLE_ON || state == BluetoothAdapter.STATE_BLE_TURNING_ON || state == BluetoothAdapter.STATE_BLE_TURNING_OFF) { if (VDBG) { - Log.d(TAG, - "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF"); + Log.d(TAG, "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF"); } state = BluetoothAdapter.STATE_OFF; } if (VDBG) { - Log.d(TAG, - "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState( - state)); + Log.d(TAG, "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState( + state)); } return state; } @@ -926,7 +941,9 @@ public final class BluetoothAdapter { mServiceLock.readLock().unlock(); } - if (VDBG) Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state)); + if (VDBG) { + Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state)); + } return state; } @@ -967,7 +984,9 @@ public final class BluetoothAdapter { @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean enable() { if (isEnabled()) { - if (DBG) Log.d(TAG, "enable(): BT already enabled!"); + if (DBG) { + Log.d(TAG, "enable(): BT already enabled!"); + } return true; } try { @@ -1093,10 +1112,14 @@ public final class BluetoothAdapter { * @hide */ public ParcelUuid[] getUuids() { - if (getState() != STATE_ON) return null; + if (getState() != STATE_ON) { + return null; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getUuids(); + if (mService != null) { + return mService.getUuids(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1121,10 +1144,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean setName(String name) { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.setName(name); + if (mService != null) { + return mService.setName(name); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1143,10 +1170,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public BluetoothClass getBluetoothClass() { - if (getState() != STATE_ON) return null; + if (getState() != STATE_ON) { + return null; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getBluetoothClass(); + if (mService != null) { + return mService.getBluetoothClass(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1168,10 +1199,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setBluetoothClass(BluetoothClass bluetoothClass) { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.setBluetoothClass(bluetoothClass); + if (mService != null) { + return mService.setBluetoothClass(bluetoothClass); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1198,10 +1233,14 @@ public final class BluetoothAdapter { @RequiresPermission(Manifest.permission.BLUETOOTH) @ScanMode public int getScanMode() { - if (getState() != STATE_ON) return SCAN_MODE_NONE; + if (getState() != STATE_ON) { + return SCAN_MODE_NONE; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getScanMode(); + if (mService != null) { + return mService.getScanMode(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1239,10 +1278,14 @@ public final class BluetoothAdapter { * @hide */ public boolean setScanMode(@ScanMode int mode, int duration) { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.setScanMode(mode, duration); + if (mService != null) { + return mService.setScanMode(mode, duration); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1253,17 +1296,23 @@ public final class BluetoothAdapter { /** @hide */ public boolean setScanMode(int mode) { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } /* getDiscoverableTimeout() to use the latest from NV than use 0 */ return setScanMode(mode, getDiscoverableTimeout()); } /** @hide */ public int getDiscoverableTimeout() { - if (getState() != STATE_ON) return -1; + if (getState() != STATE_ON) { + return -1; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getDiscoverableTimeout(); + if (mService != null) { + return mService.getDiscoverableTimeout(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1274,10 +1323,14 @@ public final class BluetoothAdapter { /** @hide */ public void setDiscoverableTimeout(int timeout) { - if (getState() != STATE_ON) return; + if (getState() != STATE_ON) { + return; + } try { mServiceLock.readLock().lock(); - if (mService != null) mService.setDiscoverableTimeout(timeout); + if (mService != null) { + mService.setDiscoverableTimeout(timeout); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1296,7 +1349,9 @@ public final class BluetoothAdapter { public long getDiscoveryEndMillis() { try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getDiscoveryEndMillis(); + if (mService != null) { + return mService.getDiscoveryEndMillis(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1336,10 +1391,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean startDiscovery() { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.startDiscovery(); + if (mService != null) { + return mService.startDiscovery(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1366,10 +1425,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean cancelDiscovery() { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.cancelDiscovery(); + if (mService != null) { + return mService.cancelDiscovery(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1398,10 +1461,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH) public boolean isDiscovering() { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isDiscovering(); + if (mService != null) { + return mService.isDiscovering(); + } } catch (RemoteException e) { Log.e(TAG, "", e); } finally { @@ -1416,10 +1483,14 @@ public final class BluetoothAdapter { * @return true if Multiple Advertisement feature is supported */ public boolean isMultipleAdvertisementSupported() { - if (getState() != STATE_ON) return false; + if (getState() != STATE_ON) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isMultiAdvertisementSupported(); + if (mService != null) { + return mService.isMultiAdvertisementSupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isMultipleAdvertisementSupported, error: ", e); } finally { @@ -1454,10 +1525,14 @@ public final class BluetoothAdapter { * @return true if chipset supports on-chip filtering */ public boolean isOffloadedFilteringSupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isOffloadedFilteringSupported(); + if (mService != null) { + return mService.isOffloadedFilteringSupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isOffloadedFilteringSupported, error: ", e); } finally { @@ -1472,10 +1547,14 @@ public final class BluetoothAdapter { * @return true if chipset supports on-chip scan batching */ public boolean isOffloadedScanBatchingSupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isOffloadedScanBatchingSupported(); + if (mService != null) { + return mService.isOffloadedScanBatchingSupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isOffloadedScanBatchingSupported, error: ", e); } finally { @@ -1490,10 +1569,14 @@ public final class BluetoothAdapter { * @return true if chipset supports LE 2M PHY feature */ public boolean isLe2MPhySupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isLe2MPhySupported(); + if (mService != null) { + return mService.isLe2MPhySupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isExtendedAdvertisingSupported, error: ", e); } finally { @@ -1508,10 +1591,14 @@ public final class BluetoothAdapter { * @return true if chipset supports LE Coded PHY feature */ public boolean isLeCodedPhySupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isLeCodedPhySupported(); + if (mService != null) { + return mService.isLeCodedPhySupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isLeCodedPhySupported, error: ", e); } finally { @@ -1526,10 +1613,14 @@ public final class BluetoothAdapter { * @return true if chipset supports LE Extended Advertising feature */ public boolean isLeExtendedAdvertisingSupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isLeExtendedAdvertisingSupported(); + if (mService != null) { + return mService.isLeExtendedAdvertisingSupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isLeExtendedAdvertisingSupported, error: ", e); } finally { @@ -1544,10 +1635,14 @@ public final class BluetoothAdapter { * @return true if chipset supports LE Periodic Advertising feature */ public boolean isLePeriodicAdvertisingSupported() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.isLePeriodicAdvertisingSupported(); + if (mService != null) { + return mService.isLePeriodicAdvertisingSupported(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get isLePeriodicAdvertisingSupported, error: ", e); } finally { @@ -1563,10 +1658,14 @@ public final class BluetoothAdapter { * @return the maximum LE advertising data length. */ public int getLeMaximumAdvertisingDataLength() { - if (!getLeAccess()) return 0; + if (!getLeAccess()) { + return 0; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getLeMaximumAdvertisingDataLength(); + if (mService != null) { + return mService.getLeMaximumAdvertisingDataLength(); + } } catch (RemoteException e) { Log.e(TAG, "failed to get getLeMaximumAdvertisingDataLength, error: ", e); } finally { @@ -1582,7 +1681,9 @@ public final class BluetoothAdapter { * @hide */ public boolean isHardwareTrackingFiltersAvailable() { - if (!getLeAccess()) return false; + if (!getLeAccess()) { + return false; + } try { IBluetoothGatt iGatt = mManagerService.getBluetoothGatt(); if (iGatt == null) { @@ -1669,7 +1770,9 @@ public final class BluetoothAdapter { } try { mServiceLock.readLock().lock(); - if (mService != null) return toDeviceSet(mService.getBondedDevices()); + if (mService != null) { + return toDeviceSet(mService.getBondedDevices()); + } return toDeviceSet(new BluetoothDevice[0]); } catch (RemoteException e) { Log.e(TAG, "", e); @@ -1723,10 +1826,14 @@ public final class BluetoothAdapter { * @hide */ public int getConnectionState() { - if (getState() != STATE_ON) return BluetoothAdapter.STATE_DISCONNECTED; + if (getState() != STATE_ON) { + return BluetoothAdapter.STATE_DISCONNECTED; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getAdapterConnectionState(); + if (mService != null) { + return mService.getAdapterConnectionState(); + } } catch (RemoteException e) { Log.e(TAG, "getConnectionState:", e); } finally { @@ -1750,10 +1857,14 @@ public final class BluetoothAdapter { */ @RequiresPermission(Manifest.permission.BLUETOOTH) public int getProfileConnectionState(int profile) { - if (getState() != STATE_ON) return BluetoothProfile.STATE_DISCONNECTED; + if (getState() != STATE_ON) { + return BluetoothProfile.STATE_DISCONNECTED; + } try { mServiceLock.readLock().lock(); - if (mService != null) return mService.getProfileConnectionState(profile); + if (mService != null) { + return mService.getProfileConnectionState(profile); + } } catch (RemoteException e) { Log.e(TAG, "getProfileConnectionState:", e); } finally { @@ -1790,7 +1901,7 @@ public final class BluetoothAdapter { *

    Valid RFCOMM channels are in range 1 to 30. *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} *

    To auto assign a channel without creating a SDP record use - * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number. + * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number. * * @param channel RFCOMM channel to listen on * @param mitm enforce man-in-the-middle protection for authentication. @@ -1802,10 +1913,10 @@ public final class BluetoothAdapter { * @hide */ public BluetoothServerSocket listenUsingRfcommOn(int channel, boolean mitm, - boolean min16DigitPin) - throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm, min16DigitPin); + boolean min16DigitPin) throws IOException { + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm, + min16DigitPin); int errno = socket.mSocket.bindListen(); if (channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { socket.setChannel(socket.mSocket.getPort()); @@ -1913,8 +2024,8 @@ public final class BluetoothAdapter { * permissions, or channel in use. * @hide */ - public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord( - String name, UUID uuid) throws IOException { + public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord(String name, UUID uuid) + throws IOException { return createNewRfcommSocketAndRecord(name, uuid, false, true); } @@ -1922,8 +2033,8 @@ public final class BluetoothAdapter { private BluetoothServerSocket createNewRfcommSocketAndRecord(String name, UUID uuid, boolean auth, boolean encrypt) throws IOException { BluetoothServerSocket socket; - socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth, - encrypt, new ParcelUuid(uuid)); + socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth, encrypt, + new ParcelUuid(uuid)); socket.setServiceName(name); int errno = socket.mSocket.bindListen(); if (errno != 0) { @@ -1945,8 +2056,8 @@ public final class BluetoothAdapter { * @hide */ public BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_RFCOMM, false, false, port); + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, false, port); int errno = socket.mSocket.bindListen(); if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { socket.setChannel(socket.mSocket.getPort()); @@ -1969,10 +2080,9 @@ public final class BluetoothAdapter { * permissions. * @hide */ - public BluetoothServerSocket listenUsingEncryptedRfcommOn(int port) - throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_RFCOMM, false, true, port); + public BluetoothServerSocket listenUsingEncryptedRfcommOn(int port) throws IOException { + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, true, port); int errno = socket.mSocket.bindListen(); if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { socket.setChannel(socket.mSocket.getPort()); @@ -1996,8 +2106,8 @@ public final class BluetoothAdapter { * @hide */ public static BluetoothServerSocket listenUsingScoOn() throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_SCO, false, false, -1); + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_SCO, false, false, -1); int errno = socket.mSocket.bindListen(); if (errno < 0) { //TODO(BT): Throw the same exception error code @@ -2011,7 +2121,7 @@ public final class BluetoothAdapter { * Construct an encrypted, authenticated, L2CAP server socket. * Call #accept to retrieve connections to this socket. *

    To auto assign a port without creating a SDP record use - * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. + * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. * * @param port the PSM to listen on * @param mitm enforce man-in-the-middle protection for authentication. @@ -2024,8 +2134,9 @@ public final class BluetoothAdapter { */ public BluetoothServerSocket listenUsingL2capOn(int port, boolean mitm, boolean min16DigitPin) throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_L2CAP, true, true, port, mitm, min16DigitPin); + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, true, true, port, mitm, + min16DigitPin); int errno = socket.mSocket.bindListen(); if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { socket.setChannel(socket.mSocket.getPort()); @@ -2043,7 +2154,7 @@ public final class BluetoothAdapter { * Construct an encrypted, authenticated, L2CAP server socket. * Call #accept to retrieve connections to this socket. *

    To auto assign a port without creating a SDP record use - * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. + * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. * * @param port the PSM to listen on * @return An L2CAP BluetoothServerSocket @@ -2060,7 +2171,7 @@ public final class BluetoothAdapter { * Construct an insecure L2CAP server socket. * Call #accept to retrieve connections to this socket. *

    To auto assign a port without creating a SDP record use - * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. + * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number. * * @param port the PSM to listen on * @return An L2CAP BluetoothServerSocket @@ -2069,8 +2180,9 @@ public final class BluetoothAdapter { * @hide */ public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException { - BluetoothServerSocket socket = new BluetoothServerSocket( - BluetoothSocket.TYPE_L2CAP, false, false, port, false, false); + BluetoothServerSocket socket = + new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false, + false); int errno = socket.mSocket.bindListen(); if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { socket.setChannel(socket.mSocket.getPort()); @@ -2114,7 +2226,9 @@ public final class BluetoothAdapter { */ public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener, int profile) { - if (context == null || listener == null) return false; + if (context == null || listener == null) { + return false; + } if (profile == BluetoothProfile.HEADSET) { BluetoothHeadset headset = new BluetoothHeadset(context, listener); @@ -2172,7 +2286,9 @@ public final class BluetoothAdapter { * @param proxy Profile proxy object */ public void closeProfileProxy(int profile, BluetoothProfile proxy) { - if (proxy == null) return; + if (proxy == null) { + return; + } switch (profile) { case BluetoothProfile.HEADSET: @@ -2241,7 +2357,9 @@ public final class BluetoothAdapter { private final IBluetoothManagerCallback mManagerCallback = new IBluetoothManagerCallback.Stub() { public void onBluetoothServiceUp(IBluetooth bluetoothService) { - if (DBG) Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService); + if (DBG) { + Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService); + } mServiceLock.writeLock().lock(); mService = bluetoothService; @@ -2263,14 +2381,22 @@ public final class BluetoothAdapter { } public void onBluetoothServiceDown() { - if (DBG) Log.d(TAG, "onBluetoothServiceDown: " + mService); + if (DBG) { + Log.d(TAG, "onBluetoothServiceDown: " + mService); + } try { mServiceLock.writeLock().lock(); mService = null; - if (mLeScanClients != null) mLeScanClients.clear(); - if (sBluetoothLeAdvertiser != null) sBluetoothLeAdvertiser.cleanup(); - if (sBluetoothLeScanner != null) sBluetoothLeScanner.cleanup(); + if (mLeScanClients != null) { + mLeScanClients.clear(); + } + if (sBluetoothLeAdvertiser != null) { + sBluetoothLeAdvertiser.cleanup(); + } + if (sBluetoothLeScanner != null) { + sBluetoothLeScanner.cleanup(); + } } finally { mServiceLock.writeLock().unlock(); } @@ -2291,7 +2417,9 @@ public final class BluetoothAdapter { } public void onBrEdrDown() { - if (VDBG) Log.i(TAG, "onBrEdrDown: " + mService); + if (VDBG) { + Log.i(TAG, "onBrEdrDown: " + mService); + } } }; @@ -2305,7 +2433,9 @@ public final class BluetoothAdapter { @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean enableNoAutoConnect() { if (isEnabled()) { - if (DBG) Log.d(TAG, "enableNoAutoConnect(): BT already enabled!"); + if (DBG) { + Log.d(TAG, "enableNoAutoConnect(): BT already enabled!"); + } return true; } try { @@ -2354,7 +2484,10 @@ public final class BluetoothAdapter { * @hide */ public interface BluetoothStateChangeCallback { - public void onBluetoothStateChange(boolean on); + /** + * @hide + */ + void onBluetoothStateChange(boolean on); } /** @@ -2363,8 +2496,7 @@ public final class BluetoothAdapter { public class StateChangeCallbackWrapper extends IBluetoothStateChangeCallback.Stub { private BluetoothStateChangeCallback mCallback; - StateChangeCallbackWrapper(BluetoothStateChangeCallback - callback) { + StateChangeCallbackWrapper(BluetoothStateChangeCallback callback) { mCallback = callback; } @@ -2461,7 +2593,7 @@ public final class BluetoothAdapter { * if no RSSI value is available. * @param scanRecord The content of the advertisement record offered by the remote device. */ - public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord); + void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord); } /** @@ -2497,20 +2629,28 @@ public final class BluetoothAdapter { @Deprecated @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean startLeScan(final UUID[] serviceUuids, final LeScanCallback callback) { - if (DBG) Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids)); + if (DBG) { + Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids)); + } if (callback == null) { - if (DBG) Log.e(TAG, "startLeScan: null callback"); + if (DBG) { + Log.e(TAG, "startLeScan: null callback"); + } return false; } BluetoothLeScanner scanner = getBluetoothLeScanner(); if (scanner == null) { - if (DBG) Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner"); + if (DBG) { + Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner"); + } return false; } synchronized (mLeScanClients) { if (mLeScanClients.containsKey(callback)) { - if (DBG) Log.e(TAG, "LE Scan has already started"); + if (DBG) { + Log.e(TAG, "LE Scan has already started"); + } return false; } @@ -2540,7 +2680,9 @@ public final class BluetoothAdapter { } List scanServiceUuids = scanRecord.getServiceUuids(); if (scanServiceUuids == null || !scanServiceUuids.containsAll(uuids)) { - if (DBG) Log.d(TAG, "uuids does not match"); + if (DBG) { + Log.d(TAG, "uuids does not match"); + } return; } } @@ -2548,16 +2690,18 @@ public final class BluetoothAdapter { scanRecord.getBytes()); } }; - ScanSettings settings = new ScanSettings.Builder() - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); + ScanSettings settings = new ScanSettings.Builder().setCallbackType( + ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(); List filters = new ArrayList(); if (serviceUuids != null && serviceUuids.length > 0) { // Note scan filter does not support matching an UUID array so we put one // UUID to hardware and match the whole array in callback. - ScanFilter filter = new ScanFilter.Builder().setServiceUuid( - new ParcelUuid(serviceUuids[0])).build(); + ScanFilter filter = + new ScanFilter.Builder().setServiceUuid(new ParcelUuid(serviceUuids[0])) + .build(); filters.add(filter); } scanner.startScan(filters, settings, scanCallback); @@ -2582,7 +2726,9 @@ public final class BluetoothAdapter { @Deprecated @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public void stopLeScan(LeScanCallback callback) { - if (DBG) Log.d(TAG, "stopLeScan()"); + if (DBG) { + Log.d(TAG, "stopLeScan()"); + } BluetoothLeScanner scanner = getBluetoothLeScanner(); if (scanner == null) { return; @@ -2590,7 +2736,9 @@ public final class BluetoothAdapter { synchronized (mLeScanClients) { ScanCallback scanCallback = mLeScanClients.remove(callback); if (scanCallback == null) { - if (DBG) Log.d(TAG, "scan not started yet"); + if (DBG) { + Log.d(TAG, "scan not started yet"); + } return; } scanner.stopScan(scanCallback); diff --git a/android/bluetooth/BluetoothHeadset.java b/android/bluetooth/BluetoothHeadset.java index 1241f230..838d3153 100644 --- a/android/bluetooth/BluetoothHeadset.java +++ b/android/bluetooth/BluetoothHeadset.java @@ -677,33 +677,6 @@ public final class BluetoothHeadset implements BluetoothProfile { return false; } - /** - * Get battery usage hint for Bluetooth Headset service. - * This is a monotonically increasing integer. Wraps to 0 at - * Integer.MAX_INT, and at boot. - * Current implementation returns the number of AT commands handled since - * boot. This is a good indicator for spammy headset/handsfree units that - * can keep the device awake by polling for cellular status updates. As a - * rule of thumb, each AT command prevents the CPU from sleeping for 500 ms - * - * @param device the bluetooth headset. - * @return monotonically increasing battery usage hint, or a negative error code on error - * @hide - */ - public int getBatteryUsageHint(BluetoothDevice device) { - if (VDBG) log("getBatteryUsageHint()"); - final IBluetoothHeadset service = mService; - if (service != null && isEnabled() && isValidDevice(device)) { - try { - return service.getBatteryUsageHint(device); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(new Throwable())); - } - } - if (service == null) Log.w(TAG, "Proxy not attached to service"); - return -1; - } - /** * Indicates if current platform supports voice dialing over bluetooth SCO. * @@ -715,49 +688,6 @@ public final class BluetoothHeadset implements BluetoothProfile { com.android.internal.R.bool.config_bluetooth_sco_off_call); } - /** - * Accept the incoming connection. - * Note: This is an internal function and shouldn't be exposed - * - * @hide - */ - public boolean acceptIncomingConnect(BluetoothDevice device) { - if (DBG) log("acceptIncomingConnect"); - final IBluetoothHeadset service = mService; - if (service != null && isEnabled()) { - try { - return service.acceptIncomingConnect(device); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); - } - return false; - } - - /** - * Reject the incoming connection. - * - * @hide - */ - public boolean rejectIncomingConnect(BluetoothDevice device) { - if (DBG) log("rejectIncomingConnect"); - final IBluetoothHeadset service = mService; - if (service != null) { - try { - return service.rejectIncomingConnect(device); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); - } - return false; - } - /** * Get the current audio state of the Headset. * Note: This is an internal function and shouldn't be exposed @@ -1052,50 +982,6 @@ public final class BluetoothHeadset implements BluetoothProfile { return false; } - /** - * enable WBS codec setting. - * - * @return true if successful false if there was some error such as there is no connected - * headset - * @hide - */ - public boolean enableWBS() { - final IBluetoothHeadset service = mService; - if (service != null && isEnabled()) { - try { - return service.enableWBS(); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); - } - return false; - } - - /** - * disable WBS codec settting. It set NBS codec. - * - * @return true if successful false if there was some error such as there is no connected - * headset - * @hide - */ - public boolean disableWBS() { - final IBluetoothHeadset service = mService; - if (service != null && isEnabled()) { - try { - return service.disableWBS(); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); - } - return false; - } - /** * check if in-band ringing is supported for this platform. * @@ -1107,30 +993,6 @@ public final class BluetoothHeadset implements BluetoothProfile { com.android.internal.R.bool.config_bluetooth_hfp_inband_ringing_support); } - /** - * Send Headset the BIND response from AG to report change in the status of the - * HF indicators to the headset - * - * @param indId Assigned Number of the indicator (defined by SIG) - * @param indStatus possible values- false-Indicator is disabled, no value changes shall be - * sent for this indicator true-Indicator is enabled, value changes may be sent for this - * indicator - * @hide - */ - public void bindResponse(int indId, boolean indStatus) { - final IBluetoothHeadset service = mService; - if (service != null && isEnabled()) { - try { - service.bindResponse(indId, indStatus); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); - } - } - private final IBluetoothProfileServiceConnection mConnection = new IBluetoothProfileServiceConnection.Stub() { @Override diff --git a/android/bluetooth/BluetoothHidDevice.java b/android/bluetooth/BluetoothHidDevice.java index 6692e137..2fab305b 100644 --- a/android/bluetooth/BluetoothHidDevice.java +++ b/android/bluetooth/BluetoothHidDevice.java @@ -31,36 +31,31 @@ import java.util.Arrays; import java.util.List; /** - * Provides the public APIs to control the Bluetooth HID Device - * profile. + * Provides the public APIs to control the Bluetooth HID Device profile. * - * BluetoothHidDevice is a proxy object for controlling the Bluetooth HID - * Device Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get - * the BluetoothHidDevice proxy object. - * - * {@hide} + *

    BluetoothHidDevice is a proxy object for controlling the Bluetooth HID Device Service via IPC. + * Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHidDevice proxy object. */ public final class BluetoothHidDevice implements BluetoothProfile { private static final String TAG = BluetoothHidDevice.class.getSimpleName(); /** - * Intent used to broadcast the change in connection state of the Input - * Host profile. + * Intent used to broadcast the change in connection state of the Input Host profile. * *

    This intent will have 3 extras: + * *

      - *
    • {@link #EXTRA_STATE} - The current state of the profile.
    • - *
    • {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
    • - *
    • {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
    • + *
    • {@link #EXTRA_STATE} - The current state of the profile. + *
    • {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. + *
    • {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. *
    * - *

    {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of - * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, - * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. + *

    {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link + * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link + * #STATE_DISCONNECTING}. * - *

    Requires {@link android.Manifest.permission#BLUETOOTH} permission to - * receive. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} permission to receive. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_CONNECTION_STATE_CHANGED = @@ -69,9 +64,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { /** * Constants representing device subclass. * - * @see #registerApp - * (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings, - * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback) + * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings, + * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback) */ public static final byte SUBCLASS1_NONE = (byte) 0x00; public static final byte SUBCLASS1_KEYBOARD = (byte) 0x40; @@ -91,7 +85,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { * * @see BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int) * @see BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[]) - * @see BluetoothHidDeviceCallback#onIntrData(BluetoothDevice, byte, byte[]) + * @see BluetoothHidDeviceCallback#onInterruptData(BluetoothDevice, byte, byte[]) */ public static final byte REPORT_TYPE_INPUT = (byte) 1; public static final byte REPORT_TYPE_OUTPUT = (byte) 2; @@ -110,8 +104,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { public static final byte ERROR_RSP_UNKNOWN = (byte) 14; /** - * Constants representing protocol mode used set by host. Default is always - * {@link #PROTOCOL_REPORT_MODE} unless notified otherwise. + * Constants representing protocol mode used set by host. Default is always {@link + * #PROTOCOL_REPORT_MODE} unless notified otherwise. * * @see BluetoothHidDeviceCallback#onSetProtocol(BluetoothDevice, byte) */ @@ -126,8 +120,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { private BluetoothAdapter mAdapter; - private static class BluetoothHidDeviceCallbackWrapper extends - IBluetoothHidDeviceCallback.Stub { + private static class BluetoothHidDeviceCallbackWrapper + extends IBluetoothHidDeviceCallback.Stub { private BluetoothHidDeviceCallback mCallback; @@ -136,9 +130,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { } @Override - public void onAppStatusChanged(BluetoothDevice pluggedDevice, - BluetoothHidDeviceAppConfiguration config, boolean registered) { - mCallback.onAppStatusChanged(pluggedDevice, config, registered); + public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { + mCallback.onAppStatusChanged(pluggedDevice, registered); } @Override @@ -162,8 +155,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { } @Override - public void onIntrData(BluetoothDevice device, byte reportId, byte[] data) { - mCallback.onIntrData(device, reportId, data); + public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) { + mCallback.onInterruptData(device, reportId, data); } @Override @@ -185,13 +178,11 @@ public final class BluetoothHidDevice implements BluetoothProfile { doBind(); } } catch (IllegalStateException e) { - Log.e(TAG, - "onBluetoothStateChange: could not bind to HID Dev " - + "service: ", e); + Log.e(TAG, "onBluetoothStateChange: could not bind to HID Dev " + + "service: ", e); } catch (SecurityException e) { - Log.e(TAG, - "onBluetoothStateChange: could not bind to HID Dev " - + "service: ", e); + Log.e(TAG, "onBluetoothStateChange: could not bind to HID Dev " + + "service: ", e); } } else { Log.d(TAG, "Unbinding service..."); @@ -201,23 +192,25 @@ public final class BluetoothHidDevice implements BluetoothProfile { } }; - private final ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - Log.d(TAG, "onServiceConnected()"); - mService = IBluetoothHidDevice.Stub.asInterface(service); - if (mServiceListener != null) { - mServiceListener.onServiceConnected(BluetoothProfile.HID_DEVICE, - BluetoothHidDevice.this); - } - } - public void onServiceDisconnected(ComponentName className) { - Log.d(TAG, "onServiceDisconnected()"); - mService = null; - if (mServiceListener != null) { - mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE); - } - } - }; + private final ServiceConnection mConnection = + new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + Log.d(TAG, "onServiceConnected()"); + mService = IBluetoothHidDevice.Stub.asInterface(service); + if (mServiceListener != null) { + mServiceListener.onServiceConnected( + BluetoothProfile.HID_DEVICE, BluetoothHidDevice.this); + } + } + + public void onServiceDisconnected(ComponentName className) { + Log.d(TAG, "onServiceDisconnected()"); + mService = null; + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE); + } + } + }; BluetoothHidDevice(Context context, ServiceListener listener) { Log.v(TAG, "BluetoothHidDevice"); @@ -281,9 +274,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { mServiceListener = null; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public List getConnectedDevices() { Log.v(TAG, "getConnectedDevices()"); @@ -302,9 +293,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { return new ArrayList(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public List getDevicesMatchingConnectionStates(int[] states) { Log.v(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); @@ -323,9 +312,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { return new ArrayList(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public int getConnectionState(BluetoothDevice device) { Log.v(TAG, "getConnectionState(): device=" + device); @@ -345,33 +332,31 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Registers application to be used for HID device. Connections to HID - * Device are only possible when application is registered. Only one - * application can be registered at time. When no longer used, application - * should be unregistered using - * {@link #unregisterApp(BluetoothHidDeviceAppConfiguration)}. - * The registration status should be tracked by the application by handling callback from - * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related - * to the return value of this method. + * Registers application to be used for HID device. Connections to HID Device are only possible + * when application is registered. Only one application can be registered at one time. When an + * application is registered, the HID Host service will be disabled until it is unregistered. + * When no longer used, application should be unregistered using {@link #unregisterApp()}. The + * registration status should be tracked by the application by handling callback from + * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related to + * the return value of this method. * - * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of HID Device SDP record. - * The HID Device SDP record is required. - * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of Incoming QoS Settings. - * The Incoming QoS Settings is not required. Use null or default - * BluetoothHidDeviceAppQosSettings.Builder for default values. - * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of Outgoing QoS Settings. - * The Outgoing QoS Settings is not required. Use null or default - * BluetoothHidDeviceAppQosSettings.Builder for default values. + * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of HID Device SDP record. The HID + * Device SDP record is required. + * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of Incoming QoS Settings. The + * Incoming QoS Settings is not required. Use null or default + * BluetoothHidDeviceAppQosSettings.Builder for default values. + * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of Outgoing QoS Settings. The + * Outgoing QoS Settings is not required. Use null or default + * BluetoothHidDeviceAppQosSettings.Builder for default values. * @param callback {@link BluetoothHidDeviceCallback} object to which callback messages will be - * sent. - * The BluetoothHidDeviceCallback object is required. + * sent. The BluetoothHidDeviceCallback object is required. * @return true if the command is successfully sent; otherwise false. */ public boolean registerApp(BluetoothHidDeviceAppSdpSettings sdp, BluetoothHidDeviceAppQosSettings inQos, BluetoothHidDeviceAppQosSettings outQos, BluetoothHidDeviceCallback callback) { Log.v(TAG, "registerApp(): sdp=" + sdp + " inQos=" + inQos + " outQos=" + outQos - + " callback=" + callback); + + " callback=" + callback); boolean result = false; @@ -382,11 +367,9 @@ public final class BluetoothHidDevice implements BluetoothProfile { final IBluetoothHidDevice service = mService; if (service != null) { try { - BluetoothHidDeviceAppConfiguration config = - new BluetoothHidDeviceAppConfiguration(); BluetoothHidDeviceCallbackWrapper cbw = new BluetoothHidDeviceCallbackWrapper(callback); - result = service.registerApp(config, sdp, inQos, outQos, cbw); + result = service.registerApp(sdp, inQos, outQos, cbw); } catch (RemoteException e) { Log.e(TAG, e.toString()); } @@ -398,22 +381,17 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Unregisters application. Active connection will be disconnected and no - * new connections will be allowed until registered again using - * {@link #registerApp + * Unregisters application. Active connection will be disconnected and no new connections will + * be allowed until registered again using {@link #registerApp * (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings, - * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)} - * The registration status should be tracked by the application by handling callback from - * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related - * to the return value of this method. + * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)} The registration status should + * be tracked by the application by handling callback from + * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related to + * the return value of this method. * - * @param config {@link BluetoothHidDeviceAppConfiguration} object as obtained from {@link - * BluetoothHidDeviceCallback#onAppStatusChanged(BluetoothDevice, - * BluetoothHidDeviceAppConfiguration, - * boolean)} * @return true if the command is successfully sent; otherwise false. */ - public boolean unregisterApp(BluetoothHidDeviceAppConfiguration config) { + public boolean unregisterApp() { Log.v(TAG, "unregisterApp()"); boolean result = false; @@ -421,7 +399,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { final IBluetoothHidDevice service = mService; if (service != null) { try { - result = service.unregisterApp(config); + result = service.unregisterApp(); } catch (RemoteException e) { Log.e(TAG, e.toString()); } @@ -436,7 +414,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { * Sends report to remote host using interrupt channel. * * @param id Report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in - * descriptor. + * descriptor. * @param data Report data, not including Report Id. * @return true if the command is successfully sent; otherwise false. */ @@ -458,8 +436,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Sends report to remote host as reply for GET_REPORT request from - * {@link BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int)}. + * Sends report to remote host as reply for GET_REPORT request from {@link + * BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int)}. * * @param type Report Type, as in request. * @param id Report Id, as in request. @@ -486,8 +464,8 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Sends error handshake message as reply for invalid SET_REPORT request - * from {@link BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[])}. + * Sends error handshake message as reply for invalid SET_REPORT request from {@link + * BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[])}. * * @param error Error to be sent for SET_REPORT via HANDSHAKE. * @return true if the command is successfully sent; otherwise false. @@ -515,6 +493,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { * Sends Virtual Cable Unplug to currently connected host. * * @return + * {@hide} */ public boolean unplug(BluetoothDevice device) { Log.v(TAG, "unplug(): device=" + device); @@ -536,11 +515,11 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Initiates connection to host which is currently paired with this device. - * If the application is not registered, #connect(BluetoothDevice) will fail. - * The connection state should be tracked by the application by handling callback from - * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related - * to the return value of this method. + * Initiates connection to host which is currently paired with this device. If the application + * is not registered, #connect(BluetoothDevice) will fail. The connection state should be + * tracked by the application by handling callback from + * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related to + * the return value of this method. * * @return true if the command is successfully sent; otherwise false. */ @@ -564,10 +543,9 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** - * Disconnects from currently connected host. - * The connection state should be tracked by the application by handling callback from - * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related - * to the return value of this method. + * Disconnects from currently connected host. The connection state should be tracked by the + * application by handling callback from BluetoothHidDeviceCallback#onConnectionStateChanged. + * The connection state is not related to the return value of this method. * * @return true if the command is successfully sent; otherwise false. */ diff --git a/android/bluetooth/BluetoothHidDeviceAppConfiguration.java b/android/bluetooth/BluetoothHidDeviceAppConfiguration.java deleted file mode 100644 index d1efa2d6..00000000 --- a/android/bluetooth/BluetoothHidDeviceAppConfiguration.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -import android.os.Parcel; -import android.os.Parcelable; - -import java.util.Random; - -/** - * Represents the app configuration for a Bluetooth HID Device application. - * - * The app needs a BluetoothHidDeviceAppConfiguration token to unregister - * the Bluetooth HID Device service. - * - * {@see BluetoothHidDevice} - * - * {@hide} - */ -public final class BluetoothHidDeviceAppConfiguration implements Parcelable { - private final long mHash; - - BluetoothHidDeviceAppConfiguration() { - Random rnd = new Random(); - mHash = rnd.nextLong(); - } - - BluetoothHidDeviceAppConfiguration(long hash) { - mHash = hash; - } - - @Override - public boolean equals(Object o) { - if (o instanceof BluetoothHidDeviceAppConfiguration) { - BluetoothHidDeviceAppConfiguration config = (BluetoothHidDeviceAppConfiguration) o; - return mHash == config.mHash; - } - return false; - } - - @Override - public int describeContents() { - return 0; - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - - @Override - public BluetoothHidDeviceAppConfiguration createFromParcel(Parcel in) { - long hash = in.readLong(); - return new BluetoothHidDeviceAppConfiguration(hash); - } - - @Override - public BluetoothHidDeviceAppConfiguration[] newArray(int size) { - return new BluetoothHidDeviceAppConfiguration[size]; - } - }; - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeLong(mHash); - } -} diff --git a/android/bluetooth/BluetoothHidDeviceAppQosSettings.java b/android/bluetooth/BluetoothHidDeviceAppQosSettings.java index 881ae98d..c05df2d2 100644 --- a/android/bluetooth/BluetoothHidDeviceAppQosSettings.java +++ b/android/bluetooth/BluetoothHidDeviceAppQosSettings.java @@ -20,15 +20,12 @@ import android.os.Parcel; import android.os.Parcelable; /** - * Represents the Quality of Service (QoS) settings for a Bluetooth HID Device - * application. + * Represents the Quality of Service (QoS) settings for a Bluetooth HID Device application. * - * The BluetoothHidDevice framework will update the L2CAP QoS settings for the - * app during registration. + *

    The BluetoothHidDevice framework will update the L2CAP QoS settings for the app during + * registration. * - * {@see BluetoothHidDevice} - * - * {@hide} + *

    {@see BluetoothHidDevice} */ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { @@ -46,13 +43,10 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { public static final int MAX = (int) 0xffffffff; /** - * Create a BluetoothHidDeviceAppQosSettings object for the Bluetooth L2CAP channel. - * The QoS Settings is optional. - * Recommended to use BluetoothHidDeviceAppQosSettings.Builder. - * {@see - * https://www.bluetooth.com/specifications/profiles-overview - * - * Bluetooth HID Specfication v1.1.1 Section 5.2 and Appendix D } + * Create a BluetoothHidDeviceAppQosSettings object for the Bluetooth L2CAP channel. The QoS + * Settings is optional. Recommended to use BluetoothHidDeviceAppQosSettings.Builder. + * Please refer to Bluetooth HID Specfication v1.1.1 Section 5.2 and Appendix D for parameters. + * * @param serviceType L2CAP service type * @param tokenRate L2CAP token rate * @param tokenBucketSize L2CAP token bucket size @@ -123,13 +117,11 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** @return an int array representation of this instance */ public int[] toArray() { return new int[] { - serviceType, tokenRate, tokenBucketSize, peakBandwidth, latency, delayVariation + serviceType, tokenRate, tokenBucketSize, peakBandwidth, latency, delayVariation }; } - /** - * A helper to build the BluetoothHidDeviceAppQosSettings object. - */ + /** A helper to build the BluetoothHidDeviceAppQosSettings object. */ public static class Builder { // Optional parameters - initialized to default values private int mServiceType = SERVICE_BEST_EFFORT; @@ -141,8 +133,9 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** * Set the service type. + * * @param val service type. Should be one of {SERVICE_NO_TRAFFIC, SERVICE_BEST_EFFORT, - * SERVICE_GUARANTEED}, with SERVICE_BEST_EFFORT being the default one. + * SERVICE_GUARANTEED}, with SERVICE_BEST_EFFORT being the default one. * @return BluetoothHidDeviceAppQosSettings Builder with specified service type. */ public Builder serviceType(int val) { @@ -151,6 +144,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { } /** * Set the token rate. + * * @param val token rate * @return BluetoothHidDeviceAppQosSettings Builder with specified token rate. */ @@ -161,6 +155,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** * Set the bucket size. + * * @param val bucket size * @return BluetoothHidDeviceAppQosSettings Builder with specified bucket size. */ @@ -171,6 +166,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** * Set the peak bandwidth. + * * @param val peak bandwidth * @return BluetoothHidDeviceAppQosSettings Builder with specified peak bandwidth. */ @@ -180,6 +176,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { } /** * Set the latency. + * * @param val latency * @return BluetoothHidDeviceAppQosSettings Builder with specified latency. */ @@ -190,6 +187,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** * Set the delay variation. + * * @param val delay variation * @return BluetoothHidDeviceAppQosSettings Builder with specified delay variation. */ @@ -200,6 +198,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable { /** * Build the BluetoothHidDeviceAppQosSettings object. + * * @return BluetoothHidDeviceAppQosSettings object with current settings. */ public BluetoothHidDeviceAppQosSettings build() { diff --git a/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java b/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java index 46696370..562c559e 100644 --- a/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java +++ b/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java @@ -22,16 +22,12 @@ import android.os.Parcelable; import java.util.Arrays; /** - * Represents the Service Discovery Protocol (SDP) settings for a Bluetooth - * HID Device application. + * Represents the Service Discovery Protocol (SDP) settings for a Bluetooth HID Device application. * - * The BluetoothHidDevice framework adds the SDP record during app - * registration, so that the Android device can be discovered as a Bluetooth - * HID Device. + *

    The BluetoothHidDevice framework adds the SDP record during app registration, so that the + * Android device can be discovered as a Bluetooth HID Device. * - * {@see BluetoothHidDevice} - * - * {@hide} + *

    {@see BluetoothHidDevice} */ public final class BluetoothHidDeviceAppSdpSettings implements Parcelable { @@ -43,18 +39,19 @@ public final class BluetoothHidDeviceAppSdpSettings implements Parcelable { /** * Create a BluetoothHidDeviceAppSdpSettings object for the Bluetooth SDP record. + * * @param name Name of this Bluetooth HID device. Maximum length is 50 bytes. * @param description Description for this Bluetooth HID device. Maximum length is 50 bytes. * @param provider Provider of this Bluetooth HID device. Maximum length is 50 bytes. - * @param subclass Subclass of this Bluetooth HID device. - * See + * @param subclass Subclass of this Bluetooth HID device. See * www.usb.org/developers/hidpage/HID1_11.pdf Section 4.2 - * @param descriptors Descriptors of this Bluetooth HID device. - * See + * @param descriptors Descriptors of this Bluetooth HID device. See * www.usb.org/developers/hidpage/HID1_11.pdf Chapter 6 Maximum length is 2048 bytes. */ - public BluetoothHidDeviceAppSdpSettings(String name, String description, String provider, - byte subclass, byte[] descriptors) { + public BluetoothHidDeviceAppSdpSettings( + String name, String description, String provider, byte subclass, byte[] descriptors) { this.name = name; this.description = description; this.provider = provider; diff --git a/android/bluetooth/BluetoothHidDeviceCallback.java b/android/bluetooth/BluetoothHidDeviceCallback.java index 5ccda0dc..e71b00f2 100644 --- a/android/bluetooth/BluetoothHidDeviceCallback.java +++ b/android/bluetooth/BluetoothHidDeviceCallback.java @@ -19,50 +19,41 @@ package android.bluetooth; import android.util.Log; /** - * The template class that applications use to call callback functions on - * events from the HID host. Callback functions are wrapped in this class and - * registered to the Android system during app registration. + * The template class that applications use to call callback functions on events from the HID host. + * Callback functions are wrapped in this class and registered to the Android system during app + * registration. * - * {@see BluetoothHidDevice} - * - * {@hide} + *

    {@see BluetoothHidDevice} */ public abstract class BluetoothHidDeviceCallback { private static final String TAG = "BluetoothHidDevCallback"; /** - * Callback called when application registration state changes. Usually it's - * called due to either - * {@link BluetoothHidDevice#registerApp - * (String, String, String, byte, byte[], BluetoothHidDeviceCallback)} - * or - * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)} - * , but can be also unsolicited in case e.g. Bluetooth was turned off in - * which case application is unregistered automatically. + * Callback called when application registration state changes. Usually it's called due to + * either {@link BluetoothHidDevice#registerApp (String, String, String, byte, byte[], + * BluetoothHidDeviceCallback)} or {@link BluetoothHidDevice#unregisterApp()} , but can be also + * unsolicited in case e.g. Bluetooth was turned off in which case application is unregistered + * automatically. * * @param pluggedDevice {@link BluetoothDevice} object which represents host that currently has - * Virtual Cable established with device. Only valid when application is registered, can be - * null. - * @param config {@link BluetoothHidDeviceAppConfiguration} object which represents token - * required to unregister application using - * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)}. + * Virtual Cable established with device. Only valid when application is registered, can be + * null. * @param registered true if application is registered, false - * otherwise. + * otherwise. */ - public void onAppStatusChanged(BluetoothDevice pluggedDevice, - BluetoothHidDeviceAppConfiguration config, boolean registered) { - Log.d(TAG, "onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered=" - + registered); + public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { + Log.d(TAG, + "onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered=" + registered); } /** - * Callback called when connection state with remote host was changed. - * Application can assume than Virtual Cable is established when called with - * {@link BluetoothProfile#STATE_CONNECTED} state. + * Callback called when connection state with remote host was changed. Application can assume + * than Virtual Cable is established when called with {@link BluetoothProfile#STATE_CONNECTED} + * state. * * @param device {@link BluetoothDevice} object representing host device which connection state - * was changed. + * was changed. * @param state Connection state as defined in {@link BluetoothProfile}. */ public void onConnectionStateChanged(BluetoothDevice device, int state) { @@ -70,14 +61,14 @@ public abstract class BluetoothHidDeviceCallback { } /** - * Callback called when GET_REPORT is received from remote host. Should be - * replied by application using - * {@link BluetoothHidDevice#replyReport(BluetoothDevice, byte, byte, byte[])}. + * Callback called when GET_REPORT is received from remote host. Should be replied by + * application using {@link BluetoothHidDevice#replyReport(BluetoothDevice, byte, byte, + * byte[])}. * * @param type Requested Report Type. * @param id Requested Report Id, can be 0 if no Report Id are defined in descriptor. * @param bufferSize Requested buffer size, application shall respond with at least given number - * of bytes. + * of bytes. */ public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) { Log.d(TAG, "onGetReport: device=" + device + " type=" + type + " id=" + id + " bufferSize=" @@ -85,9 +76,9 @@ public abstract class BluetoothHidDeviceCallback { } /** - * Callback called when SET_REPORT is received from remote host. In case - * received data are invalid, application shall respond with - * {@link BluetoothHidDevice#reportError(BluetoothDevice, byte)}. + * Callback called when SET_REPORT is received from remote host. In case received data are + * invalid, application shall respond with {@link + * BluetoothHidDevice#reportError(BluetoothDevice, byte)}. * * @param type Report Type. * @param id Report Id. @@ -98,10 +89,9 @@ public abstract class BluetoothHidDeviceCallback { } /** - * Callback called when SET_PROTOCOL is received from remote host. - * Application shall use this information to send only reports valid for - * given protocol mode. By default, - * {@link BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed. + * Callback called when SET_PROTOCOL is received from remote host. Application shall use this + * information to send only reports valid for given protocol mode. By default, {@link + * BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed. * * @param protocol Protocol Mode. */ @@ -110,22 +100,19 @@ public abstract class BluetoothHidDeviceCallback { } /** - * Callback called when report data is received over interrupt channel. - * Report Type is assumed to be - * {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}. + * Callback called when report data is received over interrupt channel. Report Type is assumed + * to be {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}. * * @param reportId Report Id. * @param data Report data. */ - public void onIntrData(BluetoothDevice device, byte reportId, byte[] data) { - Log.d(TAG, "onIntrData: device=" + device + " reportId=" + reportId); + public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) { + Log.d(TAG, "onInterruptData: device=" + device + " reportId=" + reportId); } /** - * Callback called when Virtual Cable is removed. This can be either due to - * {@link BluetoothHidDevice#unplug(BluetoothDevice)} or request from remote - * side. After this callback is received connection will be disconnected - * automatically. + * Callback called when Virtual Cable is removed. After this callback is + * received connection will be disconnected automatically. */ public void onVirtualCableUnplug(BluetoothDevice device) { Log.d(TAG, "onVirtualCableUnplug: device=" + device); diff --git a/android/bluetooth/BluetoothPbap.java b/android/bluetooth/BluetoothPbap.java index a1a9347d..79443545 100644 --- a/android/bluetooth/BluetoothPbap.java +++ b/android/bluetooth/BluetoothPbap.java @@ -25,6 +25,10 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * The Android Bluetooth API is not finalized, and *will* change. Use at your * own risk. @@ -48,11 +52,10 @@ import android.util.Log; * * @hide */ -public class BluetoothPbap { +public class BluetoothPbap implements BluetoothProfile { private static final String TAG = "BluetoothPbap"; - private static final boolean DBG = true; - private static final boolean VDBG = false; + private static final boolean DBG = false; /** * Intent used to broadcast the change in connection state of the PBAP @@ -111,9 +114,9 @@ public class BluetoothPbap { private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = new IBluetoothStateChangeCallback.Stub() { public void onBluetoothStateChange(boolean up) { - if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); + log("onBluetoothStateChange: up=" + up); if (!up) { - if (VDBG) Log.d(TAG, "Unbinding service..."); + log("Unbinding service..."); synchronized (mConnection) { try { mService = null; @@ -126,7 +129,7 @@ public class BluetoothPbap { synchronized (mConnection) { try { if (mService == null) { - if (VDBG) Log.d(TAG, "Binding service..."); + log("Binding service..."); doBind(); } } catch (Exception re) { @@ -205,47 +208,60 @@ public class BluetoothPbap { } /** - * Get the current state of the BluetoothPbap service. - * - * @return One of the STATE_ return codes, or {@link BluetoothProfile#STATE_DISCONNECTED} - * if this proxy object is currently not connected to the Pbap service. + * {@inheritDoc} */ - public int getState() { - if (VDBG) log("getState()"); + @Override + public List getConnectedDevices() { + log("getConnectedDevices()"); final IBluetoothPbap service = mService; - if (service != null) { - try { - return service.getState(); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { + if (service == null) { Log.w(TAG, "Proxy not attached to service"); - if (DBG) log(Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + try { + return service.getConnectedDevices(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + return new ArrayList(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getConnectionState(BluetoothDevice device) { + log("getConnectionState: device=" + device); + final IBluetoothPbap service = mService; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.STATE_DISCONNECTED; + } + try { + return service.getConnectionState(device); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); } return BluetoothProfile.STATE_DISCONNECTED; } /** - * Get the currently connected remote Bluetooth device (PCE). - * - * @return The remote Bluetooth device, or null if not in connected or connecting state, or if - * this proxy object is not connected to the Pbap service. + * {@inheritDoc} */ - public BluetoothDevice getClient() { - if (VDBG) log("getClient()"); + @Override + public List getDevicesMatchingConnectionStates(int[] states) { + log("getDevicesMatchingConnectionStates: states=" + Arrays.toString(states)); final IBluetoothPbap service = mService; - if (service != null) { - try { - return service.getClient(); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { + if (service == null) { Log.w(TAG, "Proxy not attached to service"); - if (DBG) log(Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + try { + return service.getDevicesMatchingConnectionStates(states); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); } - return null; + return new ArrayList(); } /** @@ -253,20 +269,9 @@ public class BluetoothPbap { * include connecting). Returns false if not connected, or if this proxy * object is not currently connected to the Pbap service. */ + // TODO: This is currently being used by SettingsLib and internal app. public boolean isConnected(BluetoothDevice device) { - if (VDBG) log("isConnected(" + device + ")"); - final IBluetoothPbap service = mService; - if (service != null) { - try { - return service.isConnected(device); - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { - Log.w(TAG, "Proxy not attached to service"); - if (DBG) log(Log.getStackTraceString(new Throwable())); - } - return false; + return getConnectionState(device) == BluetoothAdapter.STATE_CONNECTED; } /** @@ -274,47 +279,27 @@ public class BluetoothPbap { * it may soon be made asynchronous. Returns false if this proxy object is * not currently connected to the Pbap service. */ - public boolean disconnect() { - if (DBG) log("disconnect()"); + // TODO: This is currently being used by SettingsLib and will be used in the future. + // TODO: Must specify target device. Implement this in the service. + public boolean disconnect(BluetoothDevice device) { + log("disconnect()"); final IBluetoothPbap service = mService; - if (service != null) { - try { - service.disconnect(); - return true; - } catch (RemoteException e) { - Log.e(TAG, e.toString()); - } - } else { + if (service == null) { Log.w(TAG, "Proxy not attached to service"); - if (DBG) log(Log.getStackTraceString(new Throwable())); + return false; } - return false; - } - - /** - * Check class bits for possible PBAP support. - * This is a simple heuristic that tries to guess if a device with the - * given class bits might support PBAP. It is not accurate for all - * devices. It tries to err on the side of false positives. - * - * @return True if this device might support PBAP. - */ - public static boolean doesClassMatchSink(BluetoothClass btClass) { - // TODO optimize the rule - switch (btClass.getDeviceClass()) { - case BluetoothClass.Device.COMPUTER_DESKTOP: - case BluetoothClass.Device.COMPUTER_LAPTOP: - case BluetoothClass.Device.COMPUTER_SERVER: - case BluetoothClass.Device.COMPUTER_UNCATEGORIZED: - return true; - default: - return false; + try { + service.disconnect(device); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); } + return false; } private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { - if (DBG) log("Proxy object connected"); + log("Proxy object connected"); mService = IBluetoothPbap.Stub.asInterface(service); if (mServiceListener != null) { mServiceListener.onServiceConnected(BluetoothPbap.this); @@ -322,7 +307,7 @@ public class BluetoothPbap { } public void onServiceDisconnected(ComponentName className) { - if (DBG) log("Proxy object disconnected"); + log("Proxy object disconnected"); mService = null; if (mServiceListener != null) { mServiceListener.onServiceDisconnected(); @@ -331,6 +316,8 @@ public class BluetoothPbap { }; private static void log(String msg) { - Log.d(TAG, msg); + if (DBG) { + Log.d(TAG, msg); + } } } diff --git a/android/bluetooth/BluetoothProfile.java b/android/bluetooth/BluetoothProfile.java index 46a230b5..df2028a5 100644 --- a/android/bluetooth/BluetoothProfile.java +++ b/android/bluetooth/BluetoothProfile.java @@ -153,8 +153,6 @@ public interface BluetoothProfile { /** * HID Device - * - * @hide */ public static final int HID_DEVICE = 19; @@ -254,4 +252,28 @@ public interface BluetoothProfile { */ public void onServiceDisconnected(int profile); } + + /** + * Convert an integer value of connection state into human readable string + * + * @param connectionState - One of {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, + * {@link #STATE_CONNECTED}, or {@link #STATE_DISCONNECTED} + * @return a string representation of the connection state, STATE_UNKNOWN if the state + * is not defined + * @hide + */ + static String getConnectionStateName(int connectionState) { + switch (connectionState) { + case STATE_DISCONNECTED: + return "STATE_DISCONNECTED"; + case STATE_CONNECTING: + return "STATE_CONNECTING"; + case STATE_CONNECTED: + return "STATE_CONNECTED"; + case STATE_DISCONNECTING: + return "STATE_DISCONNECTING"; + default: + return "STATE_UNKNOWN"; + } + } } diff --git a/android/bluetooth/le/PeriodicAdvertisingReport.java b/android/bluetooth/le/PeriodicAdvertisingReport.java index 55c3a730..73a2e74d 100644 --- a/android/bluetooth/le/PeriodicAdvertisingReport.java +++ b/android/bluetooth/le/PeriodicAdvertisingReport.java @@ -71,7 +71,7 @@ public final class PeriodicAdvertisingReport implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSyncHandle); - dest.writeLong(mTxPower); + dest.writeInt(mTxPower); dest.writeInt(mRssi); dest.writeInt(mDataStatus); if (mData != null) { diff --git a/android/companion/DeviceFilter.java b/android/companion/DeviceFilter.java index 9b4fdfdf..10135a45 100644 --- a/android/companion/DeviceFilter.java +++ b/android/companion/DeviceFilter.java @@ -62,7 +62,11 @@ public interface DeviceFilter extends Parcelable { } /** @hide */ - @IntDef({MEDIUM_TYPE_BLUETOOTH, MEDIUM_TYPE_BLUETOOTH_LE, MEDIUM_TYPE_WIFI}) + @IntDef(prefix = { "MEDIUM_TYPE_" }, value = { + MEDIUM_TYPE_BLUETOOTH, + MEDIUM_TYPE_BLUETOOTH_LE, + MEDIUM_TYPE_WIFI + }) @Retention(RetentionPolicy.SOURCE) @interface MediumType {} } diff --git a/android/content/AsyncTaskLoader.java b/android/content/AsyncTaskLoader.java index 6e9f09cb..c44e3568 100644 --- a/android/content/AsyncTaskLoader.java +++ b/android/content/AsyncTaskLoader.java @@ -50,7 +50,8 @@ import java.util.concurrent.Executor; * * @param the data type to be loaded. * - * @deprecated Use {@link android.support.v4.content.AsyncTaskLoader} + * @deprecated Use the Support Library + * {@link android.support.v4.content.AsyncTaskLoader} */ @Deprecated public abstract class AsyncTaskLoader extends Loader { diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java index 9ccc552f..8d2e141a 100644 --- a/android/content/ContentResolver.java +++ b/android/content/ContentResolver.java @@ -335,7 +335,7 @@ public abstract class ContentResolver { public static final String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS"; /** @hide */ - @IntDef(flag = false, value = { + @IntDef(flag = false, prefix = { "QUERY_SORT_DIRECTION_" }, value = { QUERY_SORT_DIRECTION_ASCENDING, QUERY_SORT_DIRECTION_DESCENDING }) @@ -482,11 +482,10 @@ public abstract class ContentResolver { public static final int SYNC_OBSERVER_TYPE_ALL = 0x7fffffff; /** @hide */ - @IntDef(flag = true, - value = { - NOTIFY_SYNC_TO_NETWORK, - NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS - }) + @IntDef(flag = true, prefix = { "NOTIFY_" }, value = { + NOTIFY_SYNC_TO_NETWORK, + NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS + }) @Retention(RetentionPolicy.SOURCE) public @interface NotifyFlags {} diff --git a/android/content/Context.java b/android/content/Context.java index 19e24ad5..4cedeaa0 100644 --- a/android/content/Context.java +++ b/android/content/Context.java @@ -51,6 +51,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.StatFs; @@ -75,6 +76,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; /** * Interface to global information about an application environment. This is @@ -219,17 +221,16 @@ public abstract class Context { public static final int MODE_NO_LOCALIZED_COLLATORS = 0x0010; /** @hide */ - @IntDef(flag = true, - value = { - BIND_AUTO_CREATE, - BIND_DEBUG_UNBIND, - BIND_NOT_FOREGROUND, - BIND_ABOVE_CLIENT, - BIND_ALLOW_OOM_MANAGEMENT, - BIND_WAIVE_PRIORITY, - BIND_IMPORTANT, - BIND_ADJUST_WITH_ACTIVITY - }) + @IntDef(flag = true, prefix = { "BIND_" }, value = { + BIND_AUTO_CREATE, + BIND_DEBUG_UNBIND, + BIND_NOT_FOREGROUND, + BIND_ABOVE_CLIENT, + BIND_ALLOW_OOM_MANAGEMENT, + BIND_WAIVE_PRIORITY, + BIND_IMPORTANT, + BIND_ADJUST_WITH_ACTIVITY + }) @Retention(RetentionPolicy.SOURCE) public @interface BindServiceFlags {} @@ -322,6 +323,15 @@ public abstract class Context { */ public static final int BIND_ADJUST_WITH_ACTIVITY = 0x0080; + /** + * @hide Flag for {@link #bindService}: allows binding to a service provided + * by an instant app. Note that the caller may not have access to the instant + * app providing the service which is a violation of the instant app sandbox. + * This flag is intended ONLY for development/testing and should be used with + * great care. Only the system is allowed to use this flag. + */ + public static final int BIND_ALLOW_INSTANT = 0x00400000; + /** * @hide Flag for {@link #bindService}: like {@link #BIND_NOT_FOREGROUND}, but puts it * up in to the important background state (instead of transient). @@ -404,10 +414,9 @@ public abstract class Context { public static final int BIND_EXTERNAL_SERVICE = 0x80000000; /** @hide */ - @IntDef(flag = true, - value = { - RECEIVER_VISIBLE_TO_INSTANT_APPS - }) + @IntDef(flag = true, prefix = { "RECEIVER_VISIBLE_" }, value = { + RECEIVER_VISIBLE_TO_INSTANT_APPS + }) @Retention(RetentionPolicy.SOURCE) public @interface RegisterReceiverFlags {} @@ -461,6 +470,16 @@ public abstract class Context { */ public abstract Looper getMainLooper(); + /** + * Return an {@link Executor} that will run enqueued tasks on the main + * thread associated with this context. This is the thread used to dispatch + * calls to application components (activities, services, etc). + */ + public Executor getMainExecutor() { + // This is pretty inefficient, which is why ContextImpl overrides it + return new HandlerExecutor(new Handler(getMainLooper())); + } + /** * Return the context of the single, global Application object of the * current process. This generally should only be used if you need a @@ -754,6 +773,8 @@ public abstract class Context { * to any callers for the same name, meaning they will see each other's * edits as soon as they are made. * + * This method is thead-safe. + * * @param name Desired preferences file. If a preferences file by this name * does not exist, it will be created when you retrieve an * editor (SharedPreferences.edit()) and then commit changes (Editor.commit()). @@ -2799,10 +2820,17 @@ public abstract class Context { * example, if this Context is an Activity that is stopped, the service will * not be required to continue running until the Activity is resumed. * - *

    This function will throw {@link SecurityException} if you do not + *

    If the service does not support binding, it may return {@code null} from + * its {@link android.app.Service#onBind(Intent) onBind()} method. If it does, then + * the ServiceConnection's + * {@link ServiceConnection#onNullBinding(ComponentName) onNullBinding()} method + * will be invoked instead of + * {@link ServiceConnection#onServiceConnected(ComponentName, IBinder) onServiceConnected()}. + * + *

    This method will throw {@link SecurityException} if the calling app does not * have permission to bind to the given service. * - *

    Note: this method can not be called from a + *

    Note: this method cannot be called from a * {@link BroadcastReceiver} component. A pattern you can use to * communicate from a BroadcastReceiver to a Service is to call * {@link #startService} with the arguments containing the command to be @@ -2825,8 +2853,8 @@ public abstract class Context { * {@link #BIND_WAIVE_PRIORITY}. * @return If you have successfully bound to the service, {@code true} is returned; * {@code false} is returned if the connection is not made so you will not - * receive the service object. However, you should still call - * {@link #unbindService} to release the connection. + * receive the service object. You should still call {@link #unbindService} + * to release the connection even if this method returned {@code false}. * * @throws SecurityException If the caller does not have permission to access the service * or the service can not be found. @@ -2904,7 +2932,7 @@ public abstract class Context { @Nullable String profileFile, @Nullable Bundle arguments); /** @hide */ - @StringDef({ + @StringDef(suffix = { "_SERVICE" }, value = { POWER_SERVICE, WINDOW_SERVICE, LAYOUT_INFLATER_SERVICE, @@ -2938,7 +2966,7 @@ public abstract class Context { //@hide: LOWPAN_SERVICE, //@hide: WIFI_RTT_SERVICE, //@hide: ETHERNET_SERVICE, - WIFI_RTT_SERVICE, + WIFI_RTT_RANGING_SERVICE, NSD_SERVICE, AUDIO_SERVICE, FINGERPRINT_SERVICE, @@ -2993,7 +3021,8 @@ public abstract class Context { SYSTEM_HEALTH_SERVICE, //@hide: INCIDENT_SERVICE, //@hide: STATS_COMPANION_SERVICE, - COMPANION_DEVICE_SERVICE + COMPANION_DEVICE_SERVICE, + CROSS_PROFILE_APPS_SERVICE }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -3072,6 +3101,14 @@ public abstract class Context { * service objects between various different contexts (Activities, Applications, * Services, Providers, etc.) * + *

    Note: Instant apps, for which {@link PackageManager#isInstantApp()} returns true, + * don't have access to the following system services: {@link #DEVICE_POLICY_SERVICE}, + * {@link #FINGERPRINT_SERVICE}, {@link #SHORTCUT_SERVICE}, {@link #USB_SERVICE}, + * {@link #WALLPAPER_SERVICE}, {@link #WIFI_P2P_SERVICE}, {@link #WIFI_SERVICE}, + * {@link #WIFI_AWARE_SERVICE}. For these services this method will return null. + * Generally, if you are running as an instant app you should always check whether the result + * of this method is null. + * * @param name The name of the desired service. * * @return The service or null if the name does not exist. @@ -3155,6 +3192,14 @@ public abstract class Context { * Services, Providers, etc.) *

    * + *

    Note: Instant apps, for which {@link PackageManager#isInstantApp()} returns true, + * don't have access to the following system services: {@link #DEVICE_POLICY_SERVICE}, + * {@link #FINGERPRINT_SERVICE}, {@link #SHORTCUT_SERVICE}, {@link #USB_SERVICE}, + * {@link #WALLPAPER_SERVICE}, {@link #WIFI_P2P_SERVICE}, {@link #WIFI_SERVICE}, + * {@link #WIFI_AWARE_SERVICE}. For these services this method will return null. + * Generally, if you are running as an instant app you should always check whether the result + * of this method is null. + * * @param serviceClass The class of the desired service. * @return The service or null if the class is not a supported system service. */ @@ -3403,6 +3448,14 @@ public abstract class Context { */ public static final String NETWORKMANAGEMENT_SERVICE = "network_management"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link com.android.server.slice.SliceManagerService} for managing slices. + * @hide + * @see #getSystemService + */ + public static final String SLICE_SERVICE = "slice"; + /** * Use with {@link #getSystemService} to retrieve a {@link * android.app.usage.NetworkStatsManager} for querying network usage stats. @@ -3479,7 +3532,7 @@ public abstract class Context { * @see android.net.wifi.rtt.WifiRttManager * @hide */ - public static final String WIFI_RTT2_SERVICE = "rttmanager2"; + public static final String WIFI_RTT_RANGING_SERVICE = "rttmanager2"; /** * Use with {@link #getSystemService} to retrieve a {@link diff --git a/android/content/ContextWrapper.java b/android/content/ContextWrapper.java index 85acdc6b..67de4fe6 100644 --- a/android/content/ContextWrapper.java +++ b/android/content/ContextWrapper.java @@ -45,6 +45,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.Executor; /** * Proxying implementation of Context that simply delegates all of its calls to @@ -103,7 +104,12 @@ public class ContextWrapper extends Context { public Looper getMainLooper() { return mBase.getMainLooper(); } - + + @Override + public Executor getMainExecutor() { + return mBase.getMainExecutor(); + } + @Override public Context getApplicationContext() { return mBase.getApplicationContext(); diff --git a/android/content/CursorLoader.java b/android/content/CursorLoader.java index 7f24c51d..5a08636c 100644 --- a/android/content/CursorLoader.java +++ b/android/content/CursorLoader.java @@ -39,7 +39,8 @@ import java.util.Arrays; * {@link #setSelectionArgs(String[])}, {@link #setSortOrder(String)}, * and {@link #setProjection(String[])}. * - * @deprecated Use {@link android.support.v4.content.CursorLoader} + * @deprecated Use the Support Library + * {@link android.support.v4.content.CursorLoader} */ @Deprecated public class CursorLoader extends AsyncTaskLoader { diff --git a/android/content/Intent.java b/android/content/Intent.java index bad452ce..e940769a 100644 --- a/android/content/Intent.java +++ b/android/content/Intent.java @@ -4454,12 +4454,36 @@ public class Intent implements Parcelable, Cloneable { */ public static final String EXTRA_EPHEMERAL_TOKEN = "android.intent.extra.EPHEMERAL_TOKEN"; + /** + * The action that triggered an instant application resolution. + * @hide + */ + public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION"; + + /** + * A {@link Bundle} of metadata that describes the instanta application that needs to be + * installed. This data is populated from the response to + * {@link android.content.pm.InstantAppResolveInfo#getExtras()} as provided by the registered + * instant application resolver. + * @hide + */ + public static final String EXTRA_INSTANT_APP_EXTRAS = + "android.intent.extra.INSTANT_APP_EXTRAS"; + /** * The version code of the app to install components from. + * @deprecated Use {@link #EXTRA_LONG_VERSION_CODE). * @hide */ + @Deprecated public static final String EXTRA_VERSION_CODE = "android.intent.extra.VERSION_CODE"; + /** + * The version code of the app to install components from. + * @hide + */ + public static final String EXTRA_LONG_VERSION_CODE = "android.intent.extra.LONG_VERSION_CODE"; + /** * The app that triggered the ephemeral installation. * @hide @@ -4891,8 +4915,9 @@ public class Intent implements Parcelable, Cloneable { *

  • Enumeration of features here is not meant to restrict capabilities of the quick viewer. * Quick viewer can implement features not listed below. *
  • Features included at this time are: {@link QuickViewConstants#FEATURE_VIEW}, - * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DOWNLOAD}, - * {@link QuickViewConstants#FEATURE_SEND}, {@link QuickViewConstants#FEATURE_PRINT}. + * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DELETE}, + * {@link QuickViewConstants#FEATURE_DOWNLOAD}, {@link QuickViewConstants#FEATURE_SEND}, + * {@link QuickViewConstants#FEATURE_PRINT}. *

    * Requirements: *

  • Quick viewer shouldn't show a feature if the feature is absent in @@ -5665,11 +5690,14 @@ public class Intent implements Parcelable, Cloneable { private static final int COPY_MODE_HISTORY = 2; /** @hide */ - @IntDef(value = {COPY_MODE_ALL, COPY_MODE_FILTER, COPY_MODE_HISTORY}) + @IntDef(prefix = { "COPY_MODE_" }, value = { + COPY_MODE_ALL, + COPY_MODE_FILTER, + COPY_MODE_HISTORY + }) @Retention(RetentionPolicy.SOURCE) public @interface CopyMode {} - /** * Create an empty intent. */ @@ -8941,17 +8969,16 @@ public class Intent implements Parcelable, Cloneable { } /** @hide */ - @IntDef(flag = true, - value = { - FILL_IN_ACTION, - FILL_IN_DATA, - FILL_IN_CATEGORIES, - FILL_IN_COMPONENT, - FILL_IN_PACKAGE, - FILL_IN_SOURCE_BOUNDS, - FILL_IN_SELECTOR, - FILL_IN_CLIP_DATA - }) + @IntDef(flag = true, prefix = { "FILL_IN_" }, value = { + FILL_IN_ACTION, + FILL_IN_DATA, + FILL_IN_CATEGORIES, + FILL_IN_COMPONENT, + FILL_IN_PACKAGE, + FILL_IN_SOURCE_BOUNDS, + FILL_IN_SELECTOR, + FILL_IN_CLIP_DATA + }) @Retention(RetentionPolicy.SOURCE) public @interface FillInFlags {} diff --git a/android/content/Loader.java b/android/content/Loader.java index 80f9a14c..b0555d4c 100644 --- a/android/content/Loader.java +++ b/android/content/Loader.java @@ -49,7 +49,8 @@ import java.io.PrintWriter; * * @param The result returned when the load is complete * - * @deprecated Use {@link android.support.v4.content.Loader} + * @deprecated Use the Support Library + * {@link android.support.v4.content.Loader} */ @Deprecated public class Loader { @@ -561,4 +562,4 @@ public class Loader { writer.print(" mReset="); writer.println(mReset); } } -} \ No newline at end of file +} diff --git a/android/content/QuickViewConstants.java b/android/content/QuickViewConstants.java index 7455d0cb..a25513de 100644 --- a/android/content/QuickViewConstants.java +++ b/android/content/QuickViewConstants.java @@ -33,7 +33,7 @@ public class QuickViewConstants { public static final String FEATURE_VIEW = "android:view"; /** - * Feature to view a document using system standard editing mechanism, like + * Feature to edit a document using system standard editing mechanism, like * {@link Intent#ACTION_EDIT}. * * @see Intent#EXTRA_QUICK_VIEW_FEATURES @@ -41,6 +41,15 @@ public class QuickViewConstants { */ public static final String FEATURE_EDIT = "android:edit"; + /** + * Feature to delete an individual document. Quick viewer implementations must use + * Storage Access Framework to both verify delete permission and to delete content. + * + * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE + * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri) + */ + public static final String FEATURE_DELETE = "android:delete"; + /** * Feature to view a document using system standard sending mechanism, like * {@link Intent#ACTION_SEND}. diff --git a/android/content/ServiceConnection.java b/android/content/ServiceConnection.java index 6ff49002..c16dbbe3 100644 --- a/android/content/ServiceConnection.java +++ b/android/content/ServiceConnection.java @@ -63,4 +63,21 @@ public interface ServiceConnection { */ default void onBindingDied(ComponentName name) { } + + /** + * Called when the service being bound has returned {@code null} from its + * {@link android.app.Service#onBind(Intent) onBind()} method. This indicates + * that the attempting service binding represented by this ServiceConnection + * will never become usable. + * + *

    The app which requested the binding must still call + * {@link Context#unbindService(ServiceConnection)} to release the tracking + * resources associated with this ServiceConnection even if this callback was + * invoked following {@link Context#bindService Context.bindService() bindService()}. + * + * @param name The concrete component name of the service whose binding + * has been rejected by the Service implementation. + */ + default void onNullBinding(ComponentName name) { + } } diff --git a/android/content/pm/ActivityInfo.java b/android/content/pm/ActivityInfo.java index f8cdce64..14617116 100644 --- a/android/content/pm/ActivityInfo.java +++ b/android/content/pm/ActivityInfo.java @@ -262,10 +262,10 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final int COLOR_MODE_HDR = 2; /** @hide */ - @IntDef({ - COLOR_MODE_DEFAULT, - COLOR_MODE_WIDE_COLOR_GAMUT, - COLOR_MODE_HDR, + @IntDef(prefix = { "COLOR_MODE_" }, value = { + COLOR_MODE_DEFAULT, + COLOR_MODE_WIDE_COLOR_GAMUT, + COLOR_MODE_HDR, }) @Retention(RetentionPolicy.SOURCE) public @interface ColorMode {} @@ -492,7 +492,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public int flags; /** @hide */ - @IntDef({ + @IntDef(prefix = { "SCREEN_ORIENTATION_" }, value = { SCREEN_ORIENTATION_UNSET, SCREEN_ORIENTATION_UNSPECIFIED, SCREEN_ORIENTATION_LANDSCAPE, @@ -638,25 +638,24 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED; /** @hide */ - @IntDef(flag = true, - value = { - CONFIG_MCC, - CONFIG_MNC, - CONFIG_LOCALE, - CONFIG_TOUCHSCREEN, - CONFIG_KEYBOARD, - CONFIG_KEYBOARD_HIDDEN, - CONFIG_NAVIGATION, - CONFIG_ORIENTATION, - CONFIG_SCREEN_LAYOUT, - CONFIG_UI_MODE, - CONFIG_SCREEN_SIZE, - CONFIG_SMALLEST_SCREEN_SIZE, - CONFIG_DENSITY, - CONFIG_LAYOUT_DIRECTION, - CONFIG_COLOR_MODE, - CONFIG_FONT_SCALE, - }) + @IntDef(flag = true, prefix = { "CONFIG_" }, value = { + CONFIG_MCC, + CONFIG_MNC, + CONFIG_LOCALE, + CONFIG_TOUCHSCREEN, + CONFIG_KEYBOARD, + CONFIG_KEYBOARD_HIDDEN, + CONFIG_NAVIGATION, + CONFIG_ORIENTATION, + CONFIG_SCREEN_LAYOUT, + CONFIG_UI_MODE, + CONFIG_SCREEN_SIZE, + CONFIG_SMALLEST_SCREEN_SIZE, + CONFIG_DENSITY, + CONFIG_LAYOUT_DIRECTION, + CONFIG_COLOR_MODE, + CONFIG_FONT_SCALE, + }) @Retention(RetentionPolicy.SOURCE) public @interface Config {} diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java index edb27cd4..15e119b2 100644 --- a/android/content/pm/ApplicationInfo.java +++ b/android/content/pm/ApplicationInfo.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; @@ -594,6 +595,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int PRIVATE_FLAG_OEM = 1 << 17; + /** + * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * vendor partition of the system image. + * @hide + */ + public static final int PRIVATE_FLAG_VENDOR = 1 << 18; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = { PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, @@ -613,6 +621,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { PRIVATE_FLAG_PRIVILEGED, PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, PRIVATE_FLAG_STATIC_SHARED_LIBRARY, + PRIVATE_FLAG_VENDOR, PRIVATE_FLAG_VIRTUAL_PRELOAD, }) @Retention(RetentionPolicy.SOURCE) @@ -888,7 +897,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * The app's declared version code. * @hide */ - public int versionCode; + public long versionCode; /** * The user-visible SDK version (ex. 26) of the framework against which the application claims @@ -943,6 +952,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public int targetSandboxVersion; + /** + * The factory of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifestApplication_appComponentFactory} + * attribute. + */ + public String appComponentFactory; + /** * The category of this app. Categories are used to cluster multiple apps * together into meaningful groups, such as when summarizing battery, @@ -1259,6 +1275,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { targetSandboxVersion = orig.targetSandboxVersion; classLoaderName = orig.classLoaderName; splitClassLoaderNames = orig.splitClassLoaderNames; + appComponentFactory = orig.appComponentFactory; } public String toString() { @@ -1315,7 +1332,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(uid); dest.writeInt(minSdkVersion); dest.writeInt(targetSdkVersion); - dest.writeInt(versionCode); + dest.writeLong(versionCode); dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabledSetting); dest.writeInt(installLocation); @@ -1331,6 +1348,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeStringArray(splitClassLoaderNames); dest.writeInt(compileSdkVersion); dest.writeString(compileSdkVersionCodename); + dest.writeString(appComponentFactory); } public static final Parcelable.Creator CREATOR @@ -1384,7 +1402,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uid = source.readInt(); minSdkVersion = source.readInt(); targetSdkVersion = source.readInt(); - versionCode = source.readInt(); + versionCode = source.readLong(); enabled = source.readInt() != 0; enabledSetting = source.readInt(); installLocation = source.readInt(); @@ -1400,6 +1418,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { splitClassLoaderNames = source.readStringArray(); compileSdkVersion = source.readInt(); compileSdkVersionCodename = source.readString(); + appComponentFactory = source.readString(); } /** @@ -1569,6 +1588,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { return (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; } + /** @hide */ + public boolean isVendor() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0; + } + + /** @hide */ + public boolean isTargetingDeprecatedSdkVersion() { + return targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT; + } + /** * Returns whether or not this application was installed as a virtual preload. */ diff --git a/android/content/pm/AuxiliaryResolveInfo.java b/android/content/pm/AuxiliaryResolveInfo.java index 067363d4..6bdcefbe 100644 --- a/android/content/pm/AuxiliaryResolveInfo.java +++ b/android/content/pm/AuxiliaryResolveInfo.java @@ -45,7 +45,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter { /** Opaque token to track the instant application resolution */ public final String token; /** The version code of the package */ - public final int versionCode; + public final long versionCode; /** An intent to start upon failure to install */ public final Intent failureIntent; @@ -71,7 +71,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter { public AuxiliaryResolveInfo(@NonNull String packageName, @Nullable String splitName, @Nullable ComponentName failureActivity, - int versionCode, + long versionCode, @Nullable Intent failureIntent) { super(); this.packageName = packageName; diff --git a/android/content/pm/InstantAppResolveInfo.java b/android/content/pm/InstantAppResolveInfo.java index 22e994f4..19cb9323 100644 --- a/android/content/pm/InstantAppResolveInfo.java +++ b/android/content/pm/InstantAppResolveInfo.java @@ -19,8 +19,7 @@ package android.content.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.content.IntentFilter; -import android.net.Uri; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -44,10 +43,18 @@ public final class InstantAppResolveInfo implements Parcelable { /** The filters used to match domain */ private final List mFilters; /** The version code of the app that this class resolves to */ - private final int mVersionCode; + private final long mVersionCode; + /** Data about the app that should be passed along to the Instant App installer on resolve */ + private final Bundle mExtras; public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, @Nullable List filters, int versionCode) { + this(digest, packageName, filters, (long) versionCode, null /* extras */); + } + + public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, + @Nullable List filters, long versionCode, + @Nullable Bundle extras) { // validate arguments if ((packageName == null && (filters != null && filters.size() != 0)) || (packageName != null && (filters == null || filters.size() == 0))) { @@ -62,11 +69,13 @@ public final class InstantAppResolveInfo implements Parcelable { } mPackageName = packageName; mVersionCode = versionCode; + mExtras = extras; } public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName, @Nullable List filters) { - this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/); + this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/, + null /* extras */); } InstantAppResolveInfo(Parcel in) { @@ -74,7 +83,8 @@ public final class InstantAppResolveInfo implements Parcelable { mPackageName = in.readString(); mFilters = new ArrayList(); in.readList(mFilters, null /*loader*/); - mVersionCode = in.readInt(); + mVersionCode = in.readLong(); + mExtras = in.readBundle(); } public byte[] getDigestBytes() { @@ -93,10 +103,23 @@ public final class InstantAppResolveInfo implements Parcelable { return mFilters; } + /** + * @deprecated Use {@link #getLongVersionCode} instead. + */ + @Deprecated public int getVersionCode() { + return (int) (mVersionCode & 0xffffffff); + } + + public long getLongVersionCode() { return mVersionCode; } + @Nullable + public Bundle getExtras() { + return mExtras; + } + @Override public int describeContents() { return 0; @@ -107,7 +130,8 @@ public final class InstantAppResolveInfo implements Parcelable { out.writeParcelable(mDigest, flags); out.writeString(mPackageName); out.writeList(mFilters); - out.writeInt(mVersionCode); + out.writeLong(mVersionCode); + out.writeBundle(mExtras); } public static final Parcelable.Creator CREATOR diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java index 9e54e235..b4a7eec0 100644 --- a/android/content/pm/LauncherApps.java +++ b/android/content/pm/LauncherApps.java @@ -265,6 +265,14 @@ public class LauncherApps { /** * Include pinned shortcuts in the result. + * + *

    If you are the selected assistant app, and wishes to fetch all shortcuts that the + * user owns on the launcher (or by other launchers, in case the user has multiple), use + * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead. + * + *

    If you're a regular launcher app, there's no way to get shortcuts pinned by other + * launchers, and {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} will be ignored. So use this + * flag to get own pinned shortcuts. */ public static final int FLAG_MATCH_PINNED = 1 << 1; @@ -285,8 +293,15 @@ public class LauncherApps { * Include all pinned shortcuts by any launchers, not just by the caller, * in the result. * - * The caller must be the selected assistant app to use this flag, or have the system + *

    The caller must be the selected assistant app to use this flag, or have the system * {@code ACCESS_SHORTCUTS} permission. + * + *

    If you are the selected assistant app, and wishes to fetch all shortcuts that the + * user owns on the launcher (or by other launchers, in case the user has multiple), use + * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead. + * + *

    If you're a regular launcher app (or any app that's not the selected assistant app) + * then this flag will be ignored. */ public static final int FLAG_MATCH_PINNED_BY_ANY_LAUNCHER = 1 << 10; @@ -328,14 +343,13 @@ public class LauncherApps { public static final int FLAG_GET_KEY_FIELDS_ONLY = 1 << 2; /** @hide */ - @IntDef(flag = true, - value = { - FLAG_MATCH_DYNAMIC, - FLAG_MATCH_PINNED, - FLAG_MATCH_MANIFEST, - FLAG_GET_KEY_FIELDS_ONLY, - FLAG_MATCH_MANIFEST, - }) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_MATCH_DYNAMIC, + FLAG_MATCH_PINNED, + FLAG_MATCH_MANIFEST, + FLAG_GET_KEY_FIELDS_ONLY, + FLAG_MATCH_MANIFEST, + }) @Retention(RetentionPolicy.SOURCE) public @interface QueryFlags {} @@ -1365,7 +1379,10 @@ public class LauncherApps { public static final int REQUEST_TYPE_APPWIDGET = 2; /** @hide */ - @IntDef(value = {REQUEST_TYPE_SHORTCUT}) + @IntDef(prefix = { "REQUEST_TYPE_" }, value = { + REQUEST_TYPE_SHORTCUT, + REQUEST_TYPE_APPWIDGET + }) @Retention(RetentionPolicy.SOURCE) public @interface RequestType {} diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java index f8889b68..5a91e947 100644 --- a/android/content/pm/PackageInfo.java +++ b/android/content/pm/PackageInfo.java @@ -16,10 +16,14 @@ package android.content.pm; +import android.annotation.IntDef; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Overall information about the contents of a package. This corresponds * to all of the information collected from AndroidManifest.xml. @@ -37,12 +41,55 @@ public class PackageInfo implements Parcelable { public String[] splitNames; /** + * @deprecated Use {@link #getLongVersionCode()} instead, which includes both + * this and the additional + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} attribute. * The version number of this package, as specified by the <manifest> * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCode} * attribute. + * @see #getLongVersionCode() */ + @Deprecated public int versionCode; + /** + * @hide + * The major version number of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor} + * attribute. + * @see #getLongVersionCode() + */ + public int versionCodeMajor; + + /** + * Return {@link android.R.styleable#AndroidManifest_versionCode versionCode} and + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} combined + * together as a single long value. The + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} is placed in + * the upper 32 bits. + */ + public long getLongVersionCode() { + return composeLongVersionCode(versionCodeMajor, versionCode); + } + + /** + * Set the full version code in this PackageInfo, updating {@link #versionCode} + * with the lower bits. + * @see #getLongVersionCode() + */ + public void setLongVersionCode(long longVersionCode) { + versionCodeMajor = (int) (longVersionCode>>32); + versionCode = (int) longVersionCode; + } + + /** + * @hide Internal implementation for composing a minor and major version code in to + * a single long version code. + */ + public static long composeLongVersionCode(int major, int minor) { + return (((long) major) << 32) | (((long) minor) & 0xffffffffL); + } + /** * The version name of this package, as specified by the <manifest> * tag's {@link android.R.styleable#AndroidManifest_versionName versionName} @@ -287,8 +334,29 @@ public class PackageInfo implements Parcelable { /** @hide */ public int overlayPriority; - /** @hide */ - public boolean isStaticOverlay; + /** + * Flag for use with {@link #mOverlayFlags}. Marks the overlay as static, meaning it cannot + * be enabled/disabled at runtime. + */ + static final int FLAG_OVERLAY_STATIC = 1 << 1; + + /** + * Flag for use with {@link #mOverlayFlags}. Marks the overlay as trusted (not 3rd party). + */ + static final int FLAG_OVERLAY_TRUSTED = 1 << 2; + + @IntDef(flag = true, prefix = "FLAG_OVERLAY_", value = { + FLAG_OVERLAY_STATIC, + FLAG_OVERLAY_TRUSTED + }) + @Retention(RetentionPolicy.SOURCE) + @interface OverlayFlags {} + + /** + * Modifiers that affect the state of this overlay. See {@link #FLAG_OVERLAY_STATIC}, + * {@link #FLAG_OVERLAY_TRUSTED}. + */ + @OverlayFlags int mOverlayFlags; /** * The user-visible SDK version (ex. 26) of the framework against which the application claims @@ -316,6 +384,23 @@ public class PackageInfo implements Parcelable { public PackageInfo() { } + /** + * Returns true if the package is a valid Runtime Overlay package. + * @hide + */ + public boolean isOverlayPackage() { + return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_TRUSTED) != 0; + } + + /** + * Returns true if the package is a valid static Runtime Overlay package. Static overlays + * are not updatable outside of a system update and are safe to load in the system process. + * @hide + */ + public boolean isStaticOverlayPackage() { + return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_STATIC) != 0; + } + @Override public String toString() { return "PackageInfo{" @@ -333,6 +418,7 @@ public class PackageInfo implements Parcelable { dest.writeString(packageName); dest.writeStringArray(splitNames); dest.writeInt(versionCode); + dest.writeInt(versionCodeMajor); dest.writeString(versionName); dest.writeInt(baseRevisionCode); dest.writeIntArray(splitRevisionCodes); @@ -366,8 +452,8 @@ public class PackageInfo implements Parcelable { dest.writeString(restrictedAccountType); dest.writeString(requiredAccountType); dest.writeString(overlayTarget); - dest.writeInt(isStaticOverlay ? 1 : 0); dest.writeInt(overlayPriority); + dest.writeInt(mOverlayFlags); dest.writeInt(compileSdkVersion); dest.writeString(compileSdkVersionCodename); } @@ -389,6 +475,7 @@ public class PackageInfo implements Parcelable { packageName = source.readString(); splitNames = source.createStringArray(); versionCode = source.readInt(); + versionCodeMajor = source.readInt(); versionName = source.readString(); baseRevisionCode = source.readInt(); splitRevisionCodes = source.createIntArray(); @@ -420,8 +507,8 @@ public class PackageInfo implements Parcelable { restrictedAccountType = source.readString(); requiredAccountType = source.readString(); overlayTarget = source.readString(); - isStaticOverlay = source.readInt() != 0; overlayPriority = source.readInt(); + mOverlayFlags = source.readInt(); compileSdkVersion = source.readInt(); compileSdkVersionCodename = source.readString(); diff --git a/android/content/pm/PackageInfoLite.java b/android/content/pm/PackageInfoLite.java index 1efe082b..bbf020d7 100644 --- a/android/content/pm/PackageInfoLite.java +++ b/android/content/pm/PackageInfoLite.java @@ -38,9 +38,27 @@ public class PackageInfoLite implements Parcelable { /** * The android:versionCode of the package. + * @deprecated Use {@link #getLongVersionCode()} instead, which includes both + * this and the additional + * {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor} attribute. */ + @Deprecated public int versionCode; + /** + * @hide + * The android:versionCodeMajor of the package. + */ + public int versionCodeMajor; + + /** + * Return {@link #versionCode} and {@link #versionCodeMajor} combined together as a + * single long value. The {@link #versionCodeMajor} is placed in the upper 32 bits. + */ + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } + /** Revision code of base APK */ public int baseRevisionCode; /** Revision codes of any split APKs, ordered by parsed splitName */ @@ -55,10 +73,10 @@ public class PackageInfoLite implements Parcelable { /** * Specifies the recommended install location. Can be one of - * {@link #PackageHelper.RECOMMEND_INSTALL_INTERNAL} to install on internal storage - * {@link #PackageHelper.RECOMMEND_INSTALL_EXTERNAL} to install on external media - * {@link PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors - * {@link PackageHelper.RECOMMEND_FAILED_INVALID_APK} for parse errors. + * {@link PackageHelper#RECOMMEND_INSTALL_INTERNAL} to install on internal storage, + * {@link PackageHelper#RECOMMEND_INSTALL_EXTERNAL} to install on external media, + * {@link PackageHelper#RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors, + * or {@link PackageHelper#RECOMMEND_FAILED_INVALID_APK} for parse errors. */ public int recommendedInstallLocation; public int installLocation; @@ -82,6 +100,7 @@ public class PackageInfoLite implements Parcelable { dest.writeString(packageName); dest.writeStringArray(splitNames); dest.writeInt(versionCode); + dest.writeInt(versionCodeMajor); dest.writeInt(baseRevisionCode); dest.writeIntArray(splitRevisionCodes); dest.writeInt(recommendedInstallLocation); @@ -111,6 +130,7 @@ public class PackageInfoLite implements Parcelable { packageName = source.readString(); splitNames = source.createStringArray(); versionCode = source.readInt(); + versionCodeMajor = source.readInt(); baseRevisionCode = source.readInt(); splitRevisionCodes = source.createIntArray(); recommendedInstallLocation = source.readInt(); diff --git a/android/content/pm/PackageList.java b/android/content/pm/PackageList.java new file mode 100644 index 00000000..cfd99abc --- /dev/null +++ b/android/content/pm/PackageList.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PackageManagerInternal.PackageListObserver; + +import com.android.server.LocalServices; + +import java.util.List; + +/** + * All of the package name installed on the system. + *

    A self observable list that automatically removes the listener when it goes out of scope. + * + * @hide Only for use within the system server. + */ +public class PackageList implements PackageListObserver, AutoCloseable { + private final PackageListObserver mWrappedObserver; + private final List mPackageNames; + + /** + * Create a new object. + *

    Ownership of the given {@link List} transfers to this object and should not + * be modified by the caller. + */ + public PackageList(@NonNull List packageNames, @Nullable PackageListObserver observer) { + mPackageNames = packageNames; + mWrappedObserver = observer; + } + + @Override + public void onPackageAdded(String packageName) { + if (mWrappedObserver != null) { + mWrappedObserver.onPackageAdded(packageName); + } + } + + @Override + public void onPackageRemoved(String packageName) { + if (mWrappedObserver != null) { + mWrappedObserver.onPackageRemoved(packageName); + } + } + + @Override + public void close() throws Exception { + LocalServices.getService(PackageManagerInternal.class).removePackageListObserver(this); + } + + /** + * Returns the names of packages installed on the system. + *

    The list is a copy-in-time and the actual set of installed packages may differ. Real + * time updates to the package list are sent via the {@link PackageListObserver} callback. + */ + public @NonNull List getPackageNames() { + return mPackageNames; + } +} diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java index f796aabe..2d726329 100644 --- a/android/content/pm/PackageManager.java +++ b/android/content/pm/PackageManager.java @@ -42,6 +42,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.PackageParser.PackageParserException; +import android.content.pm.dex.ArtManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Rect; @@ -2073,6 +2074,13 @@ public abstract class PackageManager { @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TELEPHONY_EUICC = "android.hardware.telephony.euicc"; + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device + * supports cell-broadcast reception using the MBMS APIs. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_TELEPHONY_MBMS = "android.hardware.telephony.mbms"; + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports connecting to USB devices @@ -2634,12 +2642,21 @@ public abstract class PackageManager { /** * Extra field name for the version code of a package pending verification. - * + * @deprecated Use {@link #EXTRA_VERIFICATION_LONG_VERSION_CODE} instead. * @hide */ + @Deprecated public static final String EXTRA_VERIFICATION_VERSION_CODE = "android.content.pm.extra.VERIFICATION_VERSION_CODE"; + /** + * Extra field name for the long version code of a package pending verification. + * + * @hide + */ + public static final String EXTRA_VERIFICATION_LONG_VERSION_CODE = + "android.content.pm.extra.VERIFICATION_LONG_VERSION_CODE"; + /** * Extra field name for the ID of a intent filter pending verification. * Passed to an intent filter verifier and is used to call back to @@ -5842,4 +5859,14 @@ public abstract class PackageManager { @SystemApi public abstract void registerDexModule(String dexModulePath, @Nullable DexModuleRegisterCallback callback); + + /** + * Returns the {@link ArtManager} associated with this package manager. + * + * @hide + */ + @SystemApi + public @NonNull ArtManager getArtManager() { + throw new UnsupportedOperationException("getArtManager not implemented in subclass"); + } } diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java index 713cd109..8ee8e102 100644 --- a/android/content/pm/PackageManagerInternal.java +++ b/android/content/pm/PackageManagerInternal.java @@ -53,6 +53,14 @@ public abstract class PackageManagerInternal { @Retention(RetentionPolicy.SOURCE) public @interface KnownPackage {} + /** Observer called whenever the list of packages changes */ + public interface PackageListObserver { + /** A package was added to the system. */ + void onPackageAdded(@NonNull String packageName); + /** A package was removed from the system. */ + void onPackageRemoved(@NonNull String packageName); + } + /** * Provider for package names. */ @@ -434,6 +442,35 @@ public abstract class PackageManagerInternal { */ public abstract @Nullable PackageParser.Package getPackage(@NonNull String packageName); + /** + * Returns a list without a change observer. + * + * {@see #getPackageList(PackageListObserver)} + */ + public @NonNull PackageList getPackageList() { + return getPackageList(null); + } + + /** + * Returns the list of packages installed at the time of the method call. + *

    The given observer is notified when the list of installed packages + * changes [eg. a package was installed or uninstalled]. It will not be + * notified if a package is updated. + *

    The package list will not be updated automatically as packages are + * installed / uninstalled. Any changes must be handled within the observer. + */ + public abstract @NonNull PackageList getPackageList(@Nullable PackageListObserver observer); + + /** + * Removes the observer. + *

    Generally not needed. {@link #getPackageList(PackageListObserver)} will automatically + * remove the observer. + *

    Does nothing if the observer isn't currently registered. + *

    Observers are notified asynchronously and it's possible for an observer to be + * invoked after its been removed. + */ + public abstract void removePackageListObserver(@NonNull PackageListObserver observer); + /** * Returns a package object for the disabled system package name. */ diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java index ebeaad78..77eb57f2 100644 --- a/android/content/pm/PackageParser.java +++ b/android/content/pm/PackageParser.java @@ -32,7 +32,6 @@ import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_ import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; -import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK; @@ -87,7 +86,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TypedValue; import android.util.apk.ApkSignatureSchemeV2Verifier; -import android.util.jar.StrictJarFile; +import android.util.apk.ApkSignatureVerifier; import android.view.Gravity; import com.android.internal.R; @@ -106,12 +105,10 @@ import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; -import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -129,8 +126,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.zip.ZipEntry; /** * Parser for package files (APKs) on disk. This supports apps packaged either @@ -173,7 +168,7 @@ public class PackageParser { // TODO: refactor "codePath" to "apkPath" /** File name in an APK for the Android manifest. */ - private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; + public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; /** Path prefix for apps on expanded storage */ private static final String MNT_EXPAND = "/mnt/expand/"; @@ -394,6 +389,7 @@ public class PackageParser { public static class PackageLite { public final String packageName; public final int versionCode; + public final int versionCodeMajor; public final int installLocation; public final VerifierInfo[] verifiers; @@ -436,6 +432,7 @@ public class PackageParser { String[] splitCodePaths, int[] splitRevisionCodes) { this.packageName = baseApk.packageName; this.versionCode = baseApk.versionCode; + this.versionCodeMajor = baseApk.versionCodeMajor; this.installLocation = baseApk.installLocation; this.verifiers = baseApk.verifiers; this.splitNames = splitNames; @@ -476,6 +473,7 @@ public class PackageParser { public final String configForSplit; public final String usesSplitName; public final int versionCode; + public final int versionCodeMajor; public final int revisionCode; public final int installLocation; public final VerifierInfo[] verifiers; @@ -489,11 +487,11 @@ public class PackageParser { public final boolean isolatedSplits; public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit, - String configForSplit, String usesSplitName, int versionCode, int revisionCode, - int installLocation, List verifiers, Signature[] signatures, - Certificate[][] certificates, boolean coreApp, boolean debuggable, - boolean multiArch, boolean use32bitAbi, boolean extractNativeLibs, - boolean isolatedSplits) { + String configForSplit, String usesSplitName, int versionCode, int versionCodeMajor, + int revisionCode, int installLocation, List verifiers, + Signature[] signatures, Certificate[][] certificates, boolean coreApp, + boolean debuggable, boolean multiArch, boolean use32bitAbi, + boolean extractNativeLibs, boolean isolatedSplits) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; @@ -501,6 +499,7 @@ public class PackageParser { this.configForSplit = configForSplit; this.usesSplitName = usesSplitName; this.versionCode = versionCode; + this.versionCodeMajor = versionCodeMajor; this.revisionCode = revisionCode; this.installLocation = installLocation; this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]); @@ -513,6 +512,10 @@ public class PackageParser { this.extractNativeLibs = extractNativeLibs; this.isolatedSplits = isolatedSplits; } + + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } } /** @@ -663,6 +666,7 @@ public class PackageParser { pi.packageName = p.packageName; pi.splitNames = p.splitNames; pi.versionCode = p.mVersionCode; + pi.versionCodeMajor = p.mVersionCodeMajor; pi.baseRevisionCode = p.baseRevisionCode; pi.splitRevisionCodes = p.splitRevisionCodes; pi.versionName = p.mVersionName; @@ -680,7 +684,15 @@ public class PackageParser { pi.requiredAccountType = p.mRequiredAccountType; pi.overlayTarget = p.mOverlayTarget; pi.overlayPriority = p.mOverlayPriority; - pi.isStaticOverlay = p.mIsStaticOverlay; + + if (p.mIsStaticOverlay) { + pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC; + } + + if (p.mTrustedOverlay) { + pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED; + } + pi.compileSdkVersion = p.mCompileSdkVersion; pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename; pi.firstInstallTime = firstInstallTime; @@ -804,23 +816,6 @@ public class PackageParser { return pi; } - private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry) - throws PackageParserException { - InputStream is = null; - try { - // We must read the stream for the JarEntry to retrieve - // its certificates. - is = jarFile.getInputStream(entry); - readFullyIgnoringContents(is); - return jarFile.getCertificateChains(entry); - } catch (IOException | RuntimeException e) { - throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, - "Failed reading " + entry.getName() + " in " + jarFile, e); - } finally { - IoUtils.closeQuietly(is); - } - } - public static final int PARSE_MUST_BE_APK = 1 << 0; public static final int PARSE_IGNORE_PROCESSES = 1 << 1; public static final int PARSE_FORWARD_LOCK = 1 << 2; @@ -1500,7 +1495,7 @@ public class PackageParser { pkg.mCertificates = certificates; try { - pkg.mSignatures = convertToSignatures(certificates); + pkg.mSignatures = ApkSignatureVerifier.convertToSignatures(certificates); } catch (CertificateEncodingException e) { // certificates weren't encoded properly; something went wrong throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, @@ -1563,157 +1558,49 @@ public class PackageParser { throws PackageParserException { final String apkPath = apkFile.getAbsolutePath(); - // Try to verify the APK using APK Signature Scheme v2. - boolean verified = false; - { - Certificate[][] allSignersCerts = null; - Signature[] signatures = null; - try { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2"); - allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath); - signatures = convertToSignatures(allSignersCerts); - // APK verified using APK Signature Scheme v2. - verified = true; - } catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) { - // No APK Signature Scheme v2 signature found - if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) { - throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "No APK Signature Scheme v2 signature in ephemeral package " + apkPath, - e); - } - // Static shared libraries must use only the V2 signing scheme - if (pkg.applicationInfo.isStaticSharedLibrary()) { - throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "Static shared libs must use v2 signature scheme " + apkPath); - } - } catch (Exception e) { - // APK Signature Scheme v2 signature was found but did not verify + int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME; + if (pkg.applicationInfo.isStaticSharedLibrary()) { + // must use v2 signing scheme + minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2; + } + ApkSignatureVerifier.Result verified; + if ((parseFlags & PARSE_IS_SYSTEM_DIR) != 0) { + // systemDir APKs are already trusted, save time by not verifying + verified = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts( + apkPath, minSignatureScheme); + } else { + verified = ApkSignatureVerifier.verify(apkPath, minSignatureScheme); + } + if (verified.signatureSchemeVersion + < ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2) { + // TODO (b/68860689): move this logic to packagemanagerserivce + if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) { throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "Failed to collect certificates from " + apkPath - + " using APK Signature Scheme v2", - e); - } finally { - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - } - - if (verified) { - if (pkg.mCertificates == null) { - pkg.mCertificates = allSignersCerts; - pkg.mSignatures = signatures; - pkg.mSigningKeys = new ArraySet<>(allSignersCerts.length); - for (int i = 0; i < allSignersCerts.length; i++) { - Certificate[] signerCerts = allSignersCerts[i]; - Certificate signerCert = signerCerts[0]; - pkg.mSigningKeys.add(signerCert.getPublicKey()); - } - } else { - if (!Signature.areExactMatch(pkg.mSignatures, signatures)) { - throw new PackageParserException( - INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, - apkPath + " has mismatched certificates"); - } - } - // Not yet done, because we need to confirm that AndroidManifest.xml exists and, - // if requested, that classes.dex exists. + "No APK Signature Scheme v2 signature in ephemeral package " + apkPath); } } - StrictJarFile jarFile = null; - try { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor"); - // Ignore signature stripping protections when verifying APKs from system partition. - // For those APKs we only care about extracting signer certificates, and don't care - // about verifying integrity. - boolean signatureSchemeRollbackProtectionsEnforced = - (parseFlags & PARSE_IS_SYSTEM_DIR) == 0; - jarFile = new StrictJarFile( - apkPath, - !verified, // whether to verify JAR signature - signatureSchemeRollbackProtectionsEnforced); - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - - // Always verify manifest, regardless of source - final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME); - if (manifestEntry == null) { - throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Package " + apkPath + " has no manifest"); - } - - // Optimization: early termination when APK already verified - if (verified) { - return; + // Verify that entries are signed consistently with the first pkg + // we encountered. Note that for splits, certificates may have + // already been populated during an earlier parse of a base APK. + if (pkg.mCertificates == null) { + pkg.mCertificates = verified.certs; + pkg.mSignatures = verified.sigs; + pkg.mSigningKeys = new ArraySet<>(verified.certs.length); + for (int i = 0; i < verified.certs.length; i++) { + Certificate[] signerCerts = verified.certs[i]; + Certificate signerCert = signerCerts[0]; + pkg.mSigningKeys.add(signerCert.getPublicKey()); } - - // APK's integrity needs to be verified using JAR signature scheme. - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1"); - final List toVerify = new ArrayList<>(); - toVerify.add(manifestEntry); - - // If we're parsing an untrusted package, verify all contents - if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) { - final Iterator i = jarFile.iterator(); - while (i.hasNext()) { - final ZipEntry entry = i.next(); - - if (entry.isDirectory()) continue; - - final String entryName = entry.getName(); - if (entryName.startsWith("META-INF/")) continue; - if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue; - - toVerify.add(entry); - } - } - - // Verify that entries are signed consistently with the first entry - // we encountered. Note that for splits, certificates may have - // already been populated during an earlier parse of a base APK. - for (ZipEntry entry : toVerify) { - final Certificate[][] entryCerts = loadCertificates(jarFile, entry); - if (ArrayUtils.isEmpty(entryCerts)) { - throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "Package " + apkPath + " has no certificates at entry " - + entry.getName()); - } - final Signature[] entrySignatures = convertToSignatures(entryCerts); - - if (pkg.mCertificates == null) { - pkg.mCertificates = entryCerts; - pkg.mSignatures = entrySignatures; - pkg.mSigningKeys = new ArraySet(); - for (int i=0; i < entryCerts.length; i++) { - pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey()); - } - } else { - if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) { - throw new PackageParserException( - INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath - + " has mismatched certificates at entry " - + entry.getName()); - } - } + } else { + if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) { + throw new PackageParserException( + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + apkPath + " has mismatched certificates"); } - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - } catch (GeneralSecurityException e) { - throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING, - "Failed to collect certificates from " + apkPath, e); - } catch (IOException | RuntimeException e) { - throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, - "Failed to collect certificates from " + apkPath, e); - } finally { - closeQuietly(jarFile); } } - private static Signature[] convertToSignatures(Certificate[][] certs) - throws CertificateEncodingException { - final Signature[] res = new Signature[certs.length]; - for (int i = 0; i < certs.length; i++) { - res[i] = new Signature(certs[i]); - } - return res; - } - private static AssetManager newConfiguredAssetManager() { AssetManager assetManager = new AssetManager(); assetManager.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -1880,6 +1767,7 @@ public class PackageParser { int installLocation = PARSE_DEFAULT_INSTALL_LOCATION; int versionCode = 0; + int versionCodeMajor = 0; int revisionCode = 0; boolean coreApp = false; boolean debuggable = false; @@ -1898,6 +1786,8 @@ public class PackageParser { PARSE_DEFAULT_INSTALL_LOCATION); } else if (attr.equals("versionCode")) { versionCode = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("versionCodeMajor")) { + versionCodeMajor = attrs.getAttributeIntValue(i, 0); } else if (attr.equals("revisionCode")) { revisionCode = attrs.getAttributeIntValue(i, 0); } else if (attr.equals("coreApp")) { @@ -1963,9 +1853,9 @@ public class PackageParser { } return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, - configForSplit, usesSplitName, versionCode, revisionCode, installLocation, - verifiers, signatures, certificates, coreApp, debuggable, multiArch, use32bitAbi, - extractNativeLibs, isolatedSplits); + configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode, + installLocation, verifiers, signatures, certificates, coreApp, debuggable, + multiArch, use32bitAbi, extractNativeLibs, isolatedSplits); } /** @@ -2086,8 +1976,11 @@ public class PackageParser { TypedArray sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifest); - pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger( + pkg.mVersionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCode, 0); + pkg.mVersionCodeMajor = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0); + pkg.applicationInfo.versionCode = pkg.getLongVersionCode(); pkg.baseRevisionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_revisionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( @@ -2912,7 +2805,7 @@ public class PackageParser { 1, additionalCertSha256Digests.length); pkg.usesStaticLibraries = ArrayUtils.add(pkg.usesStaticLibraries, lname); - pkg.usesStaticLibrariesVersions = ArrayUtils.appendInt( + pkg.usesStaticLibrariesVersions = ArrayUtils.appendLong( pkg.usesStaticLibrariesVersions, version, true); pkg.usesStaticLibrariesCertDigests = ArrayUtils.appendElement(String[].class, pkg.usesStaticLibrariesCertDigests, certSha256Digests, true); @@ -3733,6 +3626,11 @@ public class PackageParser { } ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName, str, outError); + String factory = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_appComponentFactory); + if (factory != null) { + ai.appComponentFactory = buildClassName(ai.packageName, factory, outError); + } if (outError[0] == null) { CharSequence pname; @@ -3867,6 +3765,9 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestStaticLibrary_name); final int version = sa.getInt( com.android.internal.R.styleable.AndroidManifestStaticLibrary_version, -1); + final int versionMajor = sa.getInt( + com.android.internal.R.styleable.AndroidManifestStaticLibrary_versionMajor, + 0); sa.recycle(); @@ -3894,7 +3795,12 @@ public class PackageParser { } owner.staticSharedLibName = lname.intern(); - owner.staticSharedLibVersion = version; + if (version >= 0) { + owner.staticSharedLibVersion = + PackageInfo.composeLongVersionCode(versionMajor, version); + } else { + owner.staticSharedLibVersion = version; + } ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY; XmlUtils.skipCurrentTag(parser); @@ -5895,11 +5801,11 @@ public class PackageParser { public ArrayList childPackages; public String staticSharedLibName = null; - public int staticSharedLibVersion = 0; + public long staticSharedLibVersion = 0; public ArrayList libraryNames = null; public ArrayList usesLibraries = null; public ArrayList usesStaticLibraries = null; - public int[] usesStaticLibrariesVersions = null; + public long[] usesStaticLibrariesVersions = null; public String[][] usesStaticLibrariesCertDigests = null; public ArrayList usesOptionalLibraries = null; public String[] usesLibraryFiles = null; @@ -5916,6 +5822,14 @@ public class PackageParser { // The version code declared for this package. public int mVersionCode; + // The major version code declared for this package. + public int mVersionCodeMajor; + + // Return long containing mVersionCode and mVersionCodeMajor. + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(mVersionCodeMajor, mVersionCode); + } + // The version name declared for this package. public String mVersionName; @@ -6266,6 +6180,11 @@ public class PackageParser { return applicationInfo.isOem(); } + /** @hide */ + public boolean isVendor() { + return applicationInfo.isVendor(); + } + /** @hide */ public boolean isPrivileged() { return applicationInfo.isPrivilegedApp(); @@ -6385,7 +6304,7 @@ public class PackageParser { if (staticSharedLibName != null) { staticSharedLibName = staticSharedLibName.intern(); } - staticSharedLibVersion = dest.readInt(); + staticSharedLibVersion = dest.readLong(); libraryNames = dest.createStringArrayList(); internStringArrayList(libraryNames); usesLibraries = dest.createStringArrayList(); @@ -6399,8 +6318,8 @@ public class PackageParser { usesStaticLibraries = new ArrayList<>(libCount); dest.readStringList(usesStaticLibraries); internStringArrayList(usesStaticLibraries); - usesStaticLibrariesVersions = new int[libCount]; - dest.readIntArray(usesStaticLibrariesVersions); + usesStaticLibrariesVersions = new long[libCount]; + dest.readLongArray(usesStaticLibrariesVersions); usesStaticLibrariesCertDigests = new String[libCount][]; for (int i = 0; i < libCount; i++) { usesStaticLibrariesCertDigests[i] = dest.createStringArray(); @@ -6418,6 +6337,7 @@ public class PackageParser { mAdoptPermissions = dest.createStringArrayList(); mAppMetaData = dest.readBundle(); mVersionCode = dest.readInt(); + mVersionCodeMajor = dest.readInt(); mVersionName = dest.readString(); if (mVersionName != null) { mVersionName = mVersionName.intern(); @@ -6540,7 +6460,7 @@ public class PackageParser { dest.writeParcelableList(childPackages, flags); dest.writeString(staticSharedLibName); - dest.writeInt(staticSharedLibVersion); + dest.writeLong(staticSharedLibVersion); dest.writeStringList(libraryNames); dest.writeStringList(usesLibraries); dest.writeStringList(usesOptionalLibraries); @@ -6551,7 +6471,7 @@ public class PackageParser { } else { dest.writeInt(usesStaticLibraries.size()); dest.writeStringList(usesStaticLibraries); - dest.writeIntArray(usesStaticLibrariesVersions); + dest.writeLongArray(usesStaticLibrariesVersions); for (String[] usesStaticLibrariesCertDigest : usesStaticLibrariesCertDigests) { dest.writeStringArray(usesStaticLibrariesCertDigest); } @@ -6564,6 +6484,7 @@ public class PackageParser { dest.writeStringList(mAdoptPermissions); dest.writeBundle(mAppMetaData); dest.writeInt(mVersionCode); + dest.writeInt(mVersionCodeMajor); dest.writeString(mVersionName); dest.writeString(mSharedUserId); dest.writeInt(mSharedUserLabel); @@ -7601,33 +7522,6 @@ public class PackageParser { sCompatibilityModeEnabled = compatibilityModeEnabled; } - private static AtomicReference sBuffer = new AtomicReference(); - - public static long readFullyIgnoringContents(InputStream in) throws IOException { - byte[] buffer = sBuffer.getAndSet(null); - if (buffer == null) { - buffer = new byte[4096]; - } - - int n = 0; - int count = 0; - while ((n = in.read(buffer, 0, buffer.length)) != -1) { - count += n; - } - - sBuffer.set(buffer); - return count; - } - - public static void closeQuietly(StrictJarFile jarFile) { - if (jarFile != null) { - try { - jarFile.close(); - } catch (Exception ignored) { - } - } - } - public static class PackageParserException extends Exception { public final int error; diff --git a/android/content/pm/PermissionInfo.java b/android/content/pm/PermissionInfo.java index 75887624..21bd7f0c 100644 --- a/android/content/pm/PermissionInfo.java +++ b/android/content/pm/PermissionInfo.java @@ -17,6 +17,7 @@ package android.content.pm; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -143,6 +144,16 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { @SystemApi public static final int PROTECTION_FLAG_OEM = 0x4000; + /** + * Additional flag for {${link #protectionLevel}, corresponding + * to the vendorPrivileged value of + * {@link android.R.attr#protectionLevel}. + * + * @hide + */ + @TestApi + public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 0x8000; + /** * Mask for {@link #protectionLevel}: the basic protection type. */ @@ -231,6 +242,12 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { if (level == PROTECTION_SIGNATURE_OR_SYSTEM) { level = PROTECTION_SIGNATURE | PROTECTION_FLAG_PRIVILEGED; } + if ((level & PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0 + && (level & PROTECTION_FLAG_PRIVILEGED) == 0) { + // 'vendorPrivileged' must be 'privileged'. If not, + // drop the vendorPrivileged. + level = level & ~PROTECTION_FLAG_VENDOR_PRIVILEGED; + } return level; } @@ -284,6 +301,9 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { if ((level & PermissionInfo.PROTECTION_FLAG_OEM) != 0) { protLevel += "|oem"; } + if ((level & PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0) { + protLevel += "|vendorPrivileged"; + } return protLevel; } diff --git a/android/content/pm/RegisteredServicesCache.java b/android/content/pm/RegisteredServicesCache.java index aea843ad..56d61efd 100644 --- a/android/content/pm/RegisteredServicesCache.java +++ b/android/content/pm/RegisteredServicesCache.java @@ -361,7 +361,7 @@ public abstract class RegisteredServicesCache { } IntArray updatedUids = null; for (ServiceInfo service : allServices) { - int versionCode = service.componentInfo.applicationInfo.versionCode; + long versionCode = service.componentInfo.applicationInfo.versionCode; String pkg = service.componentInfo.packageName; ApplicationInfo newAppInfo = null; try { diff --git a/android/content/pm/SharedLibraryInfo.java b/android/content/pm/SharedLibraryInfo.java index 7d301a31..33bc9515 100644 --- a/android/content/pm/SharedLibraryInfo.java +++ b/android/content/pm/SharedLibraryInfo.java @@ -36,13 +36,11 @@ import java.util.List; public final class SharedLibraryInfo implements Parcelable { /** @hide */ - @IntDef( - flag = true, - value = { - TYPE_BUILTIN, - TYPE_DYNAMIC, - TYPE_STATIC, - }) + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_BUILTIN, + TYPE_DYNAMIC, + TYPE_STATIC, + }) @Retention(RetentionPolicy.SOURCE) @interface Type{} @@ -73,8 +71,7 @@ public final class SharedLibraryInfo implements Parcelable { private final String mName; - // TODO: Make long when we change the paltform to use longs - private final int mVersion; + private final long mVersion; private final @Type int mType; private final VersionedPackage mDeclaringPackage; private final List mDependentPackages; @@ -90,7 +87,7 @@ public final class SharedLibraryInfo implements Parcelable { * * @hide */ - public SharedLibraryInfo(String name, int version, int type, + public SharedLibraryInfo(String name, long version, int type, VersionedPackage declaringPackage, List dependentPackages) { mName = name; mVersion = version; @@ -100,7 +97,7 @@ public final class SharedLibraryInfo implements Parcelable { } private SharedLibraryInfo(Parcel parcel) { - this(parcel.readString(), parcel.readInt(), parcel.readInt(), + this(parcel.readString(), parcel.readLong(), parcel.readInt(), parcel.readParcelable(null), parcel.readArrayList(null)); } @@ -123,6 +120,14 @@ public final class SharedLibraryInfo implements Parcelable { return mName; } + /** + * @deprecated Use {@link #getLongVersion()} instead. + */ + @Deprecated + public @IntRange(from = -1) int getVersion() { + return mVersion < 0 ? (int) mVersion : (int) (mVersion & 0x7fffffff); + } + /** * Gets the version of the library. For {@link #TYPE_STATIC static} libraries * this is the declared version and for {@link #TYPE_DYNAMIC dynamic} and @@ -131,7 +136,7 @@ public final class SharedLibraryInfo implements Parcelable { * * @return The version. */ - public @IntRange(from = -1) int getVersion() { + public @IntRange(from = -1) long getLongVersion() { return mVersion; } @@ -192,7 +197,7 @@ public final class SharedLibraryInfo implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mName); - parcel.writeInt(mVersion); + parcel.writeLong(mVersion); parcel.writeInt(mType); parcel.writeParcelable(mDeclaringPackage, flags); parcel.writeList(mDependentPackages); diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java index 9ff07757..8839cf9d 100644 --- a/android/content/pm/ShortcutInfo.java +++ b/android/content/pm/ShortcutInfo.java @@ -109,8 +109,7 @@ public final class ShortcutInfo implements Parcelable { public static final int FLAG_SHADOW = 1 << 12; /** @hide */ - @IntDef(flag = true, - value = { + @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_DYNAMIC, FLAG_PINNED, FLAG_HAS_ICON_RES, @@ -153,15 +152,14 @@ public final class ShortcutInfo implements Parcelable { | CLONE_REMOVE_RES_NAMES; /** @hide */ - @IntDef(flag = true, - value = { - CLONE_REMOVE_ICON, - CLONE_REMOVE_INTENT, - CLONE_REMOVE_NON_KEY_INFO, - CLONE_REMOVE_RES_NAMES, - CLONE_REMOVE_FOR_CREATOR, - CLONE_REMOVE_FOR_LAUNCHER - }) + @IntDef(flag = true, prefix = { "CLONE_" }, value = { + CLONE_REMOVE_ICON, + CLONE_REMOVE_INTENT, + CLONE_REMOVE_NON_KEY_INFO, + CLONE_REMOVE_RES_NAMES, + CLONE_REMOVE_FOR_CREATOR, + CLONE_REMOVE_FOR_LAUNCHER + }) @Retention(RetentionPolicy.SOURCE) public @interface CloneFlags {} @@ -212,7 +210,7 @@ public final class ShortcutInfo implements Parcelable { public static final int DISABLED_REASON_OTHER_RESTORE_ISSUE = 103; /** @hide */ - @IntDef(value = { + @IntDef(prefix = { "DISABLED_REASON_" }, value = { DISABLED_REASON_NOT_DISABLED, DISABLED_REASON_BY_APP, DISABLED_REASON_APP_CHANGED, @@ -220,7 +218,7 @@ public final class ShortcutInfo implements Parcelable { DISABLED_REASON_BACKUP_NOT_SUPPORTED, DISABLED_REASON_SIGNATURE_MISMATCH, DISABLED_REASON_OTHER_RESTORE_ISSUE, - }) + }) @Retention(RetentionPolicy.SOURCE) public @interface DisabledReason{} diff --git a/android/content/pm/ShortcutManager.java b/android/content/pm/ShortcutManager.java index 61b0eb0b..30222b74 100644 --- a/android/content/pm/ShortcutManager.java +++ b/android/content/pm/ShortcutManager.java @@ -36,15 +36,26 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.List; /** - * The ShortcutManager manages an app's shortcuts. Shortcuts provide users with quick - * access to activities other than an app's main activity in the currently-active launcher, provided - * that the launcher supports app shortcuts. For example, an email app may publish the "compose new - * email" action, which will directly open the compose activity. The {@link ShortcutInfo} class - * contains information about each of the shortcuts themselves. + * The ShortcutManager performs operations on an app's set of shortcuts. The + * {@link ShortcutInfo} class contains information about each of the shortcuts themselves. + * + *

    An app's shortcuts represent specific tasks and actions that users can perform within your + * app. When a user selects a shortcut in the currently-active launcher, your app opens an activity + * other than the app's starting activity, provided that the currently-active launcher supports app + * shortcuts.

    + * + *

    The types of shortcuts that you create for your app depend on the app's key use cases. For + * example, an email app may publish the "compose new email" shortcut, which allows the app to + * directly open the compose activity.

    + * + *

    Note: Only main activities—activities that handle the + * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category—can + * have shortcuts. If an app has multiple main activities, you need to define the set of shortcuts + * for each activity. * *

    This page discusses the implementation details of the ShortcutManager class. For - * guidance on performing operations on app shortcuts within your app, see the - * App Shortcuts feature guide. + * definitions of key terms and guidance on performing operations on shortcuts within your app, see + * the App Shortcuts feature guide. * *

    Shortcut characteristics

    * @@ -69,8 +80,8 @@ import java.util.List; *
      *
    • The user removes it. *
    • The publisher app associated with the shortcut is uninstalled. - *
    • The user performs the clear data action on the publisher app from the device's - * Settings app. + *
    • The user selects Clear data from the publisher app's Storage screen, within + * the system's Settings app. *
    * *

    Because the system performs @@ -83,12 +94,17 @@ import java.util.List; * *

    When the launcher displays an app's shortcuts, they should appear in the following order: * - *

      - *
    • Static shortcuts (if {@link ShortcutInfo#isDeclaredInManifest()} is {@code true}), - * and then show dynamic shortcuts (if {@link ShortcutInfo#isDynamic()} is {@code true}). - *
    • Within each shortcut type (static and dynamic), sort the shortcuts in order of increasing - * rank according to {@link ShortcutInfo#getRank()}. - *
    + *
      + *
    1. Static shortcuts: Shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method + * returns {@code true}.
    2. + *
    3. Dynamic shortcuts: Shortcuts whose {@link ShortcutInfo#isDynamic()} method returns + * {@code true}.
    4. + *
    + * + *

    Within each shortcut type (static and dynamic), shortcuts are sorted in order of increasing + * rank according to {@link ShortcutInfo#getRank()}.

    + * + *

    Shortcut ranks

    * *

    Shortcut ranks are non-negative, sequential integers that determine the order in which * shortcuts appear, assuming that the shortcuts are all in the same category. You can update ranks @@ -103,64 +119,99 @@ import java.util.List; * *

    Options for static shortcuts

    * - * The following list includes descriptions for the different attributes within a static shortcut: + * The following list includes descriptions for the different attributes within a static shortcut. + * You must provide a value for {@code android:shortcutId} and {@code android:shortcutShortLabel}; + * all other values are optional. + * *
    *
    {@code android:shortcutId}
    - *
    Mandatory shortcut ID. - *

    - * This must be a string literal. - * A resource string, such as @string/foo, cannot be used. + *

    A string literal, which represents the shortcut when a {@code ShortcutManager} object + * performs operations on it.

    + *

    Note: You cannot set this attribute's value to a resource string, such + * as @string/foo.

    *
    * *
    {@code android:enabled}
    - *
    Default is {@code true}. Can be set to {@code false} in order - * to disable a static shortcut that was published in a previous version and set a custom - * disabled message. If a custom disabled message is not needed, then a static shortcut can - * be simply removed from the XML file rather than keeping it with {@code enabled="false"}.
    + *

    Whether the user can interact with the shortcut from a supported launcher.

    + *

    The default value is {@code true}. If you set it to {@code false}, you should also set + * {@code android:shortcutDisabledMessage} to a message that explains why you've disabled the + * shortcut. If you don't think you need to provide such a message, it's easiest to just remove + * the shortcut from the XML file entirely, rather than changing the values of the shortcut's + * {@code android:enabled} and {@code android:shortcutDisabledMessage} attributes. + *

    * *
    {@code android:icon}
    - *
    Shortcut icon.
    + *

    The bitmap or + * adaptive icon that the + * launcher uses when displaying the shortcut to the user. This value can be either the path to an + * image or the resource file that contains the image. Use adaptive icons whenever possible to + * improve performance and consistency.

    + *

    Note: Shortcut icons cannot include + * tints. + *

    * *
    {@code android:shortcutShortLabel}
    - *
    Mandatory shortcut short label. - * See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}. - *

    - * This must be a resource string, such as @string/shortcut_label. + *

    A concise phrase that describes the shortcut's purpose. For more information, see + * {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.

    + *

    Note: This attribute's value must be a resource string, such as + * @string/shortcut_short_label.

    *
    * *
    {@code android:shortcutLongLabel}
    - *
    Shortcut long label. - * See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}. - *

    - * This must be a resource string, such as @string/shortcut_long_label. + *

    An extended phrase that describes the shortcut's purpose. If there's enough space, the + * launcher displays this value instead of {@code android:shortcutShortLabel}. For more + * information, see {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.

    + *

    Note: This attribute's value must be a resource string, such as + * @string/shortcut_long_label.

    *
    * *
    {@code android:shortcutDisabledMessage}
    - *
    When {@code android:enabled} is set to - * {@code false}, this attribute is used to display a custom disabled message. - *

    - * This must be a resource string, such as @string/shortcut_disabled_message. + *

    The message that appears in a supported launcher when the user attempts to launch a + * disabled shortcut. The message should explain to the user why the shortcut is now disabled. + * This attribute's value has no effect if {@code android:enabled} is {@code true}.

    + *

    Note: This attribute's value must be a resource string, such as + * @string/shortcut_disabled_message.

    *
    + *
    + * + *

    Inner elements that define static shortcuts

    + * + *

    The XML file that lists an app's static shortcuts supports the following elements inside each + * {@code } element. You must include an {@code intent} inner element for each + * static shortcut that you define.

    * + *
    *
    {@code intent}
    - *
    Intent to launch when the user selects the shortcut. - * {@code android:action} is mandatory. - * See Using intents for the - * other supported tags. - *

    You can provide multiple intents for a single shortcut so that the last defined activity is - * launched with the other activities in the + *

    The action that the system launches when the user selects the shortcut. This intent must + * provide a value for the {@code android:action} attribute.

    + *

    You can provide multiple intents for a single shortcut. If you do so, the last defined + * activity is launched, and the other activities are placed in the * back stack. See - * {@link android.app.TaskStackBuilder} for details. - *

    Note: String resources may not be used within an {@code } element. + * Using Static Shortcuts and the + * {@link android.app.TaskStackBuilder} class reference for details.

    + *

    Note: This {@code intent} element cannot include string resources.

    + *

    To learn more about how to configure intents, see + * Using intents.

    *
    + * *
    {@code categories}
    - *
    Specify shortcut categories. Currently only - * {@link ShortcutInfo#SHORTCUT_CATEGORY_CONVERSATION} is defined in the framework. + *

    Provides a grouping for the types of actions that your app's shortcuts perform, such as + * creating new chat messages.

    + *

    For a list of supported shortcut categories, see the {@link ShortcutInfo} class reference + * for a list of supported shortcut categories. *

    *
    * *

    Updating shortcuts

    * + *

    Each app's launcher icon can contain at most {@link #getMaxShortcutCountPerActivity()} number + * of static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts + * that an app can create, though. + * + *

    When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut, + * the pinned shortcut is still visible and launchable. This allows an app to have more than + * {@link #getMaxShortcutCountPerActivity()} number of shortcuts. + * *

    As an example, suppose {@link #getMaxShortcutCountPerActivity()} is 5: *

      *
    1. A chat app publishes 5 dynamic shortcuts for the 5 most recent @@ -168,18 +219,13 @@ import java.util.List; * *
    2. The user pins all 5 of the shortcuts. * - *
    3. Later, the user has started 3 additional conversations (c6, c7, and c8), - * so the publisher app - * re-publishes its dynamic shortcuts. The new dynamic shortcut list is: - * c4, c5, ..., c8. - * The publisher app has to remove c1, c2, and c3 because it can't have more than - * 5 dynamic shortcuts. - * - *
    4. However, even though c1, c2, and c3 are no longer dynamic shortcuts, the pinned - * shortcuts for these conversations are still available and launchable. - * - *
    5. At this point, the user can access a total of 8 shortcuts that link to activities in - * the publisher app, including the 3 pinned shortcuts, even though an app can have at most 5 + *
    6. Later, the user has started 3 additional conversations (c6, c7, and c8), so the publisher + * app re-publishes its dynamic shortcuts. The new dynamic shortcut list is: c4, c5, ..., c8. + *

      The publisher app has to remove c1, c2, and c3 because it can't have more than 5 dynamic + * shortcuts. However, c1, c2, and c3 are still pinned shortcuts that the user can access and + * launch. + *

      At this point, the user can access a total of 8 shortcuts that link to activities in the + * publisher app, including the 3 pinned shortcuts, even though an app can have at most 5 * dynamic shortcuts. * *

    7. The app can use {@link #updateShortcuts(List)} to update any of the existing @@ -196,44 +242,23 @@ import java.util.List; * Dynamic shortcuts can be published with any set of {@link Intent#addFlags Intent} flags. * Typically, {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} is specified, possibly along with other * flags; otherwise, if the app is already running, the app is simply brought to - * the foreground, and the target activity may not appear. + * the foreground, and the target activity might not appear. * *

      Static shortcuts cannot have custom intent flags. * The first intent of a static shortcut will always have {@link Intent#FLAG_ACTIVITY_NEW_TASK} * and {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} set. This means, when the app is already running, all - * the existing activities in your app will be destroyed when a static shortcut is launched. + * the existing activities in your app are destroyed when a static shortcut is launched. * If this behavior is not desirable, you can use a trampoline activity, or an invisible * activity that starts another activity in {@link Activity#onCreate}, then calls * {@link Activity#finish()}: *

        *
      1. In the AndroidManifest.xml file, the trampoline activity should include the * attribute assignment {@code android:taskAffinity=""}. - *
      2. In the shortcuts resource file, the intent within the static shortcut should point at + *
      3. In the shortcuts resource file, the intent within the static shortcut should reference * the trampoline activity. *
      * - *

      Handling system locale changes

      - * - *

      Apps should update dynamic and pinned shortcuts when the system locale changes using the - * {@link Intent#ACTION_LOCALE_CHANGED} broadcast. When the system locale changes, - * rate limiting is reset, so even - * background apps can add and update dynamic shortcuts until the rate limit is reached again. - * - *

      Shortcut limits

      - * - *

      Only main activities—activities that handle the {@code MAIN} action and the - * {@code LAUNCHER} category—can have shortcuts. If an app has multiple main activities, you - * need to define the set of shortcuts for each activity. - * - *

      Each launcher icon can have at most {@link #getMaxShortcutCountPerActivity()} number of - * static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts that - * an app can create. - * - *

      When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut, - * the pinned shortcut is still visible and launchable. This allows an app to have more than - * {@link #getMaxShortcutCountPerActivity()} number of shortcuts. - * - *

      Rate limiting

      + *

      Rate limiting

      * *

      When rate limiting is active, * {@link #isRateLimitingActive()} returns {@code true}. @@ -243,8 +268,20 @@ import java.util.List; *

        *
      • An app comes to the foreground. *
      • The system locale changes. - *
      • The user performs the inline reply action on a notification. + *
      • The user performs the inline + * reply action on a notification. *
      + * + *

      Handling system locale changes

      + * + *

      Apps should update dynamic and pinned shortcuts when they receive the + * {@link Intent#ACTION_LOCALE_CHANGED} broadcast, indicating that the system locale has changed. + *

      When the system locale changes, rate + * limiting is reset, so even background apps can add and update dynamic shortcuts until the + * rate limit is reached again. + * + *

      Retrieving class instances

      + * */ @SystemService(Context.SHORTCUT_SERVICE) public class ShortcutManager { diff --git a/android/content/pm/ShortcutServiceInternal.java b/android/content/pm/ShortcutServiceInternal.java index dadfaa9f..e6f682d2 100644 --- a/android/content/pm/ShortcutServiceInternal.java +++ b/android/content/pm/ShortcutServiceInternal.java @@ -81,4 +81,7 @@ public abstract class ShortcutServiceInternal { @Nullable IntentSender resultIntent, int userId); public abstract boolean isRequestPinItemSupported(int callingUserId, int requestType); + + public abstract boolean isForegroundDefaultLauncher(@NonNull String callingPackage, + int callingUid); } diff --git a/android/content/pm/VersionedPackage.java b/android/content/pm/VersionedPackage.java index 29c5efe7..39534664 100644 --- a/android/content/pm/VersionedPackage.java +++ b/android/content/pm/VersionedPackage.java @@ -28,7 +28,7 @@ import java.lang.annotation.RetentionPolicy; */ public final class VersionedPackage implements Parcelable { private final String mPackageName; - private final int mVersionCode; + private final long mVersionCode; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -47,9 +47,21 @@ public final class VersionedPackage implements Parcelable { mVersionCode = versionCode; } + /** + * Creates a new instance. Use {@link PackageManager#VERSION_CODE_HIGHEST} + * to refer to the highest version code of this package. + * @param packageName The package name. + * @param versionCode The version code. + */ + public VersionedPackage(@NonNull String packageName, + @VersionCode long versionCode) { + mPackageName = packageName; + mVersionCode = versionCode; + } + private VersionedPackage(Parcel parcel) { mPackageName = parcel.readString(); - mVersionCode = parcel.readInt(); + mVersionCode = parcel.readLong(); } /** @@ -61,12 +73,20 @@ public final class VersionedPackage implements Parcelable { return mPackageName; } + /** + * @deprecated use {@link #getLongVersionCode()} instead. + */ + @Deprecated + public @VersionCode int getVersionCode() { + return (int) (mVersionCode & 0x7fffffff); + } + /** * Gets the version code. * * @return The version code. */ - public @VersionCode int getVersionCode() { + public @VersionCode long getLongVersionCode() { return mVersionCode; } @@ -83,7 +103,7 @@ public final class VersionedPackage implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mPackageName); - parcel.writeInt(mVersionCode); + parcel.writeLong(mVersionCode); } public static final Creator CREATOR = new Creator() { diff --git a/android/content/pm/crossprofile/CrossProfileApps.java b/android/content/pm/crossprofile/CrossProfileApps.java index c441b5f3..414c1389 100644 --- a/android/content/pm/crossprofile/CrossProfileApps.java +++ b/android/content/pm/crossprofile/CrossProfileApps.java @@ -16,15 +16,19 @@ package android.content.pm.crossprofile; import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import com.android.internal.R; +import com.android.internal.util.UserIcons; + import java.util.List; /** @@ -35,11 +39,15 @@ import java.util.List; public class CrossProfileApps { private final Context mContext; private final ICrossProfileApps mService; + private final UserManager mUserManager; + private final Resources mResources; /** @hide */ public CrossProfileApps(Context context, ICrossProfileApps service) { mContext = context; mService = service; + mUserManager = context.getSystemService(UserManager.class); + mResources = context.getResources(); } /** @@ -52,15 +60,10 @@ public class CrossProfileApps { * @param user The UserHandle of the profile, must be one of the users returned by * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will * be thrown. - * @param sourceBounds The Rect containing the source bounds of the clicked icon, see - * {@link android.content.Intent#setSourceBounds(Rect)}. - * @param startActivityOptions Options to pass to startActivity */ - public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user, - @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) { + public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user) { try { - mService.startActivityAsUser(mContext.getPackageName(), - component, sourceBounds, startActivityOptions, user); + mService.startActivityAsUser(mContext.getPackageName(), component, user); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -87,4 +90,58 @@ public class CrossProfileApps { throw ex.rethrowFromSystemServer(); } } + + /** + * Return a label that calling app can show to user for the semantic of profile switching -- + * launching its own activity in specified user profile. For example, it may return + * "Switch to work" if the given user handle is the managed profile one. + * + * @param userHandle The UserHandle of the target profile, must be one of the users returned by + * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will + * be thrown. + * @return a label that calling app can show user for the semantic of launching its own + * activity in the specified user profile. + * + * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle) + */ + public @NonNull CharSequence getProfileSwitchingLabel(@NonNull UserHandle userHandle) { + verifyCanAccessUser(userHandle); + + final int stringRes = mUserManager.isManagedProfile(userHandle.getIdentifier()) + ? R.string.managed_profile_label + : R.string.user_owner_label; + return mResources.getString(stringRes); + } + + /** + * Return an icon that calling app can show to user for the semantic of profile switching -- + * launching its own activity in specified user profile. For example, it may return a briefcase + * icon if the given user handle is the managed profile one. + * + * @param userHandle The UserHandle of the target profile, must be one of the users returned by + * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will + * be thrown. + * @return an icon that calling app can show user for the semantic of launching its own + * activity in specified user profile. + * + * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle) + */ + public @NonNull Drawable getProfileSwitchingIcon(@NonNull UserHandle userHandle) { + verifyCanAccessUser(userHandle); + + final boolean isManagedProfile = + mUserManager.isManagedProfile(userHandle.getIdentifier()); + if (isManagedProfile) { + return mResources.getDrawable(R.drawable.ic_corp_badge, null); + } else { + return UserIcons.getDefaultUserIcon( + mResources, UserHandle.USER_SYSTEM, true /* light */); + } + } + + private void verifyCanAccessUser(UserHandle userHandle) { + if (!getTargetUserProfiles().contains(userHandle)) { + throw new SecurityException("Not allowed to access " + userHandle); + } + } } diff --git a/android/content/pm/dex/ArtManager.java b/android/content/pm/dex/ArtManager.java new file mode 100644 index 00000000..201cd8d3 --- /dev/null +++ b/android/content/pm/dex/ArtManager.java @@ -0,0 +1,156 @@ +/** + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm.dex; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Slog; + +/** + * Class for retrieving various kinds of information related to the runtime artifacts of + * packages that are currently installed on the device. + * + * @hide + */ +@SystemApi +public class ArtManager { + private static final String TAG = "ArtManager"; + + /** The snapshot failed because the package was not found. */ + public static final int SNAPSHOT_FAILED_PACKAGE_NOT_FOUND = 0; + /** The snapshot failed because the package code path does not exist. */ + public static final int SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND = 1; + /** The snapshot failed because of an internal error (e.g. error during opening profiles). */ + public static final int SNAPSHOT_FAILED_INTERNAL_ERROR = 2; + + private IArtManager mArtManager; + + /** + * @hide + */ + public ArtManager(@NonNull IArtManager manager) { + mArtManager = manager; + } + + /** + * Snapshots the runtime profile for an apk belonging to the package {@code packageName}. + * The apk is identified by {@code codePath}. The calling process must have + * {@code android.permission.READ_RUNTIME_PROFILE} permission. + * + * The result will be posted on {@code handler} using the given {@code callback}. + * The profile being available as a read-only {@link android.os.ParcelFileDescriptor}. + * + * @param packageName the target package name + * @param codePath the code path for which the profile should be retrieved + * @param callback the callback which should be used for the result + * @param handler the handler which should be used to post the result + */ + @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES) + public void snapshotRuntimeProfile(@NonNull String packageName, @NonNull String codePath, + @NonNull SnapshotRuntimeProfileCallback callback, @NonNull Handler handler) { + Slog.d(TAG, "Requesting profile snapshot for " + packageName + ":" + codePath); + + SnapshotRuntimeProfileCallbackDelegate delegate = + new SnapshotRuntimeProfileCallbackDelegate(callback, handler.getLooper()); + try { + mArtManager.snapshotRuntimeProfile(packageName, codePath, delegate); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Returns true if runtime profiles are enabled, false otherwise. + * + * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission. + */ + @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES) + public boolean isRuntimeProfilingEnabled() { + try { + return mArtManager.isRuntimeProfilingEnabled(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + return false; + } + + /** + * Callback used for retrieving runtime profiles. + */ + public abstract static class SnapshotRuntimeProfileCallback { + /** + * Called when the profile snapshot finished with success. + * + * @param profileReadFd the file descriptor that can be used to read the profile. Note that + * the file might be empty (which is valid profile). + */ + public abstract void onSuccess(ParcelFileDescriptor profileReadFd); + + /** + * Called when the profile snapshot finished with an error. + * + * @param errCode the error code {@see SNAPSHOT_FAILED_PACKAGE_NOT_FOUND, + * SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND, SNAPSHOT_FAILED_INTERNAL_ERROR}. + */ + public abstract void onError(int errCode); + } + + private static class SnapshotRuntimeProfileCallbackDelegate + extends android.content.pm.dex.ISnapshotRuntimeProfileCallback.Stub + implements Handler.Callback { + private static final int MSG_SNAPSHOT_OK = 1; + private static final int MSG_ERROR = 2; + private final ArtManager.SnapshotRuntimeProfileCallback mCallback; + private final Handler mHandler; + + private SnapshotRuntimeProfileCallbackDelegate( + ArtManager.SnapshotRuntimeProfileCallback callback, Looper looper) { + mCallback = callback; + mHandler = new Handler(looper, this); + } + + @Override + public void onSuccess(ParcelFileDescriptor profileReadFd) { + mHandler.obtainMessage(MSG_SNAPSHOT_OK, profileReadFd).sendToTarget(); + } + + @Override + public void onError(int errCode) { + mHandler.obtainMessage(MSG_ERROR, errCode, 0).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SNAPSHOT_OK: + mCallback.onSuccess((ParcelFileDescriptor) msg.obj); + break; + case MSG_ERROR: + mCallback.onError(msg.arg1); + break; + default: return false; + } + return true; + } + } +} diff --git a/android/content/res/AssetFileDescriptor.java b/android/content/res/AssetFileDescriptor.java index 28edde02..be410362 100644 --- a/android/content/res/AssetFileDescriptor.java +++ b/android/content/res/AssetFileDescriptor.java @@ -195,7 +195,7 @@ public class AssetFileDescriptor implements Parcelable, Closeable { /** * An InputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close - * ParcelFileDescritor.close()} for you when the stream is closed. + * ParcelFileDescriptor.close()} for you when the stream is closed. */ public static class AutoCloseInputStream extends ParcelFileDescriptor.AutoCloseInputStream { @@ -282,7 +282,7 @@ public class AssetFileDescriptor implements Parcelable, Closeable { /** * An OutputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close - * ParcelFileDescritor.close()} for you when the stream is closed. + * ParcelFileDescriptor.close()} for you when the stream is closed. */ public static class AutoCloseOutputStream extends ParcelFileDescriptor.AutoCloseOutputStream { diff --git a/android/content/res/Configuration.java b/android/content/res/Configuration.java index 26efda10..eb309799 100644 --- a/android/content/res/Configuration.java +++ b/android/content/res/Configuration.java @@ -781,25 +781,24 @@ public final class Configuration implements Parcelable, ComparableThe value of {@link Settings.Global#SQLITE_COMPATIBILITY_WAL_FLAGS} is cached on first access + * for consistent behavior across all connections opened in the process. + * @hide + */ +public class SQLiteCompatibilityWalFlags { + + private static final String TAG = "SQLiteCompatibilityWalFlags"; + + private static volatile boolean sInitialized; + private static volatile boolean sFlagsSet; + private static volatile boolean sCompatibilityWalSupported; + private static volatile String sWALSyncMode; + // This flag is used to avoid recursive initialization due to circular dependency on Settings + private static volatile boolean sCallingGlobalSettings; + + /** + * @hide + */ + @VisibleForTesting + public static boolean areFlagsSet() { + initIfNeeded(); + return sFlagsSet; + } + + /** + * @hide + */ + @VisibleForTesting + public static boolean isCompatibilityWalSupported() { + initIfNeeded(); + return sCompatibilityWalSupported; + } + + /** + * @hide + */ + @VisibleForTesting + public static String getWALSyncMode() { + initIfNeeded(); + return sWALSyncMode; + } + + private static void initIfNeeded() { + if (sInitialized || sCallingGlobalSettings) { + return; + } + ActivityThread activityThread = ActivityThread.currentActivityThread(); + Application app = activityThread == null ? null : activityThread.getApplication(); + String flags = null; + if (app == null) { + Log.w(TAG, "Cannot read global setting " + + Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS + " - " + + "Application state not available"); + } else { + try { + sCallingGlobalSettings = true; + flags = Settings.Global.getString(app.getContentResolver(), + Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS); + } finally { + sCallingGlobalSettings = false; + } + } + + init(flags); + } + + /** + * @hide + */ + @VisibleForTesting + public static void init(String flags) { + if (TextUtils.isEmpty(flags)) { + sInitialized = true; + return; + } + KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(flags); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Setting has invalid format: " + flags, e); + sInitialized = true; + return; + } + sCompatibilityWalSupported = parser.getBoolean("compatibility_wal_supported", + SQLiteGlobal.isCompatibilityWalSupported()); + sWALSyncMode = parser.getString("wal_syncmode", SQLiteGlobal.getWALSyncMode()); + Log.i(TAG, "Read compatibility WAL flags: compatibility_wal_supported=" + + sCompatibilityWalSupported + ", wal_syncmode=" + sWALSyncMode); + sFlagsSet = true; + sInitialized = true; + } + + /** + * @hide + */ + @VisibleForTesting + public static void reset() { + sInitialized = false; + sFlagsSet = false; + sCompatibilityWalSupported = false; + sWALSyncMode = null; + } +} diff --git a/android/database/sqlite/SQLiteConnection.java b/android/database/sqlite/SQLiteConnection.java index 2c93a7fe..7717b8d3 100644 --- a/android/database/sqlite/SQLiteConnection.java +++ b/android/database/sqlite/SQLiteConnection.java @@ -296,7 +296,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen && mConfiguration.syncMode == null && mConfiguration.useCompatibilityWal; if (walEnabled || useCompatibilityWal) { setJournalMode("WAL"); - setSyncMode(SQLiteGlobal.getWALSyncMode()); + if (useCompatibilityWal && SQLiteCompatibilityWalFlags.areFlagsSet()) { + setSyncMode(SQLiteCompatibilityWalFlags.getWALSyncMode()); + } else { + setSyncMode(SQLiteGlobal.getWALSyncMode()); + } } else { setJournalMode(mConfiguration.journalMode == null ? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode); diff --git a/android/database/sqlite/SQLiteConnectionPool.java b/android/database/sqlite/SQLiteConnectionPool.java index 5adb1196..b2117003 100644 --- a/android/database/sqlite/SQLiteConnectionPool.java +++ b/android/database/sqlite/SQLiteConnectionPool.java @@ -1094,6 +1094,12 @@ public final class SQLiteConnectionPool implements Closeable { printer.println(" Open: " + mIsOpen); printer.println(" Max connections: " + mMaxConnectionPoolSize); printer.println(" Total execution time: " + mTotalExecutionTimeCounter); + if (SQLiteCompatibilityWalFlags.areFlagsSet()) { + printer.println(" Compatibility WAL settings: compatibility_wal_supported=" + + SQLiteCompatibilityWalFlags + .isCompatibilityWalSupported() + ", wal_syncmode=" + + SQLiteCompatibilityWalFlags.getWALSyncMode()); + } if (mConfiguration.isLookasideConfigSet()) { printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize + " cnt=" + mConfiguration.lookasideSlotCount); diff --git a/android/database/sqlite/SQLiteDatabase.java b/android/database/sqlite/SQLiteDatabase.java index 09bb9c69..c1c0812e 100644 --- a/android/database/sqlite/SQLiteDatabase.java +++ b/android/database/sqlite/SQLiteDatabase.java @@ -289,6 +289,10 @@ public final class SQLiteDatabase extends SQLiteClosable { mConfigurationLocked.journalMode = journalMode; mConfigurationLocked.syncMode = syncMode; mConfigurationLocked.useCompatibilityWal = SQLiteGlobal.isCompatibilityWalSupported(); + if (!mConfigurationLocked.isInMemoryDb() && SQLiteCompatibilityWalFlags.areFlagsSet()) { + mConfigurationLocked.useCompatibilityWal = SQLiteCompatibilityWalFlags + .isCompatibilityWalSupported(); + } } @Override diff --git a/android/graphics/BitmapFactory_Delegate.java b/android/graphics/BitmapFactory_Delegate.java index 8bd2a7ac..ee099e1d 100644 --- a/android/graphics/BitmapFactory_Delegate.java +++ b/android/graphics/BitmapFactory_Delegate.java @@ -101,20 +101,26 @@ import java.util.Set; @LayoutlibDelegate /*package*/ static Bitmap nativeDecodeFileDescriptor(FileDescriptor fd, Rect padding, Options opts) { - opts.inBitmap = null; + if (opts != null) { + opts.inBitmap = null; + } return null; } @LayoutlibDelegate /*package*/ static Bitmap nativeDecodeAsset(long asset, Rect padding, Options opts) { - opts.inBitmap = null; + if (opts != null) { + opts.inBitmap = null; + } return null; } @LayoutlibDelegate /*package*/ static Bitmap nativeDecodeByteArray(byte[] data, int offset, int length, Options opts) { - opts.inBitmap = null; + if (opts != null) { + opts.inBitmap = null; + } return null; } diff --git a/android/graphics/Bitmap_Delegate.java b/android/graphics/Bitmap_Delegate.java index 00645379..6c72cb2f 100644 --- a/android/graphics/Bitmap_Delegate.java +++ b/android/graphics/Bitmap_Delegate.java @@ -608,7 +608,8 @@ public final class Bitmap_Delegate { if (delegate == null) { return 0; } - return nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight(); + int size = nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight(); + return size < 0 ? Integer.MAX_VALUE : size; } diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java new file mode 100644 index 00000000..60416a72 --- /dev/null +++ b/android/graphics/ImageDecoder.java @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RawRes; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.NinePatchDrawable; + +import java.nio.ByteBuffer; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ArrayIndexOutOfBoundsException; +import java.lang.NullPointerException; +import java.lang.RuntimeException; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Class for decoding images as {@link Bitmap}s or {@link Drawable}s. + * @hide + */ +public final class ImageDecoder { + /** + * Source of the encoded image data. + */ + public static abstract class Source { + /* @hide */ + Resources getResources() { return null; } + + /* @hide */ + void close() {} + + /* @hide */ + abstract ImageDecoder createImageDecoder(); + }; + + private static class ByteArraySource extends Source { + ByteArraySource(byte[] data, int offset, int length) { + mData = data; + mOffset = offset; + mLength = length; + }; + private final byte[] mData; + private final int mOffset; + private final int mLength; + + @Override + public ImageDecoder createImageDecoder() { + return nCreate(mData, mOffset, mLength); + } + } + + private static class ByteBufferSource extends Source { + ByteBufferSource(ByteBuffer buffer) { + mBuffer = buffer; + } + private final ByteBuffer mBuffer; + + @Override + public ImageDecoder createImageDecoder() { + if (!mBuffer.isDirect() && mBuffer.hasArray()) { + int offset = mBuffer.arrayOffset() + mBuffer.position(); + int length = mBuffer.limit() - mBuffer.position(); + return nCreate(mBuffer.array(), offset, length); + } + return nCreate(mBuffer, mBuffer.position(), mBuffer.limit()); + } + } + + private static class ResourceSource extends Source { + ResourceSource(Resources res, int resId) + throws Resources.NotFoundException { + // Test that the resource can be found. + InputStream is = null; + try { + is = res.openRawResource(resId); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + + mResources = res; + mResId = resId; + } + + final Resources mResources; + final int mResId; + // This is just stored here in order to keep the underlying Asset + // alive. FIXME: Can I access the Asset (and keep it alive) without + // this object? + InputStream mInputStream; + + @Override + public Resources getResources() { return mResources; } + + @Override + public ImageDecoder createImageDecoder() { + // FIXME: Can I bypass creating the stream? + try { + mInputStream = mResources.openRawResource(mResId); + } catch (Resources.NotFoundException e) { + // This should never happen, since we already tested in the + // constructor. + } + if (!(mInputStream instanceof AssetManager.AssetInputStream)) { + // This should never happen. + throw new RuntimeException("Resource is not an asset?"); + } + long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset(); + return nCreate(asset); + } + + @Override + public void close() { + try { + mInputStream.close(); + } catch (IOException e) { + } finally { + mInputStream = null; + } + } + } + + /** + * Contains information about the encoded image. + */ + public static class ImageInfo { + public final int width; + public final int height; + // TODO?: Add more info? mimetype, ninepatch etc? + + ImageInfo(int width, int height) { + this.width = width; + this.height = height; + } + }; + + /** + * Used if the provided data is incomplete. + * + * There may be a partial image to display. + */ + public class IncompleteException extends Exception {}; + + /** + * Used if the provided data is corrupt. + * + * There may be a partial image to display. + */ + public class CorruptException extends Exception {}; + + /** + * Optional listener supplied to {@link #decodeDrawable} or + * {@link #decodeBitmap}. + */ + public static interface OnHeaderDecodedListener { + /** + * Called when the header is decoded and the size is known. + * + * @param info Information about the encoded image. + * @param decoder allows changing the default settings of the decode. + */ + public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder); + + }; + + /** + * Optional listener supplied to the ImageDecoder. + */ + public static interface OnExceptionListener { + /** + * Called when there is a problem in the stream or in the data. + * FIXME: Or do not allow streams? + * FIXME: Report how much of the image has been decoded? + * + * @param e Exception containing information about the error. + * @return True to create and return a {@link Drawable}/ + * {@link Bitmap} with partial data. False to return + * {@code null}. True is the default. + */ + public boolean onException(Exception e); + }; + + // Fields + private long mNativePtr; + private final int mWidth; + private final int mHeight; + + private int mDesiredWidth; + private int mDesiredHeight; + private int mAllocator = DEFAULT_ALLOCATOR; + private boolean mRequireUnpremultiplied = false; + private boolean mMutable = false; + private boolean mPreferRamOverQuality = false; + private boolean mAsAlphaMask = false; + private Rect mCropRect; + + private PostProcess mPostProcess; + private OnExceptionListener mOnExceptionListener; + + + /** + * Private constructor called by JNI. {@link #recycle} must be + * called after decoding to delete native resources. + */ + @SuppressWarnings("unused") + private ImageDecoder(long nativePtr, int width, int height) { + mNativePtr = nativePtr; + mWidth = width; + mHeight = height; + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Create a new {@link Source} from an asset. + * + * @param res the {@link Resources} object containing the image data. + * @param resId resource ID of the image data. + * // FIXME: Can be an @DrawableRes? + * @return a new Source object, which can be passed to + * {@link #decodeDrawable} or {@link #decodeBitmap}. + * @throws Resources.NotFoundException if the asset does not exist. + */ + public static Source createSource(@NonNull Resources res, @RawRes int resId) + throws Resources.NotFoundException { + return new ResourceSource(res, resId); + } + + /** + * Create a new {@link Source} from a byte array. + * @param data byte array of compressed image data. + * @param offset offset into data for where the decoder should begin + * parsing. + * @param length number of bytes, beginning at offset, to parse. + * @throws NullPointerException if data is null. + * @throws ArrayIndexOutOfBoundsException if offset and length are + * not within data. + */ + // TODO: Overloads that don't use offset, length + public static Source createSource(@NonNull byte[] data, int offset, + int length) throws ArrayIndexOutOfBoundsException { + if (data == null) { + throw new NullPointerException("null byte[] in createSource!"); + } + if (offset < 0 || length < 0 || offset >= data.length || + offset + length > data.length) { + throw new ArrayIndexOutOfBoundsException( + "invalid offset/length!"); + } + return new ByteArraySource(data, offset, length); + } + + /** + * Create a new {@link Source} from a {@link java.nio.ByteBuffer}. + * + * The returned {@link Source} effectively takes ownership of the + * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after + * this call. + * + * Decoding will start from {@link java.nio.ByteBuffer#position()}. + */ + public static Source createSource(ByteBuffer buffer) { + return new ByteBufferSource(buffer); + } + + /** + * Return the width and height of a given sample size. + * + * This takes an input that functions like + * {@link BitmapFactory.Options#inSampleSize}. It returns a width and + * height that can be acheived by sampling the encoded image. Other widths + * and heights may be supported, but will require an additional (internal) + * scaling step. Such internal scaling is *not* supported with + * {@link #requireUnpremultiplied}. + * + * @param sampleSize Sampling rate of the encoded image. + * @return Point {@link Point#x} and {@link Point#y} correspond to the + * width and height after sampling. + */ + public Point getSampledSize(int sampleSize) { + if (sampleSize <= 0) { + throw new IllegalArgumentException("sampleSize must be positive! " + + "provided " + sampleSize); + } + if (mNativePtr == 0) { + throw new IllegalStateException("ImageDecoder is recycled!"); + } + + return nGetSampledSize(mNativePtr, sampleSize); + } + + // Modifiers + /** + * Resize the output to have the following size. + * + * @param width must be greater than 0. + * @param height must be greater than 0. + */ + public void resize(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Dimensions must be positive! " + + "provided (" + width + ", " + height + ")"); + } + + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Resize based on a sample size. + * + * This has the same effect as passing the result of + * {@link #getSampledSize} to {@link #resize(int, int)}. + * + * @param sampleSize Sampling rate of the encoded image. + */ + public void resize(int sampleSize) { + Point dimensions = this.getSampledSize(sampleSize); + this.resize(dimensions.x, dimensions.y); + } + + // These need to stay in sync with ImageDecoder.cpp's Allocator enum. + /** + * Use the default allocation for the pixel memory. + * + * Will typically result in a {@link Bitmap.Config#HARDWARE} + * allocation, but may be software for small images. In addition, this will + * switch to software when HARDWARE is incompatible, e.g. + * {@link #setMutable}, {@link #setAsAlphaMask}. + */ + public static final int DEFAULT_ALLOCATOR = 0; + + /** + * Use a software allocation for the pixel memory. + * + * Useful for drawing to a software {@link Canvas} or for + * accessing the pixels on the final output. + */ + public static final int SOFTWARE_ALLOCATOR = 1; + + /** + * Use shared memory for the pixel memory. + * + * Useful for sharing across processes. + */ + public static final int SHARED_MEMORY_ALLOCATOR = 2; + + /** + * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. + * + * This will throw an {@link java.lang.IllegalStateException} when combined + * with incompatible options, like {@link #setMutable} or + * {@link #setAsAlphaMask}. + */ + public static final int HARDWARE_ALLOCATOR = 3; + + /** @hide **/ + @Retention(SOURCE) + @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR, + HARDWARE_ALLOCATOR }) + public @interface Allocator {}; + + /** + * Choose the backing for the pixel memory. + * + * This is ignored for animated drawables. + * + * TODO: Allow accessing the backing from the Bitmap. + * + * @param allocator Type of allocator to use. + */ + public void setAllocator(@Allocator int allocator) { + if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) { + throw new IllegalArgumentException("invalid allocator " + allocator); + } + mAllocator = allocator; + } + + /** + * Create a {@link Bitmap} with unpremultiplied pixels. + * + * By default, ImageDecoder will create a {@link Bitmap} with + * premultiplied pixels, which is required for drawing with the + * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling + * this method will result in {@link #decodeBitmap} returning a + * {@link Bitmap} with unpremultiplied pixels. See + * {@link Bitmap#isPremultiplied}. Incompatible with + * {@link #decodeDrawable}; attempting to decode an unpremultiplied + * {@link Drawable} will throw an {@link java.lang.IllegalStateException}. + */ + public void requireUnpremultiplied() { + mRequireUnpremultiplied = true; + } + + /** + * Modify the image after decoding and scaling. + * + * This allows adding effects prior to returning a {@link Drawable} or + * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap}, + * this is the only way to process the image after decoding. + * + * If set on a nine-patch image, the nine-patch data is ignored. + * + * For an animated image, the drawing commands drawn on the {@link Canvas} + * will be recorded immediately and then applied to each frame. + */ + public void setPostProcess(PostProcess p) { + mPostProcess = p; + } + + /** + * Set (replace) the {@link OnExceptionListener} on this object. + * + * Will be called if there is an error in the input. Without one, a + * partial {@link Bitmap} will be created. + */ + public void setOnExceptionListener(OnExceptionListener l) { + mOnExceptionListener = l; + } + + /** + * Crop the output to {@code subset} of the (possibly) scaled image. + * + * {@code subset} must be contained within the size set by {@link #resize} + * or the bounds of the image if resize was not called. Otherwise an + * {@link IllegalStateException} will be thrown. + * + * NOT intended as a replacement for + * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats, + * but merely crops the output. + */ + public void crop(Rect subset) { + mCropRect = subset; + } + + /** + * Create a mutable {@link Bitmap}. + * + * By default, a {@link Bitmap} created will be immutable, but that can be + * changed with this call. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}, because + * {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to + * combine them will throw an {@link java.lang.IllegalStateException}. + * + * Incompatible with {@link #decodeDrawable}, which would require + * retrieving the Bitmap from the returned Drawable in order to modify. + * Attempting to decode a mutable {@link Drawable} will throw an + * {@link java.lang.IllegalStateException} + */ + public void setMutable() { + mMutable = true; + } + + /** + * Potentially save RAM at the expense of quality. + * + * This may result in a {@link Bitmap} with a denser {@link Bitmap.Config}, + * depending on the image. For example, for an opaque {@link Bitmap}, this + * may result in a {@link Bitmap.Config} with no alpha information. + */ + public void setPreferRamOverQuality() { + mPreferRamOverQuality = true; + } + + /** + * Potentially treat the output as an alpha mask. + * + * If the image is encoded in a format with only one channel, treat that + * channel as alpha. Otherwise this call has no effect. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them + * will throw an {@link java.lang.IllegalStateException}. + */ + public void setAsAlphaMask() { + mAsAlphaMask = true; + } + + /** + * Clean up resources. + * + * ImageDecoder has a private constructor, and will always be recycled + * by decodeDrawable or decodeBitmap which creates it, so there is no + * need for a finalizer. + */ + private void recycle() { + if (mNativePtr == 0) { + return; + } + nRecycle(mNativePtr); + mNativePtr = 0; + } + + private void checkState() { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot reuse ImageDecoder.Source!"); + } + + checkSubset(mDesiredWidth, mDesiredHeight, mCropRect); + + if (mAllocator == HARDWARE_ALLOCATOR) { + if (mMutable) { + throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!"); + } + if (mAsAlphaMask) { + throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!"); + } + } + + if (mPostProcess != null && mRequireUnpremultiplied) { + throw new IllegalStateException("Cannot draw to unpremultiplied pixels!"); + } + } + + private static void checkSubset(int width, int height, Rect r) { + if (r == null) { + return; + } + if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { + throw new IllegalStateException("Subset " + r + " not contained by " + + "scaled image bounds: (" + width + " x " + height + ")"); + } + } + + /** + * Create a {@link Drawable}. + */ + public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + if (decoder.mRequireUnpremultiplied) { + // Though this could be supported (ignored) for opaque images, it + // seems better to always report this error. + throw new IllegalStateException("Cannot decode a Drawable with" + + " unpremultiplied pixels!"); + } + + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } + + try { + Bitmap bm = nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + false, // decoder.mMutable + decoder.mAllocator, + false, // decoder.mRequireUnpremultiplied + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask + ); + if (bm == null) { + return null; + } + + Resources res = src.getResources(); + if (res == null) { + bm.setDensity(Bitmap.DENSITY_NONE); + } + + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = new Rect(); + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); + } + + // TODO: Handle animation. + return new BitmapDrawable(res, bm); + } finally { + decoder.recycle(); + src.close(); + } + } + + /** + * Create a {@link Bitmap}. + */ + public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + try { + return nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + decoder.mMutable, + decoder.mAllocator, + decoder.mRequireUnpremultiplied, + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask); + } finally { + decoder.recycle(); + src.close(); + } + } + + private static native ImageDecoder nCreate(long asset); + private static native ImageDecoder nCreate(ByteBuffer buffer, + int position, + int limit); + private static native ImageDecoder nCreate(byte[] data, int offset, + int length); + private static native Bitmap nDecodeBitmap(long nativePtr, + OnExceptionListener listener, + PostProcess postProcess, + int width, int height, + Rect cropRect, boolean mutable, + int allocator, boolean requireUnpremul, + boolean preferRamOverQuality, boolean asAlphaMask); + private static native Point nGetSampledSize(long nativePtr, + int sampleSize); + private static native void nGetPadding(long nativePtr, Rect outRect); + private static native void nRecycle(long nativePtr); +} diff --git a/android/graphics/Point.java b/android/graphics/Point.java index abcccbdb..c6b6c668 100644 --- a/android/graphics/Point.java +++ b/android/graphics/Point.java @@ -18,6 +18,7 @@ package android.graphics; import android.os.Parcel; import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; @@ -121,6 +122,21 @@ public class Point implements Parcelable { out.writeInt(y); } + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.graphics.PointProto} + * + * @param protoOutputStream Stream to write the Rect object to. + * @param fieldId Field Id of the Rect as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(PointProto.X, x); + protoOutputStream.write(PointProto.Y, y); + protoOutputStream.end(token); + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { /** * Return a new point from the data in the specified parcel. diff --git a/android/graphics/PostProcess.java b/android/graphics/PostProcess.java new file mode 100644 index 00000000..c5a31e82 --- /dev/null +++ b/android/graphics/PostProcess.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.graphics.drawable.Drawable; + + +/** + * Helper interface for adding custom processing to an image. + * + * The image being processed may be a {@link Drawable}, {@link Bitmap} or frame + * of an animated image produced by {@link ImageDecoder}. This is called before + * the requested object is returned. + * + * This custom processing also applies to image types that are otherwise + * immutable, such as {@link Bitmap.Config#HARDWARE}. + * + * On an animated image, the callback will only be called once, but the drawing + * commands will be applied to each frame, as if the {@code Canvas} had been + * returned by {@link Picture#beginRecording}. + * + * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}. + * @hide + */ +public interface PostProcess { + /** + * Do any processing after (for example) decoding. + * + * Drawing to the {@link Canvas} will behave as if the initial processing + * (e.g. decoding) already exists in the Canvas. An implementation can draw + * effects on top of this, or it can even draw behind it using + * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency + * to the corners to achieve rounded corners. That can be done with the + * following code: + * + * + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * + * + * + * @param canvas The {@link Canvas} to draw to. + * @param width Width of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @param height Height of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @return Opacity of the result after drawing. + * {@link PixelFormat#UNKNOWN} means that the implementation did not + * change whether the image has alpha. Return this unless you added + * transparency (e.g. with the code above, in which case you should + * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to + * be opaque (e.g. by drawing everywhere with an opaque color and + * {@code PorterDuff.Mode.DST_OVER}, in which case you should return + * {@code PixelFormat.OPAQUE}). + * {@link PixelFormat#TRANSLUCENT} means that the implementation added + * transparency. This is safe to return even if the image already had + * transparency. This is also safe to return if the result is opaque, + * though it may draw more slowly. + * {@link PixelFormat#OPAQUE} means that the implementation forced the + * image to be opaque. This is safe to return even if the image was + * already opaque. + * {@link PixelFormat#TRANSPARENT} (or any other integer) is not + * allowed, and will result in throwing an + * {@link java.lang.IllegalArgumentException}. + */ + @PixelFormat.Opacity + public int postProcess(@NonNull Canvas canvas, int width, int height); +} diff --git a/android/graphics/drawable/RippleComponent.java b/android/graphics/drawable/RippleComponent.java index 0e38826e..626bcee9 100644 --- a/android/graphics/drawable/RippleComponent.java +++ b/android/graphics/drawable/RippleComponent.java @@ -93,12 +93,8 @@ abstract class RippleComponent { protected final void onHotspotBoundsChanged() { if (!mHasMaxRadius) { - final float halfWidth = mBounds.width() / 2.0f; - final float halfHeight = mBounds.height() / 2.0f; - final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth - + halfHeight * halfHeight); - - onTargetRadiusChanged(targetRadius); + mTargetRadius = getTargetRadius(mBounds); + onTargetRadiusChanged(mTargetRadius); } } diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java index 8b185f2b..734cff54 100644 --- a/android/graphics/drawable/RippleDrawable.java +++ b/android/graphics/drawable/RippleDrawable.java @@ -299,6 +299,12 @@ public class RippleDrawable extends LayerDrawable { onHotspotBoundsChanged(); } + final int count = mExitingRipplesCount; + final RippleForeground[] ripples = mExitingRipples; + for (int i = 0; i < count; i++) { + ripples[i].onBoundsChange(); + } + if (mBackground != null) { mBackground.onBoundsChange(); } @@ -560,8 +566,7 @@ public class RippleDrawable extends LayerDrawable { y = mHotspotBounds.exactCenterY(); } - final boolean isBounded = isBounded(); - mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware); + mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware); } mRipple.setup(mState.mMaxRadius, mDensity); diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java index 0b5020cb..ecbf5780 100644 --- a/android/graphics/drawable/RippleForeground.java +++ b/android/graphics/drawable/RippleForeground.java @@ -30,6 +30,7 @@ import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; import java.util.ArrayList; @@ -38,18 +39,14 @@ import java.util.ArrayList; */ class RippleForeground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( - 400f, 1.4f, 0); + // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that + private static final TimeInterpolator DECELERATE_INTERPOLATOR = + new PathInterpolator(0.4f, 0f, 0.2f, 1f); - // Pixel-based accelerations and velocities. - private static final float WAVE_TOUCH_DOWN_ACCELERATION = 2048; - private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; - - // Bounded ripple animation properties. - private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300; - private static final int BOUNDED_RADIUS_EXIT_DURATION = 800; - private static final int BOUNDED_OPACITY_EXIT_DURATION = 400; - private static final float MAX_BOUNDED_RADIUS = 350; + // Time it takes for the ripple to expand + private static final int RIPPLE_ENTER_DURATION = 225; + // Time it takes for the ripple to slide from the touch to the center point + private static final int RIPPLE_ORIGIN_DURATION = 225; private static final int OPACITY_ENTER_DURATION = 75; private static final int OPACITY_EXIT_DURATION = 150; @@ -71,9 +68,6 @@ class RippleForeground extends RippleComponent { private float mTargetX = 0; private float mTargetY = 0; - /** Ripple target radius used when bounded. Not used for clamping. */ - private float mBoundedRadius = 0; - // Software rendering properties. private float mOpacity = 0; @@ -107,19 +101,13 @@ class RippleForeground extends RippleComponent { private float mStartRadius = 0; public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, - boolean isBounded, boolean forceSoftware) { + boolean forceSoftware) { super(owner, bounds); mForceSoftware = forceSoftware; mStartingX = startingX; mStartingY = startingY; - if (isBounded) { - mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f - + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1); - } else { - mBoundedRadius = 0; - } // Take 60% of the maximum of the width and height, then divided half to get the radius. mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f; } @@ -127,6 +115,7 @@ class RippleForeground extends RippleComponent { @Override protected void onTargetRadiusChanged(float targetRadius) { clampStartingPosition(); + switchToUiThreadAnimation(); } private void drawSoftware(Canvas c, Paint p) { @@ -228,16 +217,14 @@ class RippleForeground extends RippleComponent { } mRunningSwAnimators.clear(); - final int duration = getRadiusDuration(); - final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); - tweenRadius.setDuration(duration); + tweenRadius.setDuration(RIPPLE_ENTER_DURATION); tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); tweenRadius.start(); mRunningSwAnimators.add(tweenRadius); final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); - tweenOrigin.setDuration(duration); + tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION); tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); tweenOrigin.start(); mRunningSwAnimators.add(tweenOrigin); @@ -267,20 +254,18 @@ class RippleForeground extends RippleComponent { final Paint paint = mOwner.getRipplePaint(); mPropPaint = CanvasProperty.createPaint(paint); - final int radiusDuration = getRadiusDuration(); - final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); - radius.setDuration(radiusDuration); + radius.setDuration(RIPPLE_ORIGIN_DURATION); radius.setInterpolator(DECELERATE_INTERPOLATOR); mPendingHwAnimators.add(radius); final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); - x.setDuration(radiusDuration); + x.setDuration(RIPPLE_ORIGIN_DURATION); x.setInterpolator(DECELERATE_INTERPOLATOR); mPendingHwAnimators.add(x); final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); - y.setDuration(radiusDuration); + y.setDuration(RIPPLE_ORIGIN_DURATION); y.setInterpolator(DECELERATE_INTERPOLATOR); mPendingHwAnimators.add(y); @@ -333,12 +318,6 @@ class RippleForeground extends RippleComponent { return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); } - private int getRadiusDuration() { - final float remainingRadius = mTargetRadius - getCurrentRadius(); - return (int) (1000 * Math.sqrt(remainingRadius / WAVE_TOUCH_DOWN_ACCELERATION * - mDensityScale) + 0.5); - } - private float getCurrentRadius() { return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); } @@ -402,6 +381,14 @@ class RippleForeground extends RippleComponent { } } + private void clearHwProps() { + mPropPaint = null; + mPropRadius = null; + mPropX = null; + mPropY = null; + mUsingProperties = false; + } + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { @@ -410,39 +397,20 @@ class RippleForeground extends RippleComponent { pruneSwFinished(); if (mRunningHwAnimators.isEmpty()) { - mPropPaint = null; - mPropRadius = null; - mPropX = null; - mPropY = null; + clearHwProps(); } } }; - /** - * Interpolator with a smooth log deceleration. - */ - private static final class LogDecelerateInterpolator implements TimeInterpolator { - private final float mBase; - private final float mDrift; - private final float mTimeScale; - private final float mOutputScale; - - public LogDecelerateInterpolator(float base, float timeScale, float drift) { - mBase = base; - mDrift = drift; - mTimeScale = 1f / timeScale; - - mOutputScale = 1f / computeLog(1f); - } - - private float computeLog(float t) { - return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); - } - - @Override - public float getInterpolation(float t) { - return computeLog(t) * mOutputScale; + private void switchToUiThreadAnimation() { + for (int i = 0; i < mRunningHwAnimators.size(); i++) { + Animator animator = mRunningHwAnimators.get(i); + animator.removeListener(mAnimationListener); + animator.end(); } + mRunningHwAnimators.clear(); + clearHwProps(); + invalidateSelf(); } /** diff --git a/android/graphics/drawable/VectorDrawable.java b/android/graphics/drawable/VectorDrawable.java index 7b2e21a4..c71585f3 100644 --- a/android/graphics/drawable/VectorDrawable.java +++ b/android/graphics/drawable/VectorDrawable.java @@ -896,6 +896,13 @@ public class VectorDrawable extends Drawable { return mVectorState.getNativeRenderer(); } + /** + * @hide + */ + public void setAntiAlias(boolean aa) { + nSetAntiAlias(mVectorState.mNativeTree.get(), aa); + } + static class VectorDrawableState extends ConstantState { // Variables below need to be copied (deep copy if applicable) for mutation. int[] mThemeAttrs; @@ -2269,6 +2276,8 @@ public class VectorDrawable extends Drawable { @FastNative private static native float nGetRootAlpha(long rendererPtr); @FastNative + private static native void nSetAntiAlias(long rendererPtr, boolean aa); + @FastNative private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching); @FastNative diff --git a/android/graphics/drawable/VectorDrawable_Delegate.java b/android/graphics/drawable/VectorDrawable_Delegate.java index 00630464..d9f8692e 100644 --- a/android/graphics/drawable/VectorDrawable_Delegate.java +++ b/android/graphics/drawable/VectorDrawable_Delegate.java @@ -134,6 +134,12 @@ public class VectorDrawable_Delegate { return nativePathRenderer.getRootAlpha(); } + @LayoutlibDelegate + static void nSetAntiAlias(long rendererPtr, boolean aa) { + VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr); + nativePathRenderer.setAntiAlias(aa); + } + @LayoutlibDelegate static void nSetAllowCaching(long rendererPtr, boolean allowCaching) { // ignored @@ -1060,6 +1066,7 @@ public class VectorDrawable_Delegate { private Paint mStrokePaint; private Paint mFillPaint; private PathMeasure mPathMeasure; + private boolean mAntiAlias = true; private VPathRenderer_Delegate(long rootGroupPtr) { mRootGroupPtr = rootGroupPtr; @@ -1169,7 +1176,7 @@ public class VectorDrawable_Delegate { if (mFillPaint == null) { mFillPaint = new Paint(); mFillPaint.setStyle(Style.FILL); - mFillPaint.setAntiAlias(true); + mFillPaint.setAntiAlias(mAntiAlias); } final Paint fillPaint = mFillPaint; @@ -1203,7 +1210,7 @@ public class VectorDrawable_Delegate { if (mStrokePaint == null) { mStrokePaint = new Paint(); mStrokePaint.setStyle(Style.STROKE); - mStrokePaint.setAntiAlias(true); + mStrokePaint.setAntiAlias(mAntiAlias); } final Paint strokePaint = mStrokePaint; @@ -1261,6 +1268,10 @@ public class VectorDrawable_Delegate { return matrixScale; } + private void setAntiAlias(boolean aa) { + mAntiAlias = aa; + } + @Override public void setName(String name) { } diff --git a/android/graphics/perftests/PaintMeasureTextTest.java b/android/graphics/perftests/PaintMeasureTextTest.java index b81908c3..b9ee6133 100644 --- a/android/graphics/perftests/PaintMeasureTextTest.java +++ b/android/graphics/perftests/PaintMeasureTextTest.java @@ -80,11 +80,11 @@ public class PaintMeasureTextTest { } while (state.keepRunning()) { - state.pauseTiming(); if (mCacheMode == DONT_USE_CACHE) { + state.pauseTiming(); Canvas.freeTextLayoutCaches(); + state.resumeTiming(); } - state.resumeTiming(); paint.measureText(mText); } diff --git a/android/hardware/HardwareBuffer.java b/android/hardware/HardwareBuffer.java index 7049628b..7866b52c 100644 --- a/android/hardware/HardwareBuffer.java +++ b/android/hardware/HardwareBuffer.java @@ -17,6 +17,7 @@ package android.hardware; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -41,7 +42,15 @@ import libcore.util.NativeAllocationRegistry; public final class HardwareBuffer implements Parcelable, AutoCloseable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RGBA_8888, RGBA_FP16, RGBA_1010102, RGBX_8888, RGB_888, RGB_565, BLOB}) + @IntDef(prefix = { "RGB", "BLOB" }, value = { + RGBA_8888, + RGBA_FP16, + RGBA_1010102, + RGBX_8888, + RGB_888, + RGB_565, + BLOB + }) public @interface Format { } @@ -70,7 +79,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN, + @LongDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN, USAGE_CPU_WRITE_RARELY, USAGE_CPU_WRITE_OFTEN, USAGE_GPU_SAMPLED_IMAGE, USAGE_GPU_COLOR_OUTPUT, USAGE_PROTECTED_CONTENT, USAGE_VIDEO_ENCODE, USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA}) diff --git a/android/hardware/SensorAdditionalInfo.java b/android/hardware/SensorAdditionalInfo.java index 7c876cfc..5ff627f4 100644 --- a/android/hardware/SensorAdditionalInfo.java +++ b/android/hardware/SensorAdditionalInfo.java @@ -68,8 +68,15 @@ public class SensorAdditionalInfo { * * @hide */ - @IntDef({TYPE_FRAME_BEGIN, TYPE_FRAME_END, TYPE_UNTRACKED_DELAY, TYPE_INTERNAL_TEMPERATURE, - TYPE_VEC3_CALIBRATION, TYPE_SENSOR_PLACEMENT, TYPE_SAMPLING}) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_FRAME_BEGIN, + TYPE_FRAME_END, + TYPE_UNTRACKED_DELAY, + TYPE_INTERNAL_TEMPERATURE, + TYPE_VEC3_CALIBRATION, + TYPE_SENSOR_PLACEMENT, + TYPE_SAMPLING + }) @Retention(RetentionPolicy.SOURCE) public @interface AdditionalInfoType {} diff --git a/android/hardware/SensorDirectChannel.java b/android/hardware/SensorDirectChannel.java index 36607c90..214d3ec2 100644 --- a/android/hardware/SensorDirectChannel.java +++ b/android/hardware/SensorDirectChannel.java @@ -40,8 +40,12 @@ public final class SensorDirectChannel implements Channel { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {TYPE_MEMORY_FILE, TYPE_HARDWARE_BUFFER}) - public @interface MemoryType {}; + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_MEMORY_FILE, + TYPE_HARDWARE_BUFFER + }) + public @interface MemoryType {} + /** * Shared memory type ashmem, wrapped in MemoryFile object. * @@ -60,8 +64,13 @@ public final class SensorDirectChannel implements Channel { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {RATE_STOP, RATE_NORMAL, RATE_FAST, RATE_VERY_FAST}) - public @interface RateLevel {}; + @IntDef(flag = true, prefix = { "RATE_" }, value = { + RATE_STOP, + RATE_NORMAL, + RATE_FAST, + RATE_VERY_FAST + }) + public @interface RateLevel {} /** * Sensor stopped (no event output). diff --git a/android/hardware/camera2/CameraAccessException.java b/android/hardware/camera2/CameraAccessException.java index f9b659c6..d238797c 100644 --- a/android/hardware/camera2/CameraAccessException.java +++ b/android/hardware/camera2/CameraAccessException.java @@ -16,7 +16,6 @@ package android.hardware.camera2; -import android.annotation.NonNull; import android.annotation.IntDef; import android.util.AndroidException; @@ -81,15 +80,16 @@ public class CameraAccessException extends AndroidException { */ public static final int CAMERA_DEPRECATED_HAL = 1000; - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef( - {CAMERA_IN_USE, - MAX_CAMERAS_IN_USE, - CAMERA_DISABLED, - CAMERA_DISCONNECTED, - CAMERA_ERROR}) - public @interface AccessError {}; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "CAMERA_", "MAX_CAMERAS_IN_USE" }, value = { + CAMERA_IN_USE, + MAX_CAMERAS_IN_USE, + CAMERA_DISABLED, + CAMERA_DISCONNECTED, + CAMERA_ERROR + }) + public @interface AccessError {} // Make the eclipse warning about serializable exceptions go away private static final long serialVersionUID = 5630338637471475675L; // randomly generated diff --git a/android/hardware/camera2/CameraCharacteristics.java b/android/hardware/camera2/CameraCharacteristics.java index 3a3048ef..57ab18e2 100644 --- a/android/hardware/camera2/CameraCharacteristics.java +++ b/android/hardware/camera2/CameraCharacteristics.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; +import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.utils.TypeReference; import android.util.Rational; @@ -169,6 +170,7 @@ public final class CameraCharacteristics extends CameraMetadata> mKeys; private List> mAvailableRequestKeys; + private List> mAvailableSessionKeys; private List> mAvailableResultKeys; /** @@ -250,6 +252,67 @@ public final class CameraCharacteristics extends CameraMetadataReturns a subset of {@link #getAvailableCaptureRequestKeys} keys that the + * camera device can pass as part of the capture session initialization.

      + * + *

      This list includes keys that are difficult to apply per-frame and + * can result in unexpected delays when modified during the capture session + * lifetime. Typical examples include parameters that require a + * time-consuming hardware re-configuration or internal camera pipeline + * change. For performance reasons we suggest clients to pass their initial + * values as part of {@link SessionConfiguration#setSessionParameters}. Once + * the camera capture session is enabled it is also recommended to avoid + * changing them from their initial values set in + * {@link SessionConfiguration#setSessionParameters }. + * Control over session parameters can still be exerted in capture requests + * but clients should be aware and expect delays during their application. + * An example usage scenario could look like this:

      + *
        + *
      • The camera client starts by quering the session parameter key list via + * {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.
      • + *
      • 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.
      • + *
      • 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.
      • + *
      • If there is no such match, the capture request can be passed + * unmodified to {@link SessionConfiguration#setSessionParameters }.
      • + *
      • If matches do exist, the client should update the respective values + * and pass the request to {@link SessionConfiguration#setSessionParameters }.
      • + *
      • After the capture session initialization completes the session parameter + * key list can continue to serve as reference when posting or updating + * further requests. As mentioned above further changes to session + * parameters should ideally be avoided, if updates are necessary + * however clients could expect a delay/glitch during the + * parameter switch.
      • + *
      + * + *

      The list returned is not modifiable, so any attempts to modify it will throw + * a {@code UnsupportedOperationException}.

      + * + *

      Each key is only listed once in the list. The order of the keys is undefined.

      + * + * @return List of keys that can be passed during capture session initialization. In case the + * camera device doesn't support such keys the list can be null. + */ + @SuppressWarnings({"unchecked"}) + public List> getAvailableSessionKeys() { + if (mAvailableSessionKeys == null) { + Object crKey = CaptureRequest.Key.class; + Class> crKeyTyped = (Class>)crKey; + + int[] filterTags = get(REQUEST_AVAILABLE_SESSION_KEYS); + if (filterTags == null) { + return null; + } + mAvailableSessionKeys = + getAvailableKeyList(CaptureRequest.class, crKeyTyped, filterTags); + } + return mAvailableSessionKeys; + } + /** * Returns the list of keys supported by this {@link CameraDevice} for querying * with a {@link CaptureRequest}. @@ -1570,6 +1633,48 @@ public final class CameraCharacteristics extends CameraMetadata REQUEST_AVAILABLE_CHARACTERISTICS_KEYS = new Key("android.request.availableCharacteristicsKeys", int[].class); + /** + *

      A subset of the available request keys that the camera device + * can pass as part of the capture session initialization.

      + *

      This is a subset of android.request.availableRequestKeys which + * contains a list of keys that are difficult to apply per-frame and + * can result in unexpected delays when modified during the capture session + * lifetime. Typical examples include parameters that require a + * time-consuming hardware re-configuration or internal camera pipeline + * change. For performance reasons we advise clients to pass their initial + * values as part of {@link SessionConfiguration#setSessionParameters }. Once + * the camera capture session is enabled it is also recommended to avoid + * changing them from their initial values set in + * {@link SessionConfiguration#setSessionParameters }. + * Control over session parameters can still be exerted in capture requests + * but clients should be aware and expect delays during their application. + * An example usage scenario could look like this:

      + *
        + *
      • The camera client starts by quering the session parameter key list via + * {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.
      • + *
      • 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.
      • + *
      • 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.
      • + *
      • If there is no such match, the capture request can be passed + * unmodified to {@link SessionConfiguration#setSessionParameters }.
      • + *
      • If matches do exist, the client should update the respective values + * and pass the request to {@link SessionConfiguration#setSessionParameters }.
      • + *
      • After the capture session initialization completes the session parameter + * key list can continue to serve as reference when posting or updating + * further requests. As mentioned above further changes to session + * parameters should ideally be avoided, if updates are necessary + * however clients could expect a delay/glitch during the + * parameter switch.
      • + *
      + *

      This key is available on all devices.

      + * @hide + */ + public static final Key REQUEST_AVAILABLE_SESSION_KEYS = + new Key("android.request.availableSessionKeys", int[].class); + /** *

      The list of image formats that are supported by this * camera device for output streams.

      diff --git a/android/hardware/camera2/CameraDevice.java b/android/hardware/camera2/CameraDevice.java index 55343a29..87e503de 100644 --- a/android/hardware/camera2/CameraDevice.java +++ b/android/hardware/camera2/CameraDevice.java @@ -26,6 +26,7 @@ import static android.hardware.camera2.ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_ import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; import android.os.Handler; import android.view.Surface; @@ -810,6 +811,26 @@ public abstract class CameraDevice implements AutoCloseable { @Nullable Handler handler) throws CameraAccessException; + /** + *

      Create a new {@link CameraCaptureSession} using a {@link SessionConfiguration} helper + * object that aggregates all supported parameters.

      + * + * @param config A session configuration (see {@link SessionConfiguration}). + * + * @throws IllegalArgumentException In case the session configuration is invalid; or the output + * configurations are empty. + * @throws CameraAccessException In case the camera device is no longer connected or has + * encountered a fatal error. + * @see #createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) + * @see #createCaptureSessionByOutputConfigurations + * @see #createReprocessableCaptureSession + * @see #createConstrainedHighSpeedCaptureSession + */ + public void createCaptureSession( + SessionConfiguration config) throws CameraAccessException { + throw new UnsupportedOperationException("No default implementation"); + } + /** *

      Create a {@link CaptureRequest.Builder} for new capture requests, * initialized with template for a target use case. The settings are chosen diff --git a/android/hardware/camera2/CameraMetadata.java b/android/hardware/camera2/CameraMetadata.java index 4b57018b..cb11d0f5 100644 --- a/android/hardware/camera2/CameraMetadata.java +++ b/android/hardware/camera2/CameraMetadata.java @@ -2724,6 +2724,22 @@ public abstract class CameraMetadata { */ public static final int CONTROL_AWB_STATE_LOCKED = 3; + // + // Enumeration values for CaptureResult#CONTROL_AF_SCENE_CHANGE + // + + /** + *

      Scene change is not detected within the AF region(s).

      + * @see CaptureResult#CONTROL_AF_SCENE_CHANGE + */ + public static final int CONTROL_AF_SCENE_CHANGE_NOT_DETECTED = 0; + + /** + *

      Scene change is detected within the AF region(s).

      + * @see CaptureResult#CONTROL_AF_SCENE_CHANGE + */ + public static final int CONTROL_AF_SCENE_CHANGE_DETECTED = 1; + // // Enumeration values for CaptureResult#FLASH_STATE // diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java index 0262ecb5..77da2a51 100644 --- a/android/hardware/camera2/CaptureRequest.java +++ b/android/hardware/camera2/CaptureRequest.java @@ -21,15 +21,18 @@ import android.annotation.Nullable; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; +import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.HashCodeHelpers; import android.hardware.camera2.utils.TypeReference; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; import android.view.Surface; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -198,7 +201,24 @@ public final class CaptureRequest extends CameraMetadata> } } - private final HashSet mSurfaceSet; + private final String TAG = "CaptureRequest-JV"; + + private final ArraySet mSurfaceSet = new ArraySet(); + + // Speed up sending CaptureRequest across IPC: + // mSurfaceConverted should only be set to true during capture request + // submission by {@link #convertSurfaceToStreamId}. The method will convert + // surfaces to stream/surface indexes based on passed in stream configuration at that time. + // This will save significant unparcel time for remote camera device. + // Once the request is submitted, camera device will call {@link #recoverStreamIdToSurface} + // to reset the capture request back to its original state. + private final Object mSurfacesLock = new Object(); + private boolean mSurfaceConverted = false; + private int[] mStreamIdxArray; + private int[] mSurfaceIdxArray; + + private static final ArraySet mEmptySurfaceSet = new ArraySet(); + private final CameraMetadataNative mSettings; private boolean mIsReprocess; // If this request is part of constrained high speed request list that was created by @@ -218,7 +238,6 @@ public final class CaptureRequest extends CameraMetadata> private CaptureRequest() { mSettings = new CameraMetadataNative(); setNativeInstance(mSettings); - mSurfaceSet = new HashSet(); mIsReprocess = false; mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE; } @@ -232,7 +251,7 @@ public final class CaptureRequest extends CameraMetadata> private CaptureRequest(CaptureRequest source) { mSettings = new CameraMetadataNative(source.mSettings); setNativeInstance(mSettings); - mSurfaceSet = (HashSet) source.mSurfaceSet.clone(); + mSurfaceSet.addAll(source.mSurfaceSet); mIsReprocess = source.mIsReprocess; mIsPartOfCHSRequestList = source.mIsPartOfCHSRequestList; mReprocessableSessionId = source.mReprocessableSessionId; @@ -263,7 +282,6 @@ public final class CaptureRequest extends CameraMetadata> int reprocessableSessionId) { mSettings = CameraMetadataNative.move(settings); setNativeInstance(mSettings); - mSurfaceSet = new HashSet(); mIsReprocess = isReprocess; if (isReprocess) { if (reprocessableSessionId == CameraCaptureSession.SESSION_ID_NONE) { @@ -463,22 +481,25 @@ public final class CaptureRequest extends CameraMetadata> private void readFromParcel(Parcel in) { mSettings.readFromParcel(in); setNativeInstance(mSettings); - - mSurfaceSet.clear(); - - Parcelable[] parcelableArray = in.readParcelableArray(Surface.class.getClassLoader()); - - if (parcelableArray == null) { - return; - } - - for (Parcelable p : parcelableArray) { - Surface s = (Surface) p; - mSurfaceSet.add(s); - } - mIsReprocess = (in.readInt() == 0) ? false : true; mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE; + + synchronized (mSurfacesLock) { + mSurfaceSet.clear(); + Parcelable[] parcelableArray = in.readParcelableArray(Surface.class.getClassLoader()); + if (parcelableArray != null) { + for (Parcelable p : parcelableArray) { + Surface s = (Surface) p; + mSurfaceSet.add(s); + } + } + // Intentionally disallow java side readFromParcel to receive streamIdx/surfaceIdx + // Since there is no good way to convert indexes back to Surface + int streamSurfaceSize = in.readInt(); + if (streamSurfaceSize != 0) { + throw new RuntimeException("Reading cached CaptureRequest is not supported"); + } + } } @Override @@ -489,8 +510,21 @@ public final class CaptureRequest extends CameraMetadata> @Override public void writeToParcel(Parcel dest, int flags) { mSettings.writeToParcel(dest, flags); - dest.writeParcelableArray(mSurfaceSet.toArray(new Surface[mSurfaceSet.size()]), flags); dest.writeInt(mIsReprocess ? 1 : 0); + + synchronized (mSurfacesLock) { + final ArraySet surfaces = mSurfaceConverted ? mEmptySurfaceSet : mSurfaceSet; + dest.writeParcelableArray(surfaces.toArray(new Surface[surfaces.size()]), flags); + if (mSurfaceConverted) { + dest.writeInt(mStreamIdxArray.length); + for (int i = 0; i < mStreamIdxArray.length; i++) { + dest.writeInt(mStreamIdxArray[i]); + dest.writeInt(mSurfaceIdxArray[i]); + } + } else { + dest.writeInt(0); + } + } } /** @@ -507,6 +541,67 @@ public final class CaptureRequest extends CameraMetadata> return Collections.unmodifiableCollection(mSurfaceSet); } + /** + * @hide + */ + public void convertSurfaceToStreamId( + final SparseArray configuredOutputs) { + synchronized (mSurfacesLock) { + if (mSurfaceConverted) { + Log.v(TAG, "Cannot convert already converted surfaces!"); + return; + } + + mStreamIdxArray = new int[mSurfaceSet.size()]; + mSurfaceIdxArray = new int[mSurfaceSet.size()]; + int i = 0; + for (Surface s : mSurfaceSet) { + boolean streamFound = false; + for (int j = 0; j < configuredOutputs.size(); ++j) { + int streamId = configuredOutputs.keyAt(j); + OutputConfiguration outConfig = configuredOutputs.valueAt(j); + int surfaceId = 0; + for (Surface outSurface : outConfig.getSurfaces()) { + if (s == outSurface) { + streamFound = true; + mStreamIdxArray[i] = streamId; + mSurfaceIdxArray[i] = surfaceId; + i++; + break; + } + surfaceId++; + } + if (streamFound) { + break; + } + } + if (!streamFound) { + mStreamIdxArray = null; + mSurfaceIdxArray = null; + throw new IllegalArgumentException( + "CaptureRequest contains unconfigured Input/Output Surface!"); + } + } + mSurfaceConverted = true; + } + } + + /** + * @hide + */ + public void recoverStreamIdToSurface() { + synchronized (mSurfacesLock) { + if (!mSurfaceConverted) { + Log.v(TAG, "Cannot convert already converted surfaces!"); + return; + } + + mStreamIdxArray = null; + mSurfaceIdxArray = null; + mSurfaceConverted = false; + } + } + /** * A builder for capture requests. * diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java index cfad098c..6d7b06fc 100644 --- a/android/hardware/camera2/CaptureResult.java +++ b/android/hardware/camera2/CaptureResult.java @@ -2184,6 +2184,30 @@ public class CaptureResult extends CameraMetadata> { public static final Key CONTROL_ENABLE_ZSL = new Key("android.control.enableZsl", boolean.class); + /** + *

      Whether a significant scene change is detected within the currently-set AF + * region(s).

      + *

      When the camera focus routine detects a change in the scene it is looking at, + * such as a large shift in camera viewpoint, significant motion in the scene, or a + * significant illumination change, this value will be set to DETECTED for a single capture + * result. Otherwise the value will be NOT_DETECTED. The threshold for detection is similar + * to what would trigger a new passive focus scan to begin in CONTINUOUS autofocus modes.

      + *

      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.

      + *

      This key will be available if the camera device advertises this key via {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.

      + *

      Possible values: + *

        + *
      • {@link #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED NOT_DETECTED}
      • + *
      • {@link #CONTROL_AF_SCENE_CHANGE_DETECTED DETECTED}
      • + *

      + *

      Optional - This value may be {@code null} on some devices.

      + * @see #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED + * @see #CONTROL_AF_SCENE_CHANGE_DETECTED + */ + @PublicKey + public static final Key CONTROL_AF_SCENE_CHANGE = + new Key("android.control.afSceneChange", int.class); + /** *

      Operation mode for edge * enhancement.

      diff --git a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index 374789c6..8b8bbc34 100644 --- a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -800,7 +800,8 @@ public class CameraCaptureSessionImpl extends CameraCaptureSession try { // begin transition to unconfigured mDeviceImpl.configureStreamsChecked(/*inputConfig*/null, /*outputs*/null, - /*operatingMode*/ ICameraDeviceUser.NORMAL_MODE); + /*operatingMode*/ ICameraDeviceUser.NORMAL_MODE, + /*sessionParams*/ null); } catch (CameraAccessException e) { // OK: do not throw checked exceptions. Log.e(TAG, mIdString + "Exception while unconfiguring outputs: ", e); diff --git a/android/hardware/camera2/impl/CameraDeviceImpl.java b/android/hardware/camera2/impl/CameraDeviceImpl.java index 6787d84b..f1ffb890 100644 --- a/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -32,6 +32,7 @@ import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.ReprocessFormatsMap; +import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.utils.SubmitInfo; import android.hardware.camera2.utils.SurfaceUtils; @@ -362,7 +363,7 @@ public class CameraDeviceImpl extends CameraDevice outputConfigs.add(new OutputConfiguration(s)); } configureStreamsChecked(/*inputConfig*/null, outputConfigs, - /*operatingMode*/ICameraDeviceUser.NORMAL_MODE); + /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null); } @@ -382,12 +383,13 @@ public class CameraDeviceImpl extends CameraDevice * @param outputs a list of one or more surfaces, or {@code null} to unconfigure * @param operatingMode If the stream configuration is for a normal session, * a constrained high speed session, or something else. + * @param sessionParams Session parameters. * @return whether or not the configuration was successful * * @throws CameraAccessException if there were any unexpected problems during configuration */ public boolean configureStreamsChecked(InputConfiguration inputConfig, - List outputs, int operatingMode) + List outputs, int operatingMode, CaptureRequest sessionParams) throws CameraAccessException { // Treat a null input the same an empty list if (outputs == null) { @@ -463,7 +465,11 @@ public class CameraDeviceImpl extends CameraDevice } } - mRemoteDevice.endConfigure(operatingMode); + if (sessionParams != null) { + mRemoteDevice.endConfigure(operatingMode, sessionParams.getNativeCopy()); + } else { + mRemoteDevice.endConfigure(operatingMode, null); + } success = true; } catch (IllegalArgumentException e) { @@ -499,7 +505,7 @@ public class CameraDeviceImpl extends CameraDevice outConfigurations.add(new OutputConfiguration(surface)); } createCaptureSessionInternal(null, outConfigurations, callback, handler, - /*operatingMode*/ICameraDeviceUser.NORMAL_MODE); + /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null); } @Override @@ -515,7 +521,7 @@ public class CameraDeviceImpl extends CameraDevice List currentOutputs = new ArrayList<>(outputConfigurations); createCaptureSessionInternal(null, currentOutputs, callback, handler, - /*operatingMode*/ICameraDeviceUser.NORMAL_MODE); + /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/null); } @Override @@ -535,7 +541,7 @@ public class CameraDeviceImpl extends CameraDevice outConfigurations.add(new OutputConfiguration(surface)); } createCaptureSessionInternal(inputConfig, outConfigurations, callback, handler, - /*operatingMode*/ICameraDeviceUser.NORMAL_MODE); + /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null); } @Override @@ -563,7 +569,8 @@ public class CameraDeviceImpl extends CameraDevice currentOutputs.add(new OutputConfiguration(output)); } createCaptureSessionInternal(inputConfig, currentOutputs, - callback, handler, /*operatingMode*/ICameraDeviceUser.NORMAL_MODE); + callback, handler, /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, + /*sessionParams*/ null); } @Override @@ -574,16 +581,13 @@ public class CameraDeviceImpl extends CameraDevice throw new IllegalArgumentException( "Output surface list must not be null and the size must be no more than 2"); } - StreamConfigurationMap config = - getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - SurfaceUtils.checkConstrainedHighSpeedSurfaces(outputs, /*fpsRange*/null, config); - List outConfigurations = new ArrayList<>(outputs.size()); for (Surface surface : outputs) { outConfigurations.add(new OutputConfiguration(surface)); } createCaptureSessionInternal(null, outConfigurations, callback, handler, - /*operatingMode*/ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE); + /*operatingMode*/ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE, + /*sessionParams*/ null); } @Override @@ -596,13 +600,30 @@ public class CameraDeviceImpl extends CameraDevice for (OutputConfiguration output : outputs) { currentOutputs.add(new OutputConfiguration(output)); } - createCaptureSessionInternal(inputConfig, currentOutputs, callback, handler, operatingMode); + createCaptureSessionInternal(inputConfig, currentOutputs, callback, handler, operatingMode, + /*sessionParams*/ null); + } + + @Override + public void createCaptureSession(SessionConfiguration config) + throws CameraAccessException { + if (config == null) { + throw new IllegalArgumentException("Invalid session configuration"); + } + + List outputConfigs = config.getOutputConfigurations(); + if (outputConfigs == null) { + throw new IllegalArgumentException("Invalid output configurations"); + } + createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs, + config.getStateCallback(), config.getHandler(), config.getSessionType(), + config.getSessionParameters()); } private void createCaptureSessionInternal(InputConfiguration inputConfig, List outputConfigurations, CameraCaptureSession.StateCallback callback, Handler handler, - int operatingMode) throws CameraAccessException { + int operatingMode, CaptureRequest sessionParams) throws CameraAccessException { synchronized(mInterfaceLock) { if (DEBUG) { Log.d(TAG, "createCaptureSessionInternal"); @@ -630,7 +651,7 @@ public class CameraDeviceImpl extends CameraDevice try { // configure streams and then block until IDLE configureSuccess = configureStreamsChecked(inputConfig, outputConfigurations, - operatingMode); + operatingMode, sessionParams); if (configureSuccess == true && inputConfig != null) { input = mRemoteDevice.getInputSurface(); } @@ -646,6 +667,14 @@ public class CameraDeviceImpl extends CameraDevice // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise. CameraCaptureSessionCore newSession = null; if (isConstrainedHighSpeed) { + ArrayList surfaces = new ArrayList<>(outputConfigurations.size()); + for (OutputConfiguration outConfig : outputConfigurations) { + surfaces.add(outConfig.getSurface()); + } + StreamConfigurationMap config = + getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + SurfaceUtils.checkConstrainedHighSpeedSurfaces(surfaces, /*fpsRange*/null, config); + newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++, callback, handler, this, mDeviceHandler, configureSuccess, mCharacteristics); @@ -779,6 +808,7 @@ public class CameraDeviceImpl extends CameraDevice } mRemoteDevice.updateOutputConfiguration(streamId, config); + mConfiguredOutputs.put(streamId, config); } } @@ -828,6 +858,7 @@ public class CameraDeviceImpl extends CameraDevice + " must have at least 1 surface"); } mRemoteDevice.finalizeOutputConfigurations(streamId, config); + mConfiguredOutputs.put(streamId, config); } } } @@ -950,11 +981,20 @@ public class CameraDeviceImpl extends CameraDevice SubmitInfo requestInfo; CaptureRequest[] requestArray = requestList.toArray(new CaptureRequest[requestList.size()]); + // Convert Surface to streamIdx and surfaceIdx + for (CaptureRequest request : requestArray) { + request.convertSurfaceToStreamId(mConfiguredOutputs); + } + requestInfo = mRemoteDevice.submitRequestList(requestArray, repeating); if (DEBUG) { Log.v(TAG, "last frame number " + requestInfo.getLastFrameNumber()); } + for (CaptureRequest request : requestArray) { + request.recoverStreamIdToSurface(); + } + if (callback != null) { mCaptureCallbackMap.put(requestInfo.getRequestId(), new CaptureCallbackHolder( diff --git a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index 0978ff87..1f4ed13e 100644 --- a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -106,9 +106,11 @@ public class ICameraDeviceUserWrapper { } } - public void endConfigure(int operatingMode) throws CameraAccessException { + public void endConfigure(int operatingMode, CameraMetadataNative sessionParams) + throws CameraAccessException { try { - mRemoteDevice.endConfigure(operatingMode); + mRemoteDevice.endConfigure(operatingMode, (sessionParams == null) ? + new CameraMetadataNative() : sessionParams); } catch (Throwable t) { CameraManager.throwAsPublicException(t); throw new UnsupportedOperationException("Unexpected exception", t); diff --git a/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/android/hardware/camera2/legacy/CameraDeviceUserShim.java index 119cca8d..eccab750 100644 --- a/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -498,7 +498,7 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { } @Override - public void endConfigure(int operatingMode) { + public void endConfigure(int operatingMode, CameraMetadataNative sessionParams) { if (DEBUG) { Log.d(TAG, "endConfigure called."); } diff --git a/android/hardware/camera2/params/OutputConfiguration.java b/android/hardware/camera2/params/OutputConfiguration.java index 7409671f..a85b5f71 100644 --- a/android/hardware/camera2/params/OutputConfiguration.java +++ b/android/hardware/camera2/params/OutputConfiguration.java @@ -486,6 +486,7 @@ public final class OutputConfiguration implements Parcelable { this.mConfiguredSize = other.mConfiguredSize; this.mConfiguredGenerationId = other.mConfiguredGenerationId; this.mIsDeferredConfig = other.mIsDeferredConfig; + this.mIsShared = other.mIsShared; } /** @@ -498,6 +499,7 @@ public final class OutputConfiguration implements Parcelable { int width = source.readInt(); int height = source.readInt(); boolean isDeferred = source.readInt() == 1; + boolean isShared = source.readInt() == 1; ArrayList surfaces = new ArrayList(); source.readTypedList(surfaces, Surface.CREATOR); @@ -508,6 +510,7 @@ public final class OutputConfiguration implements Parcelable { mSurfaces = surfaces; mConfiguredSize = new Size(width, height); mIsDeferredConfig = isDeferred; + mIsShared = isShared; mSurfaces = surfaces; if (mSurfaces.size() > 0) { mSurfaceType = SURFACE_TYPE_UNKNOWN; diff --git a/android/hardware/camera2/params/SessionConfiguration.java b/android/hardware/camera2/params/SessionConfiguration.java new file mode 100644 index 00000000..a79a6c17 --- /dev/null +++ b/android/hardware/camera2/params/SessionConfiguration.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.hardware.camera2.params; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.IntDef; +import android.os.Handler; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.InputConfiguration; +import android.hardware.camera2.params.OutputConfiguration; + +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static com.android.internal.util.Preconditions.*; + +/** + * A helper class that aggregates all supported arguments for capture session initialization. + */ +public final class SessionConfiguration { + /** + * A regular session type containing instances of {@link OutputConfiguration} running + * at regular non high speed FPS ranges and optionally {@link InputConfiguration} for + * reprocessable sessions. + * + * @see CameraDevice#createCaptureSession + * @see CameraDevice#createReprocessableCaptureSession + */ + public static final int SESSION_REGULAR = CameraDevice.SESSION_OPERATION_MODE_NORMAL; + + /** + * A high speed session type that can only contain instances of {@link OutputConfiguration}. + * The outputs can run using high speed FPS ranges. Calls to {@link #setInputConfiguration} + * are not supported. + * + * @see CameraDevice#createConstrainedHighSpeedCaptureSession + */ + public static final int SESSION_HIGH_SPEED = + CameraDevice.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED; + + /** + * First vendor-specific session mode + * @hide + */ + public static final int SESSION_VENDOR_START = + CameraDevice.SESSION_OPERATION_MODE_VENDOR_START; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"SESSION_"}, value = + {SESSION_REGULAR, + SESSION_HIGH_SPEED }) + public @interface SessionMode {}; + + // Camera capture session related parameters. + private List mOutputConfigurations; + private CameraCaptureSession.StateCallback mStateCallback; + private int mSessionType; + private Handler mHandler = null; + private InputConfiguration mInputConfig = null; + private CaptureRequest mSessionParameters = null; + + /** + * Create a new {@link SessionConfiguration}. + * + * @param sessionType The session type. + * @param outputs A list of output configurations for the capture session. + * @param cb A state callback interface implementation. + * @param handler The handler on which the callback will be invoked. If it is + * set to null, the callback will be invoked on the current thread's + * {@link android.os.Looper looper}. + * + * @see #SESSION_REGULAR + * @see #SESSION_HIGH_SPEED + * @see CameraDevice#createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) + * @see CameraDevice#createCaptureSessionByOutputConfigurations + * @see CameraDevice#createReprocessableCaptureSession + * @see CameraDevice#createConstrainedHighSpeedCaptureSession + */ + public SessionConfiguration(@SessionMode int sessionType, + @NonNull List outputs, + @NonNull CameraCaptureSession.StateCallback cb, @Nullable Handler handler) { + mSessionType = sessionType; + mOutputConfigurations = Collections.unmodifiableList(new ArrayList<>(outputs)); + mStateCallback = cb; + mHandler = handler; + } + + /** + * Retrieve the type of the capture session. + * + * @return The capture session type. + */ + public @SessionMode int getSessionType() { + return mSessionType; + } + + /** + * Retrieve the {@link OutputConfiguration} list for the capture session. + * + * @return A list of output configurations for the capture session. + */ + public List getOutputConfigurations() { + return mOutputConfigurations; + } + + /** + * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session. + * + * @return A state callback interface implementation. + */ + public CameraCaptureSession.StateCallback getStateCallback() { + return mStateCallback; + } + + /** + * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session. + * + * @return The handler on which the callback will be invoked. If it is + * set to null, the callback will be invoked on the current thread's + * {@link android.os.Looper looper}. + */ + public Handler getHandler() { + return mHandler; + } + + /** + * Sets the {@link InputConfiguration} for a reprocessable session. Input configuration are not + * supported for {@link #SESSION_HIGH_SPEED}. + * + * @param input Input configuration. + * @throws UnsupportedOperationException In case it is called for {@link #SESSION_HIGH_SPEED} + * type session configuration. + */ + public void setInputConfiguration(@NonNull InputConfiguration input) { + if (mSessionType != SESSION_HIGH_SPEED) { + mInputConfig = input; + } else { + throw new UnsupportedOperationException("Method not supported for high speed session" + + " types"); + } + } + + /** + * Retrieve the {@link InputConfiguration}. + * + * @return The capture session input configuration. + */ + public InputConfiguration getInputConfiguration() { + return mInputConfig; + } + + /** + * Sets the session wide camera parameters (see {@link CaptureRequest}). This argument can + * be set for every supported session type and will be passed to the camera device as part + * of the capture session initialization. Session parameters are a subset of the available + * capture request parameters (see {@link CameraCharacteristics#getAvailableSessionKeys}) + * and their application can introduce internal camera delays. To improve camera performance + * it is suggested to change them sparingly within the lifetime of the capture session and + * to pass their initial values as part of this method. + * + * @param params A capture request that includes the initial values for any available + * session wide capture keys. + */ + public void setSessionParameters(CaptureRequest params) { + mSessionParameters = params; + } + + /** + * Retrieve the session wide camera parameters (see {@link CaptureRequest}). + * + * @return A capture request that includes the initial values for any available + * session wide capture keys. + */ + public CaptureRequest getSessionParameters() { + return mSessionParameters; + } +} diff --git a/android/hardware/display/BrightnessChangeEvent.java b/android/hardware/display/BrightnessChangeEvent.java index fe24e32e..3003607e 100644 --- a/android/hardware/display/BrightnessChangeEvent.java +++ b/android/hardware/display/BrightnessChangeEvent.java @@ -21,6 +21,8 @@ import android.os.Parcelable; /** * Data about a brightness settings change. + * + * {@see DisplayManager.getBrightnessEvents()} * TODO make this SystemAPI * @hide */ @@ -31,7 +33,9 @@ public final class BrightnessChangeEvent implements Parcelable { /** Timestamp of the change {@see System.currentTimeMillis()} */ public long timeStamp; - /** Package name of focused activity when brightness was changed. */ + /** Package name of focused activity when brightness was changed. + * This will be null if the caller of {@see DisplayManager.getBrightnessEvents()} + * does not have access to usage stats {@see UsageStatsManager} */ public String packageName; /** User id of of the user running when brightness was changed. @@ -59,6 +63,20 @@ public final class BrightnessChangeEvent implements Parcelable { public BrightnessChangeEvent() { } + /** @hide */ + public BrightnessChangeEvent(BrightnessChangeEvent other) { + this.brightness = other.brightness; + this.timeStamp = other.timeStamp; + this.packageName = other.packageName; + this.userId = other.userId; + this.luxValues = other.luxValues; + this.luxTimestamps = other.luxTimestamps; + this.batteryLevel = other.batteryLevel; + this.nightMode = other.nightMode; + this.colorTemperature = other.colorTemperature; + this.lastBrightness = other.lastBrightness; + } + private BrightnessChangeEvent(Parcel source) { brightness = source.readInt(); timeStamp = source.readLong(); diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java new file mode 100644 index 00000000..6c3be816 --- /dev/null +++ b/android/hardware/display/BrightnessConfiguration.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.display; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; + +import com.android.internal.util.Preconditions; + +import java.util.Arrays; + +/** @hide */ +public final class BrightnessConfiguration implements Parcelable { + private final float[] mLux; + private final float[] mNits; + + private BrightnessConfiguration(float[] lux, float[] nits) { + mLux = lux; + mNits = nits; + } + + /** + * Gets the base brightness as curve. + * + * The curve is returned as a pair of float arrays, the first representing all of the lux + * points of the brightness curve and the second representing all of the nits values of the + * brightness curve. + * + * @return the control points for the brightness curve. + */ + public Pair getCurve() { + return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length)); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloatArray(mLux); + dest.writeFloatArray(mNits); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("BrightnessConfiguration{["); + final int size = mLux.length; + for (int i = 0; i < size; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")"); + } + sb.append("]}"); + return sb.toString(); + } + + @Override + public int hashCode() { + int result = 1; + result = result * 31 + Arrays.hashCode(mLux); + result = result * 31 + Arrays.hashCode(mNits); + return result; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof BrightnessConfiguration)) { + return false; + } + final BrightnessConfiguration other = (BrightnessConfiguration) o; + return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits); + } + + public static final Creator CREATOR = + new Creator() { + public BrightnessConfiguration createFromParcel(Parcel in) { + Builder builder = new Builder(); + float[] lux = in.createFloatArray(); + float[] nits = in.createFloatArray(); + builder.setCurve(lux, nits); + return builder.build(); + } + + public BrightnessConfiguration[] newArray(int size) { + return new BrightnessConfiguration[size]; + } + }; + + /** + * A builder class for {@link BrightnessConfiguration}s. + */ + public static class Builder { + private float[] mCurveLux; + private float[] mCurveNits; + + /** + * Sets the control points for the brightness curve. + * + * Brightness curves must have strictly increasing ambient brightness values in lux and + * monotonically increasing display brightness values in nits. In addition, the initial + * control point must be 0 lux. + * + * @throws IllegalArgumentException if the initial control point is not at 0 lux. + * @throws IllegalArgumentException if the lux levels are not strictly increasing. + * @throws IllegalArgumentException if the nit levels are not monotonically increasing. + */ + public Builder setCurve(float[] lux, float[] nits) { + Preconditions.checkNotNull(lux); + Preconditions.checkNotNull(nits); + if (lux.length == 0 || nits.length == 0) { + throw new IllegalArgumentException("Lux and nits arrays must not be empty"); + } + if (lux.length != nits.length) { + throw new IllegalArgumentException("Lux and nits arrays must be the same length"); + } + if (lux[0] != 0) { + throw new IllegalArgumentException("Initial control point must be for 0 lux"); + } + Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux"); + Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits"); + checkMonotonic(lux, true/*strictly increasing*/, "lux"); + checkMonotonic(nits, false /*strictly increasing*/, "nits"); + mCurveLux = lux; + mCurveNits = nits; + return this; + } + + /** + * Builds the {@link BrightnessConfiguration}. + * + * A brightness curve must be set before calling this. + */ + public BrightnessConfiguration build() { + if (mCurveLux == null || mCurveNits == null) { + throw new IllegalStateException("A curve must be set!"); + } + return new BrightnessConfiguration(mCurveLux, mCurveNits); + } + + private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) { + if (vals.length <= 1) { + return; + } + float prev = vals[0]; + for (int i = 1; i < vals.length; i++) { + if (prev > vals[i] || prev == vals[i] && strictlyIncreasing) { + String condition = strictlyIncreasing ? "strictly increasing" : "monotonic"; + throw new IllegalArgumentException(name + " values must be " + condition); + } + prev = vals[i]; + } + } + } +} diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java index 89357456..7de667dc 100644 --- a/android/hardware/display/DisplayManager.java +++ b/android/hardware/display/DisplayManager.java @@ -16,8 +16,10 @@ package android.hardware.display; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.app.KeyguardManager; @@ -25,6 +27,7 @@ import android.content.Context; import android.graphics.Point; import android.media.projection.MediaProjection; import android.os.Handler; +import android.os.UserHandle; import android.util.SparseArray; import android.view.Display; import android.view.Surface; @@ -619,8 +622,9 @@ public final class DisplayManager { * Fetch {@link BrightnessChangeEvent}s. * @hide until we make it a system api. */ + @RequiresPermission(Manifest.permission.BRIGHTNESS_SLIDER_USAGE) public List getBrightnessEvents() { - return mGlobal.getBrightnessEvents(); + return mGlobal.getBrightnessEvents(mContext.getOpPackageName()); } /** @@ -630,6 +634,27 @@ public final class DisplayManager { mGlobal.setBrightness(brightness); } + /** + * Sets the global display brightness configuration. + * + * @hide + */ + public void setBrightnessConfiguration(BrightnessConfiguration c) { + setBrightnessConfigurationForUser(c, UserHandle.myUserId()); + } + + /** + * Sets the global display brightness configuration for a specific user. + * + * Note this requires the INTERACT_ACROSS_USERS permission if setting the configuration for a + * user other than the one you're currently running as. + * + * @hide + */ + public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) { + mGlobal.setBrightnessConfigurationForUser(c, userId); + } + /** * Listens for changes in available display devices. */ diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java index d93d0e4e..bf4cc1d8 100644 --- a/android/hardware/display/DisplayManagerGlobal.java +++ b/android/hardware/display/DisplayManagerGlobal.java @@ -462,9 +462,10 @@ public final class DisplayManagerGlobal { /** * Retrieves brightness change events. */ - public List getBrightnessEvents() { + public List getBrightnessEvents(String callingPackage) { try { - ParceledListSlice events = mDm.getBrightnessEvents(); + ParceledListSlice events = + mDm.getBrightnessEvents(callingPackage); if (events == null) { return Collections.emptyList(); } @@ -486,6 +487,19 @@ public final class DisplayManagerGlobal { } } + /** + * Sets the global brightness configuration for a given user. + * + * @hide + */ + public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) { + try { + mDm.setBrightnessConfigurationForUser(c, userId); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, int event) { diff --git a/android/hardware/input/InputManager.java b/android/hardware/input/InputManager.java index c531a899..1de8882e 100644 --- a/android/hardware/input/InputManager.java +++ b/android/hardware/input/InputManager.java @@ -188,7 +188,11 @@ public final class InputManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({SWITCH_STATE_UNKNOWN, SWITCH_STATE_OFF, SWITCH_STATE_ON}) + @IntDef(prefix = { "SWITCH_STATE_" }, value = { + SWITCH_STATE_UNKNOWN, + SWITCH_STATE_OFF, + SWITCH_STATE_ON + }) public @interface SwitchState {} /** diff --git a/android/hardware/location/ContextHubInfo.java b/android/hardware/location/ContextHubInfo.java index e1137aa5..c2b28001 100644 --- a/android/hardware/location/ContextHubInfo.java +++ b/android/hardware/location/ContextHubInfo.java @@ -26,7 +26,7 @@ import java.util.Arrays; * @hide */ @SystemApi -public class ContextHubInfo { +public class ContextHubInfo implements Parcelable { private int mId; private String mName; private String mVendor; @@ -262,7 +262,7 @@ public class ContextHubInfo { @Override public String toString() { String retVal = ""; - retVal += "Id : " + mId; + retVal += "ID/handle : " + mId; retVal += ", Name : " + mName; retVal += "\n\tVendor : " + mVendor; retVal += ", Toolchain : " + mToolchain; @@ -275,8 +275,6 @@ public class ContextHubInfo { retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW"; retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW"; retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes"; - retVal += "\n\tSupported sensors : " + Arrays.toString(mSupportedSensors); - retVal += "\n\tMemory Regions : " + Arrays.toString(mMemoryRegions); return retVal; } diff --git a/android/hardware/location/ContextHubManager.java b/android/hardware/location/ContextHubManager.java index b31c7bcd..4cea0acd 100644 --- a/android/hardware/location/ContextHubManager.java +++ b/android/hardware/location/ContextHubManager.java @@ -15,13 +15,15 @@ */ package android.hardware.location; -import android.annotation.Nullable; +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; @@ -29,6 +31,7 @@ import android.os.ServiceManager.ServiceNotFoundException; import android.util.Log; import java.util.List; +import java.util.concurrent.Executor; /** * A class that exposes the Context hubs on a device to applications. @@ -258,9 +261,9 @@ public final class ContextHubManager { } /** - * Returns the list of context hubs in the system. + * Returns the list of ContextHubInfo objects describing the available Context Hubs. * - * @return the list of context hub informations + * @return the list of ContextHubInfo objects * * @see ContextHubInfo * @@ -268,10 +271,14 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public List getContextHubs() { - throw new UnsupportedOperationException("TODO: Implement this"); + try { + return mService.getContextHubs(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } - /* + /** * Helper function to generate a stub for a non-query transaction callback. * * @param transaction the transaction to unblock when complete @@ -287,7 +294,7 @@ public final class ContextHubManager { public void onQueryResponse(int result, List nanoappList) { Log.e(TAG, "Received a query callback on a non-query request"); transaction.setResponse(new ContextHubTransaction.Response( - ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null)); + ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null)); } @Override @@ -297,7 +304,7 @@ public final class ContextHubManager { }; } - /* + /** * Helper function to generate a stub for a query transaction callback. * * @param transaction the transaction to unblock when complete @@ -319,7 +326,7 @@ public final class ContextHubManager { public void onTransactionComplete(int result) { Log.e(TAG, "Received a non-query callback on a query request"); transaction.setResponse(new ContextHubTransaction.Response>( - ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null)); + ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null)); } }; } @@ -342,7 +349,17 @@ public final class ContextHubManager { @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public ContextHubTransaction loadNanoApp( ContextHubInfo hubInfo, NanoAppBinary appBinary) { - throw new UnsupportedOperationException("TODO: Implement this"); + ContextHubTransaction transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.loadNanoAppOnHub(hubInfo.getId(), callback, appBinary); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; } /** @@ -357,7 +374,17 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public ContextHubTransaction unloadNanoApp(ContextHubInfo hubInfo, long nanoAppId) { - throw new UnsupportedOperationException("TODO: Implement this"); + ContextHubTransaction transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.unloadNanoAppFromHub(hubInfo.getId(), callback, nanoAppId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; } /** @@ -372,7 +399,17 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public ContextHubTransaction enableNanoApp(ContextHubInfo hubInfo, long nanoAppId) { - throw new UnsupportedOperationException("TODO: Implement this"); + ContextHubTransaction transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_ENABLE_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.enableNanoApp(hubInfo.getId(), callback, nanoAppId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; } /** @@ -387,7 +424,17 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public ContextHubTransaction disableNanoApp(ContextHubInfo hubInfo, long nanoAppId) { - throw new UnsupportedOperationException("TODO: Implement this"); + ContextHubTransaction transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_DISABLE_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.disableNanoApp(hubInfo.getId(), callback, nanoAppId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; } /** @@ -401,7 +448,17 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public ContextHubTransaction> queryNanoApps(ContextHubInfo hubInfo) { - throw new UnsupportedOperationException("TODO: Implement this"); + ContextHubTransaction> transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_QUERY_NANOAPPS); + IContextHubTransactionCallback callback = createQueryCallback(transaction); + + try { + mService.queryNanoApps(hubInfo.getId(), callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; } /** @@ -459,46 +516,46 @@ public final class ContextHubManager { * Creates an interface to the ContextHubClient to send down to the service. * * @param callback the callback to invoke at the client process - * @param handler the handler to post callbacks for this client + * @param executor the executor to invoke callbacks for this client * * @return the callback interface */ private IContextHubClientCallback createClientCallback( - ContextHubClientCallback callback, Handler handler) { + ContextHubClientCallback callback, Executor executor) { return new IContextHubClientCallback.Stub() { @Override public void onMessageFromNanoApp(NanoAppMessage message) { - handler.post(() -> callback.onMessageFromNanoApp(message)); + executor.execute(() -> callback.onMessageFromNanoApp(message)); } @Override public void onHubReset() { - handler.post(() -> callback.onHubReset()); + executor.execute(() -> callback.onHubReset()); } @Override public void onNanoAppAborted(long nanoAppId, int abortCode) { - handler.post(() -> callback.onNanoAppAborted(nanoAppId, abortCode)); + executor.execute(() -> callback.onNanoAppAborted(nanoAppId, abortCode)); } @Override public void onNanoAppLoaded(long nanoAppId) { - handler.post(() -> callback.onNanoAppLoaded(nanoAppId)); + executor.execute(() -> callback.onNanoAppLoaded(nanoAppId)); } @Override public void onNanoAppUnloaded(long nanoAppId) { - handler.post(() -> callback.onNanoAppUnloaded(nanoAppId)); + executor.execute(() -> callback.onNanoAppUnloaded(nanoAppId)); } @Override public void onNanoAppEnabled(long nanoAppId) { - handler.post(() -> callback.onNanoAppEnabled(nanoAppId)); + executor.execute(() -> callback.onNanoAppEnabled(nanoAppId)); } @Override public void onNanoAppDisabled(long nanoAppId) { - handler.post(() -> callback.onNanoAppDisabled(nanoAppId)); + executor.execute(() -> callback.onNanoAppDisabled(nanoAppId)); } }; } @@ -510,9 +567,9 @@ public final class ContextHubManager { * registration succeeds, the client can send messages to nanoapps through the returned * {@link ContextHubClient} object, and receive notifications through the provided callback. * - * @param callback the notification callback to register * @param hubInfo the hub to attach this client to - * @param handler the handler to invoke the callback, if null uses the main thread's Looper + * @param callback the notification callback to register + * @param executor the executor to invoke the callback * @return the registered client object * * @throws IllegalArgumentException if hubInfo does not represent a valid hub @@ -522,8 +579,9 @@ public final class ContextHubManager { * @hide * @see ContextHubClientCallback */ - public ContextHubClient createClient( - ContextHubClientCallback callback, ContextHubInfo hubInfo, @Nullable Handler handler) { + @NonNull public ContextHubClient createClient( + @NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback, + @NonNull @CallbackExecutor Executor executor) { if (callback == null) { throw new NullPointerException("Callback cannot be null"); } @@ -531,8 +589,7 @@ public final class ContextHubManager { throw new NullPointerException("Hub info cannot be null"); } - Handler realHandler = (handler == null) ? new Handler(mMainLooper) : handler; - IContextHubClientCallback clientInterface = createClientCallback(callback, realHandler); + IContextHubClientCallback clientInterface = createClientCallback(callback, executor); IContextHubClient client; try { @@ -544,6 +601,25 @@ public final class ContextHubManager { return new ContextHubClient(client, clientInterface, hubInfo); } + /** + * Equivalent to {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)} + * with the executor using the main thread's Looper. + * + * @param hubInfo the hub to attach this client to + * @param callback the notification callback to register + * @return the registered client object + * + * @throws IllegalArgumentException if hubInfo does not represent a valid hub + * @throws IllegalStateException if there were too many registered clients at the service + * @throws NullPointerException if callback or hubInfo is null + * @hide + * @see ContextHubClientCallback + */ + @NonNull public ContextHubClient createClient( + @NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback) { + return createClient(hubInfo, callback, new HandlerExecutor(Handler.getMain())); + } + /** * Unregister a callback for receive messages from the context hub. * diff --git a/android/hardware/location/ContextHubTransaction.java b/android/hardware/location/ContextHubTransaction.java index a8569ef4..a1b743da 100644 --- a/android/hardware/location/ContextHubTransaction.java +++ b/android/hardware/location/ContextHubTransaction.java @@ -15,15 +15,16 @@ */ package android.hardware.location; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.os.Handler; -import android.os.Looper; -import android.util.Log; +import android.os.HandlerExecutor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -33,8 +34,8 @@ import java.util.concurrent.TimeoutException; * This object is generated as a result of an asynchronous request sent to the Context Hub * through the ContextHubManager APIs. The caller can either retrieve the result * synchronously through a blocking call ({@link #waitForResponse(long, TimeUnit)}) or - * asynchronously through a user-defined callback - * ({@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)}). + * asynchronously through a user-defined listener + * ({@link #setOnCompleteListener(Listener, Executor)} )}). * * @param the type of the contents in the transaction response * @@ -47,13 +48,15 @@ public class ContextHubTransaction { * Constants describing the type of a transaction through the Context Hub Service. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "TYPE_" }, value = { TYPE_LOAD_NANOAPP, TYPE_UNLOAD_NANOAPP, TYPE_ENABLE_NANOAPP, TYPE_DISABLE_NANOAPP, - TYPE_QUERY_NANOAPPS}) - public @interface Type {} + TYPE_QUERY_NANOAPPS + }) + public @interface Type { } + public static final int TYPE_LOAD_NANOAPP = 0; public static final int TYPE_UNLOAD_NANOAPP = 1; public static final int TYPE_ENABLE_NANOAPP = 2; @@ -64,45 +67,51 @@ public class ContextHubTransaction { * Constants describing the result of a transaction or request through the Context Hub Service. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ - TRANSACTION_SUCCESS, - TRANSACTION_FAILED_UNKNOWN, - TRANSACTION_FAILED_BAD_PARAMS, - TRANSACTION_FAILED_UNINITIALIZED, - TRANSACTION_FAILED_PENDING, - TRANSACTION_FAILED_AT_HUB, - TRANSACTION_FAILED_TIMEOUT, - TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE}) + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_SUCCESS, + RESULT_FAILED_UNKNOWN, + RESULT_FAILED_BAD_PARAMS, + RESULT_FAILED_UNINITIALIZED, + RESULT_FAILED_PENDING, + RESULT_FAILED_AT_HUB, + RESULT_FAILED_TIMEOUT, + RESULT_FAILED_SERVICE_INTERNAL_FAILURE, + RESULT_FAILED_HAL_UNAVAILABLE + }) public @interface Result {} - public static final int TRANSACTION_SUCCESS = 0; + public static final int RESULT_SUCCESS = 0; /** * Generic failure mode. */ - public static final int TRANSACTION_FAILED_UNKNOWN = 1; + public static final int RESULT_FAILED_UNKNOWN = 1; /** * Failure mode when the request parameters were not valid. */ - public static final int TRANSACTION_FAILED_BAD_PARAMS = 2; + public static final int RESULT_FAILED_BAD_PARAMS = 2; /** * Failure mode when the Context Hub is not initialized. */ - public static final int TRANSACTION_FAILED_UNINITIALIZED = 3; + public static final int RESULT_FAILED_UNINITIALIZED = 3; /** * Failure mode when there are too many transactions pending. */ - public static final int TRANSACTION_FAILED_PENDING = 4; + public static final int RESULT_FAILED_PENDING = 4; /** * Failure mode when the request went through, but failed asynchronously at the hub. */ - public static final int TRANSACTION_FAILED_AT_HUB = 5; + public static final int RESULT_FAILED_AT_HUB = 5; /** * Failure mode when the transaction has timed out. */ - public static final int TRANSACTION_FAILED_TIMEOUT = 6; + public static final int RESULT_FAILED_TIMEOUT = 6; /** * Failure mode when the transaction has failed internally at the service. */ - public static final int TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE = 7; + public static final int RESULT_FAILED_SERVICE_INTERNAL_FAILURE = 7; + /** + * Failure mode when the Context Hub HAL was not available. + */ + public static final int RESULT_FAILED_HAL_UNAVAILABLE = 8; /** * A class describing the response for a ContextHubTransaction. @@ -137,20 +146,20 @@ public class ContextHubTransaction { } /** - * An interface describing the callback to be invoked when a transaction completes. + * An interface describing the listener for a transaction completion. * - * @param the type of the contents in the transaction response + * @param the type of the contents in the transaction response */ @FunctionalInterface - public interface Callback { + public interface Listener { /** - * The callback to invoke when the transaction completes. + * The listener function to invoke when the transaction completes. * * @param transaction the transaction that this callback was attached to. * @param response the response of the transaction. */ void onComplete( - ContextHubTransaction transaction, ContextHubTransaction.Response response); + ContextHubTransaction transaction, ContextHubTransaction.Response response); } /* @@ -165,14 +174,14 @@ public class ContextHubTransaction { private ContextHubTransaction.Response mResponse; /* - * The handler to invoke the aynsc response supplied by onComplete. + * The executor to invoke the onComplete async callback. */ - private Handler mHandler = null; + private Executor mExecutor = null; /* - * The callback to invoke when the transaction completes. + * The listener to be invoked when the transaction completes. */ - private ContextHubTransaction.Callback mCallback = null; + private ContextHubTransaction.Listener mListener = null; /* * Synchronization latch used to block on response. @@ -188,6 +197,30 @@ public class ContextHubTransaction { mTransactionType = type; } + /** + * Converts a transaction type to a human-readable string + * + * @param type the type of a transaction + * @param upperCase {@code true} if upper case the first letter, {@code false} otherwise + * @return a string describing the transaction + */ + public static String typeToString(@Type int type, boolean upperCase) { + switch (type) { + case ContextHubTransaction.TYPE_LOAD_NANOAPP: + return upperCase ? "Load" : "load"; + case ContextHubTransaction.TYPE_UNLOAD_NANOAPP: + return upperCase ? "Unload" : "unload"; + case ContextHubTransaction.TYPE_ENABLE_NANOAPP: + return upperCase ? "Enable" : "enable"; + case ContextHubTransaction.TYPE_DISABLE_NANOAPP: + return upperCase ? "Disable" : "disable"; + case ContextHubTransaction.TYPE_QUERY_NANOAPPS: + return upperCase ? "Query" : "query"; + default: + return upperCase ? "Unknown" : "unknown"; + } + } + /** * @return the type of the transaction */ @@ -226,73 +259,68 @@ public class ContextHubTransaction { } /** - * Sets a callback to be invoked when the transaction completes. + * Sets the listener to be invoked invoked when the transaction completes. * * This function provides an asynchronous approach to retrieve the result of the * transaction. When the transaction response has been provided by the Context Hub, - * the given callback will be posted by the provided handler. + * the given listener will be invoked. * - * If the transaction has already completed at the time of invocation, the callback - * will be immediately posted by the handler. If the transaction has been invalidated, - * the callback will never be invoked. + * If the transaction has already completed at the time of invocation, the listener + * will be immediately invoked. If the transaction has been invalidated, + * the listener will never be invoked. * * A transaction can be invalidated if the process owning the transaction is no longer active * and the reference to this object is lost. * - * This method or {@link #setCallbackOnCompletecan(ContextHubTransaction.Callback)} can only be + * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener)} can only be * invoked once, or an IllegalStateException will be thrown. * - * @param callback the callback to be invoked upon completion - * @param handler the handler to post the callback + * @param listener the listener to be invoked upon completion + * @param executor the executor to invoke the callback * * @throws IllegalStateException if this method is called multiple times * @throws NullPointerException if the callback or handler is null */ - public void setCallbackOnComplete( - @NonNull ContextHubTransaction.Callback callback, @NonNull Handler handler) { + public void setOnCompleteListener( + @NonNull ContextHubTransaction.Listener listener, + @NonNull @CallbackExecutor Executor executor) { synchronized (this) { - if (callback == null) { - throw new NullPointerException("Callback cannot be null"); + if (listener == null) { + throw new NullPointerException("Listener cannot be null"); } - if (handler == null) { - throw new NullPointerException("Handler cannot be null"); + if (executor == null) { + throw new NullPointerException("Executor cannot be null"); } - if (mCallback != null) { + if (mListener != null) { throw new IllegalStateException( - "Cannot set ContextHubTransaction callback multiple times"); + "Cannot set ContextHubTransaction listener multiple times"); } - mCallback = callback; - mHandler = handler; + mListener = listener; + mExecutor = executor; if (mDoneSignal.getCount() == 0) { - boolean callbackPosted = mHandler.post(() -> { - mCallback.onComplete(this, mResponse); - }); - - if (!callbackPosted) { - Log.e(TAG, "Failed to post callback to Handler"); - } + mExecutor.execute(() -> mListener.onComplete(this, mResponse)); } } } /** - * Sets a callback to be invoked when the transaction completes. + * Sets the listener to be invoked invoked when the transaction completes. * - * Equivalent to {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)} - * with the handler being that of the main thread's Looper. + * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)} + * with the executor using the main thread's Looper. * - * This method or {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)} + * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)} * can only be invoked once, or an IllegalStateException will be thrown. * - * @param callback the callback to be invoked upon completion + * @param listener the listener to be invoked upon completion * * @throws IllegalStateException if this method is called multiple times * @throws NullPointerException if the callback is null */ - public void setCallbackOnComplete(@NonNull ContextHubTransaction.Callback callback) { - setCallbackOnComplete(callback, new Handler(Looper.getMainLooper())); + public void setOnCompleteListener(@NonNull ContextHubTransaction.Listener listener) { + setOnCompleteListener(listener, new HandlerExecutor(Handler.getMain())); } /** @@ -307,7 +335,7 @@ public class ContextHubTransaction { * @throws IllegalStateException if this method is invoked multiple times * @throws NullPointerException if the response is null */ - void setResponse(ContextHubTransaction.Response response) { + /* package */ void setResponse(ContextHubTransaction.Response response) { synchronized (this) { if (response == null) { throw new NullPointerException("Response cannot be null"); @@ -321,14 +349,8 @@ public class ContextHubTransaction { mIsResponseSet = true; mDoneSignal.countDown(); - if (mCallback != null) { - boolean callbackPosted = mHandler.post(() -> { - mCallback.onComplete(this, mResponse); - }); - - if (!callbackPosted) { - Log.e(TAG, "Failed to post callback to Handler"); - } + if (mListener != null) { + mExecutor.execute(() -> mListener.onComplete(this, mResponse)); } } } diff --git a/android/hardware/location/NanoAppFilter.java b/android/hardware/location/NanoAppFilter.java index bf35a3d6..5ccf546a 100644 --- a/android/hardware/location/NanoAppFilter.java +++ b/android/hardware/location/NanoAppFilter.java @@ -20,7 +20,6 @@ package android.hardware.location; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; /** * @hide @@ -130,6 +129,14 @@ public class NanoAppFilter { (versionsMatch(mVersionRestrictionMask, mAppVersion, info.getAppVersion())); } + @Override + public String toString() { + return "nanoAppId: 0x" + Long.toHexString(mAppId) + + ", nanoAppVersion: 0x" + Integer.toHexString(mAppVersion) + + ", versionMask: " + mVersionRestrictionMask + + ", vendorMask: " + mAppIdVendorMask; + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public NanoAppFilter createFromParcel(Parcel in) { diff --git a/android/hardware/location/NanoAppInstanceInfo.java b/android/hardware/location/NanoAppInstanceInfo.java index 26238304..f73fd87b 100644 --- a/android/hardware/location/NanoAppInstanceInfo.java +++ b/android/hardware/location/NanoAppInstanceInfo.java @@ -16,9 +16,7 @@ package android.hardware.location; - import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -26,50 +24,49 @@ import android.os.Parcelable; import libcore.util.EmptyArray; /** + * Describes an instance of a nanoapp, used by the internal state manged by ContextHubService. + * + * TODO(b/69270990) Remove this class once the old API is deprecated. + * * @hide */ @SystemApi public class NanoAppInstanceInfo { - private String mPublisher; - private String mName; + private String mPublisher = "Unknown"; + private String mName = "Unknown"; + private int mHandle; private long mAppId; private int mAppVersion; + private int mContexthubId; - private int mNeededReadMemBytes; - private int mNeededWriteMemBytes; - private int mNeededExecMemBytes; + private int mNeededReadMemBytes = 0; + private int mNeededWriteMemBytes = 0; + private int mNeededExecMemBytes = 0; - private int[] mNeededSensors; - private int[] mOutputEvents; - - private int mContexthubId; - private int mHandle; + private int[] mNeededSensors = EmptyArray.INT; + private int[] mOutputEvents = EmptyArray.INT; public NanoAppInstanceInfo() { - mNeededSensors = EmptyArray.INT; - mOutputEvents = EmptyArray.INT; } /** - * get the publisher of this app - * - * @return String - name of the publisher + * @hide */ - public String getPublisher() { - return mPublisher; + public NanoAppInstanceInfo(int handle, long appId, int appVersion, int contextHubId) { + mHandle = handle; + mAppId = appId; + mAppVersion = appVersion; + mContexthubId = contextHubId; } - /** - * set the publisher name for the app - * - * @param publisher - name of the publisher + * get the publisher of this app * - * @hide + * @return String - name of the publisher */ - public void setPublisher(String publisher) { - mPublisher = publisher; + public String getPublisher() { + return mPublisher; } /** @@ -81,17 +78,6 @@ public class NanoAppInstanceInfo { return mName; } - /** - * set the name of the app - * - * @param name - name of the app - * - * @hide - */ - public void setName(String name) { - mName = name; - } - /** * Get the application identifier * @@ -101,17 +87,6 @@ public class NanoAppInstanceInfo { return mAppId; } - /** - * Set the application identifier - * - * @param appId - application identifier - * - * @hide - */ - public void setAppId(long appId) { - mAppId = appId; - } - /** * Get the application version * @@ -126,17 +101,6 @@ public class NanoAppInstanceInfo { return mAppVersion; } - /** - * Set the application version - * - * @param appVersion - version of the app - * - * @hide - */ - public void setAppVersion(int appVersion) { - mAppVersion = appVersion; - } - /** * Get the read memory needed by the app * @@ -146,17 +110,6 @@ public class NanoAppInstanceInfo { return mNeededReadMemBytes; } - /** - * Set the read memory needed by the app - * - * @param neededReadMemBytes - readable Memory needed in bytes - * - * @hide - */ - public void setNeededReadMemBytes(int neededReadMemBytes) { - mNeededReadMemBytes = neededReadMemBytes; - } - /** * get writable memory needed by the app * @@ -166,18 +119,6 @@ public class NanoAppInstanceInfo { return mNeededWriteMemBytes; } - /** - * set writable memory needed by the app - * - * @param neededWriteMemBytes - writable memory needed by the - * app - * - * @hide - */ - public void setNeededWriteMemBytes(int neededWriteMemBytes) { - mNeededWriteMemBytes = neededWriteMemBytes; - } - /** * get executable memory needed by the app * @@ -187,18 +128,6 @@ public class NanoAppInstanceInfo { return mNeededExecMemBytes; } - /** - * set executable memory needed by the app - * - * @param neededExecMemBytes - executable memory needed by the - * app - * - * @hide - */ - public void setNeededExecMemBytes(int neededExecMemBytes) { - mNeededExecMemBytes = neededExecMemBytes; - } - /** * Get the sensors needed by this app * @@ -209,17 +138,6 @@ public class NanoAppInstanceInfo { return mNeededSensors; } - /** - * set the sensors needed by this app - * - * @param neededSensors - all the sensors needed by this app - * - * @hide - */ - public void setNeededSensors(@Nullable int[] neededSensors) { - mNeededSensors = neededSensors != null ? neededSensors : EmptyArray.INT; - } - /** * get the events generated by this app * @@ -230,18 +148,6 @@ public class NanoAppInstanceInfo { return mOutputEvents; } - /** - * set the output events that can be generated by this app - * - * @param outputEvents - the events that may be generated by - * this app - * - * @hide - */ - public void setOutputEvents(@Nullable int[] outputEvents) { - mOutputEvents = outputEvents != null ? outputEvents : EmptyArray.INT; - } - /** * get the context hub identifier * @@ -251,17 +157,6 @@ public class NanoAppInstanceInfo { return mContexthubId; } - /** - * set the context hub identifier - * - * @param contexthubId - system wide unique identifier - * - * @hide - */ - public void setContexthubId(int contexthubId) { - mContexthubId = contexthubId; - } - /** * get a handle to the nano app instance * @@ -271,18 +166,6 @@ public class NanoAppInstanceInfo { return mHandle; } - /** - * set the handle for an app instance - * - * @param handle - handle to this instance - * - * @hide - */ - public void setHandle(int handle) { - mHandle = handle; - } - - private NanoAppInstanceInfo(Parcel in) { mPublisher = in.readString(); mName = in.readString(); @@ -342,9 +225,7 @@ public class NanoAppInstanceInfo { public String toString() { String retVal = "handle : " + mHandle; retVal += ", Id : 0x" + Long.toHexString(mAppId); - retVal += ", Version : " + mAppVersion; - retVal += ", Name : " + mName; - retVal += ", Publisher : " + mPublisher; + retVal += ", Version : 0x" + Integer.toHexString(mAppVersion); return retVal; } diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java index 4f4361f6..4d54e31b 100644 --- a/android/hardware/radio/RadioManager.java +++ b/android/hardware/radio/RadioManager.java @@ -161,7 +161,8 @@ public class RadioManager { private final Set mSupportedIdentifierTypes; @NonNull private final Map mVendorInfo; - ModuleProperties(int id, String serviceName, int classId, String implementor, + /** @hide */ + public ModuleProperties(int id, String serviceName, int classId, String implementor, String product, String version, String serial, int numTuners, int numAudioSources, boolean isCaptureSupported, BandDescriptor[] bands, boolean isBgScanSupported, @ProgramSelector.ProgramType int[] supportedProgramTypes, diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java index 223ed73b..02b1c658 100644 --- a/android/inputmethodservice/InputMethodService.java +++ b/android/inputmethodservice/InputMethodService.java @@ -392,7 +392,7 @@ public class InputMethodService extends AbstractInputMethodService { mWindow.setToken(token); } } - + /** * {@inheritDoc} * @@ -1064,7 +1064,89 @@ public class InputMethodService extends AbstractInputMethodService { } return mInputConnection; } - + + /** + * Force switch to a new input method component. This can only be called + * from an application or a service which has a token of the currently active input method. + * @param id The unique identifier for the new input method to be switched to. + */ + public void setInputMethod(String id) { + mImm.setInputMethodInternal(mToken, id); + } + + /** + * Force switch to a new input method and subtype. This can only be called + * from an application or a service which has a token of the currently active input method. + * @param id The unique identifier for the new input method to be switched to. + * @param subtype The new subtype of the new input method to be switched to. + */ + public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) { + mImm.setInputMethodAndSubtypeInternal(mToken, id, subtype); + } + + /** + * Close/hide the input method's soft input area, so the user no longer + * sees it or can interact with it. This can only be called + * from the currently active input method, as validated by the given token. + * + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY}, + * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set. + */ + public void hideSoftInputFromInputMethod(int flags) { + mImm.hideSoftInputFromInputMethodInternal(mToken, flags); + } + + /** + * Show the input method's soft input area, so the user + * sees the input method window and can interact with it. + * This can only be called from the currently active input method, + * as validated by the given token. + * + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT} or + * {@link InputMethodManager#SHOW_FORCED} bit set. + */ + public void showSoftInputFromInputMethod(int flags) { + mImm.showSoftInputFromInputMethodInternal(mToken, flags); + } + + /** + * Force switch to the last used input method and subtype. If the last input method didn't have + * any subtypes, the framework will simply switch to the last input method with no subtype + * specified. + * @return true if the current input method and subtype was successfully switched to the last + * used input method and subtype. + */ + public boolean switchToLastInputMethod() { + return mImm.switchToLastInputMethodInternal(mToken); + } + + /** + * Force switch to the next input method and subtype. If there is no IME enabled except + * current IME and subtype, do nothing. + * @param onlyCurrentIme if true, the framework will find the next subtype which + * belongs to the current IME + * @return true if the current input method and subtype was successfully switched to the next + * input method and subtype. + */ + public boolean switchToNextInputMethod(boolean onlyCurrentIme) { + return mImm.switchToNextInputMethodInternal(mToken, onlyCurrentIme); + } + + /** + * Returns true if the current IME needs to offer the users ways to switch to a next input + * method (e.g. a globe key.). + * When an IME sets supportsSwitchingToNextInputMethod and this method returns true, + * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly. + *

      Note that the system determines the most appropriate next input method + * and subtype in order to provide the consistent user experience in switching + * between IMEs and subtypes. + */ + public boolean shouldOfferSwitchingToNextInputMethod() { + return mImm.shouldOfferSwitchingToNextInputMethodInternal(mToken); + } + public boolean getCurrentInputStarted() { return mInputStarted; } diff --git a/android/inputmethodservice/KeyboardView.java b/android/inputmethodservice/KeyboardView.java index 7836cd09..13b9206b 100644 --- a/android/inputmethodservice/KeyboardView.java +++ b/android/inputmethodservice/KeyboardView.java @@ -21,16 +21,18 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Paint.Align; import android.graphics.PorterDuff; import android.graphics.Rect; -import android.graphics.Region.Op; import android.graphics.Typeface; +import android.graphics.Paint.Align; +import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.inputmethodservice.Keyboard.Key; import android.media.AudioManager; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; @@ -984,9 +986,6 @@ public class KeyboardView extends View implements View.OnClickListener { private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) { if (mAccessibilityManager.isEnabled()) { - if (!mAccessibilityManager.isObservedEventType(eventType)) { - return; - } AccessibilityEvent event = AccessibilityEvent.obtain(eventType); onInitializeAccessibilityEvent(event); final String text; diff --git a/android/location/GnssMeasurementsEvent.java b/android/location/GnssMeasurementsEvent.java index d66fd9c4..072a7fef 100644 --- a/android/location/GnssMeasurementsEvent.java +++ b/android/location/GnssMeasurementsEvent.java @@ -49,7 +49,7 @@ public final class GnssMeasurementsEvent implements Parcelable { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({STATUS_NOT_SUPPORTED, STATUS_READY, STATUS_LOCATION_DISABLED}) + @IntDef({STATUS_NOT_SUPPORTED, STATUS_READY, STATUS_LOCATION_DISABLED, STATUS_NOT_ALLOWED}) public @interface GnssMeasurementsStatus {} /** @@ -71,6 +71,12 @@ public final class GnssMeasurementsEvent implements Parcelable { */ public static final int STATUS_LOCATION_DISABLED = 2; + /** + * The client is not allowed to register for GNSS Measurements in general or in the + * requested mode. + */ + public static final int STATUS_NOT_ALLOWED = 3; + /** * Reports the latest collected GNSS Measurements. */ diff --git a/android/location/LocalListenerHelper.java b/android/location/LocalListenerHelper.java index d7d2c513..592d01d2 100644 --- a/android/location/LocalListenerHelper.java +++ b/android/location/LocalListenerHelper.java @@ -16,14 +16,14 @@ package android.location; -import com.android.internal.util.Preconditions; - import android.annotation.NonNull; import android.content.Context; import android.os.Handler; import android.os.RemoteException; import android.util.Log; +import com.android.internal.util.Preconditions; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -46,6 +46,11 @@ abstract class LocalListenerHelper { mTag = name; } + /** + * Adds a {@param listener} to the list of listeners on which callbacks will be executed. The + * execution will happen on the {@param handler} thread or alternatively in the callback thread + * if a {@code null} handler value is passed. + */ public boolean add(@NonNull TListener listener, Handler handler) { Preconditions.checkNotNull(listener); synchronized (mListeners) { diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java index 968f596e..4802b235 100644 --- a/android/location/LocationManager.java +++ b/android/location/LocationManager.java @@ -19,6 +19,7 @@ package android.location; import com.android.internal.location.ProviderProperties; import android.Manifest; +import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; @@ -183,6 +184,17 @@ public class LocationManager { */ public static final String MODE_CHANGED_ACTION = "android.location.MODE_CHANGED"; + /** + * Broadcast intent action when {@link android.provider.Settings.Secure#LOCATION_MODE} is + * about to be changed through Settings app or Quick Settings. + * For use with the {@link android.provider.Settings.Secure#LOCATION_MODE} API. + * If you're interacting with {@link #isProviderEnabled(String)}, use + * {@link #PROVIDERS_CHANGED_ACTION} instead. + * + * @hide + */ + public static final String MODE_CHANGING_ACTION = "com.android.settings.location.MODE_CHANGING"; + /** * Broadcast intent action indicating that the GPS has either started or * stopped receiving GPS fixes. An intent extra provides this state as a @@ -214,6 +226,12 @@ public class LocationManager { public static final String HIGH_POWER_REQUEST_CHANGE_ACTION = "android.location.HIGH_POWER_REQUEST_CHANGE"; + /** + * The value returned by {@link LocationManager#getGnssHardwareModelName()} when the hardware + * does not support providing the actual value. + */ + public static final String GNSS_HARDWARE_MODEL_NAME_UNKNOWN = "Model Name Unknown"; + // Map from LocationListeners to their associated ListenerTransport objects private HashMap mListeners = new HashMap(); @@ -1958,11 +1976,10 @@ public class LocationManager { } /** - * Returns the system information of the GPS hardware. - * May return 0 if GPS hardware is earlier than 2016. - * @hide + * Returns the model year of the GNSS hardware and software build. + * + * May return 0 if the model year is less than 2016. */ - @TestApi public int getGnssYearOfHardware() { try { return mService.getGnssYearOfHardware(); @@ -1971,6 +1988,22 @@ public class LocationManager { } } + /** + * Returns the Model Name (including Vendor and Hardware/Software Version) of the GNSS hardware + * driver. + * + * Will return {@link LocationManager#GNSS_HARDWARE_MODEL_NAME_UNKNOWN} when the GNSS hardware + * abstraction layer does not support providing this value. + */ + @NonNull + public String getGnssHardwareModelName() { + try { + return mService.getGnssHardwareModelName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Returns the batch size (in number of Location objects) that are supported by the batching * interface. diff --git a/android/location/LocationRequest.java b/android/location/LocationRequest.java index 65e7cedf..6abba954 100644 --- a/android/location/LocationRequest.java +++ b/android/location/LocationRequest.java @@ -143,7 +143,7 @@ public final class LocationRequest implements Parcelable { private int mQuality = POWER_LOW; private long mInterval = 60 * 60 * 1000; // 60 minutes - private long mFastestInterval = (long)(mInterval / FASTEST_INTERVAL_FACTOR); // 10 minutes + private long mFastestInterval = (long) (mInterval / FASTEST_INTERVAL_FACTOR); // 10 minutes private boolean mExplicitFastestInterval = false; private long mExpireAt = Long.MAX_VALUE; // no expiry private int mNumUpdates = Integer.MAX_VALUE; // no expiry @@ -151,7 +151,11 @@ public final class LocationRequest implements Parcelable { private WorkSource mWorkSource = null; private boolean mHideFromAppOps = false; // True if this request shouldn't be counted by AppOps - private String mProvider = LocationManager.FUSED_PROVIDER; // for deprecated APIs that explicitly request a provider + private String mProvider = LocationManager.FUSED_PROVIDER; + // for deprecated APIs that explicitly request a provider + + /** If true, GNSS chipset will make strong tradeoffs to substantially restrict power use */ + private boolean mLowPowerMode = false; /** * Create a location request with default parameters. @@ -184,11 +188,11 @@ public final class LocationRequest implements Parcelable { } LocationRequest request = new LocationRequest() - .setProvider(provider) - .setQuality(quality) - .setInterval(minTime) - .setFastestInterval(minTime) - .setSmallestDisplacement(minDistance); + .setProvider(provider) + .setQuality(quality) + .setInterval(minTime) + .setFastestInterval(minTime) + .setSmallestDisplacement(minDistance); if (singleShot) request.setNumUpdates(1); return request; } @@ -220,16 +224,17 @@ public final class LocationRequest implements Parcelable { } LocationRequest request = new LocationRequest() - .setQuality(quality) - .setInterval(minTime) - .setFastestInterval(minTime) - .setSmallestDisplacement(minDistance); + .setQuality(quality) + .setInterval(minTime) + .setFastestInterval(minTime) + .setSmallestDisplacement(minDistance); if (singleShot) request.setNumUpdates(1); return request; } /** @hide */ - public LocationRequest() { } + public LocationRequest() { + } /** @hide */ public LocationRequest(LocationRequest src) { @@ -243,6 +248,7 @@ public final class LocationRequest implements Parcelable { mProvider = src.mProvider; mWorkSource = src.mWorkSource; mHideFromAppOps = src.mHideFromAppOps; + mLowPowerMode = src.mLowPowerMode; } /** @@ -263,8 +269,8 @@ public final class LocationRequest implements Parcelable { * on a location request. * * @param quality an accuracy or power constant - * @throws InvalidArgumentException if the quality constant is not valid * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the quality constant is not valid */ public LocationRequest setQuality(int quality) { checkQuality(quality); @@ -306,14 +312,14 @@ public final class LocationRequest implements Parcelable { * on a location request. * * @param millis desired interval in millisecond, inexact - * @throws InvalidArgumentException if the interval is less than zero * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the interval is less than zero */ public LocationRequest setInterval(long millis) { checkInterval(millis); mInterval = millis; if (!mExplicitFastestInterval) { - mFastestInterval = (long)(mInterval / FASTEST_INTERVAL_FACTOR); + mFastestInterval = (long) (mInterval / FASTEST_INTERVAL_FACTOR); } return this; } @@ -327,6 +333,34 @@ public final class LocationRequest implements Parcelable { return mInterval; } + + /** + * Requests the GNSS chipset to run in a low power mode and make strong tradeoffs to + * substantially restrict power. + * + *

      In this mode, the GNSS chipset will not, on average, run power hungry operations like RF & + * signal searches for more than one second per interval {@link #mInterval} + * + * @param enabled Enable or disable low power mode + * @return the same object, so that setters can be chained + * @hide + */ + @SystemApi + public LocationRequest setLowPowerMode(boolean enabled) { + mLowPowerMode = enabled; + return this; + } + + /** + * Returns true if low power mode is enabled. + * + * @hide + */ + @SystemApi + public boolean isLowPowerMode() { + return mLowPowerMode; + } + /** * Explicitly set the fastest interval for location updates, in * milliseconds. @@ -353,8 +387,8 @@ public final class LocationRequest implements Parcelable { * then your effective fastest interval is {@link #setInterval}. * * @param millis fastest interval for updates in milliseconds, exact - * @throws InvalidArgumentException if the interval is less than zero * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if the interval is less than zero */ public LocationRequest setFastestInterval(long millis) { checkInterval(millis); @@ -397,9 +431,9 @@ public final class LocationRequest implements Parcelable { // Check for > Long.MAX_VALUE overflow (elapsedRealtime > 0): if (millis > Long.MAX_VALUE - elapsedRealtime) { - mExpireAt = Long.MAX_VALUE; + mExpireAt = Long.MAX_VALUE; } else { - mExpireAt = millis + elapsedRealtime; + mExpireAt = millis + elapsedRealtime; } if (mExpireAt < 0) mExpireAt = 0; @@ -448,11 +482,14 @@ public final class LocationRequest implements Parcelable { * to the location manager. * * @param numUpdates the number of location updates requested - * @throws InvalidArgumentException if numUpdates is 0 or less * @return the same object, so that setters can be chained + * @throws InvalidArgumentException if numUpdates is 0 or less */ public LocationRequest setNumUpdates(int numUpdates) { - if (numUpdates <= 0) throw new IllegalArgumentException("invalid numUpdates: " + numUpdates); + if (numUpdates <= 0) { + throw new IllegalArgumentException( + "invalid numUpdates: " + numUpdates); + } mNumUpdates = numUpdates; return this; } @@ -462,6 +499,7 @@ public final class LocationRequest implements Parcelable { * *

      By default this is {@link Integer#MAX_VALUE}, which indicates that * locations are updated until the request is explicitly removed. + * * @return number of updates */ public int getNumUpdates() { @@ -539,8 +577,8 @@ public final class LocationRequest implements Parcelable { * doesn't have the {@link android.Manifest.permission#UPDATE_APP_OPS_STATS} permission. * * @param hideFromAppOps If true AppOps won't keep track of this location request. - * @see android.app.AppOpsManager * @hide + * @see android.app.AppOpsManager */ @SystemApi public void setHideFromAppOps(boolean hideFromAppOps) { @@ -587,27 +625,29 @@ public final class LocationRequest implements Parcelable { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public LocationRequest createFromParcel(Parcel in) { - LocationRequest request = new LocationRequest(); - request.setQuality(in.readInt()); - request.setFastestInterval(in.readLong()); - request.setInterval(in.readLong()); - request.setExpireAt(in.readLong()); - request.setNumUpdates(in.readInt()); - request.setSmallestDisplacement(in.readFloat()); - request.setHideFromAppOps(in.readInt() != 0); - String provider = in.readString(); - if (provider != null) request.setProvider(provider); - WorkSource workSource = in.readParcelable(null); - if (workSource != null) request.setWorkSource(workSource); - return request; - } - @Override - public LocationRequest[] newArray(int size) { - return new LocationRequest[size]; - } - }; + @Override + public LocationRequest createFromParcel(Parcel in) { + LocationRequest request = new LocationRequest(); + request.setQuality(in.readInt()); + request.setFastestInterval(in.readLong()); + request.setInterval(in.readLong()); + request.setExpireAt(in.readLong()); + request.setNumUpdates(in.readInt()); + request.setSmallestDisplacement(in.readFloat()); + request.setHideFromAppOps(in.readInt() != 0); + request.setLowPowerMode(in.readInt() != 0); + String provider = in.readString(); + if (provider != null) request.setProvider(provider); + WorkSource workSource = in.readParcelable(null); + if (workSource != null) request.setWorkSource(workSource); + return request; + } + + @Override + public LocationRequest[] newArray(int size) { + return new LocationRequest[size]; + } + }; @Override public int describeContents() { @@ -623,6 +663,7 @@ public final class LocationRequest implements Parcelable { parcel.writeInt(mNumUpdates); parcel.writeFloat(mSmallestDisplacement); parcel.writeInt(mHideFromAppOps ? 1 : 0); + parcel.writeInt(mLowPowerMode ? 1 : 0); parcel.writeString(mProvider); parcel.writeParcelable(mWorkSource, 0); } @@ -663,9 +704,10 @@ public final class LocationRequest implements Parcelable { s.append(" expireIn="); TimeUtils.formatDuration(expireIn, s); } - if (mNumUpdates != Integer.MAX_VALUE){ + if (mNumUpdates != Integer.MAX_VALUE) { s.append(" num=").append(mNumUpdates); } + s.append(" lowPowerMode=").append(mLowPowerMode); s.append(']'); return s.toString(); } diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java index 7afe267f..e0289f0b 100644 --- a/android/media/AudioAttributes.java +++ b/android/media/AudioAttributes.java @@ -741,7 +741,7 @@ public final class AudioAttributes implements Parcelable { /** * @hide * Same as {@link #setCapturePreset(int)} but authorizes the use of HOTWORD, - * REMOTE_SUBMIX and RADIO_TUNER. + * REMOTE_SUBMIX, RADIO_TUNER, VOICE_DOWNLINK, VOICE_UPLINK and VOICE_CALL. * @param preset * @return the same Builder instance. */ @@ -749,7 +749,10 @@ public final class AudioAttributes implements Parcelable { public Builder setInternalCapturePreset(int preset) { if ((preset == MediaRecorder.AudioSource.HOTWORD) || (preset == MediaRecorder.AudioSource.REMOTE_SUBMIX) - || (preset == MediaRecorder.AudioSource.RADIO_TUNER)) { + || (preset == MediaRecorder.AudioSource.RADIO_TUNER) + || (preset == MediaRecorder.AudioSource.VOICE_DOWNLINK) + || (preset == MediaRecorder.AudioSource.VOICE_UPLINK) + || (preset == MediaRecorder.AudioSource.VOICE_CALL)) { mSource = preset; } else { setCapturePreset(preset); diff --git a/android/media/AudioDeviceInfo.java b/android/media/AudioDeviceInfo.java index 1b89c966..1a97b6ba 100644 --- a/android/media/AudioDeviceInfo.java +++ b/android/media/AudioDeviceInfo.java @@ -16,9 +16,12 @@ package android.media; +import android.annotation.IntDef; import android.annotation.NonNull; import android.util.SparseIntArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.TreeSet; /** @@ -120,12 +123,71 @@ public final class AudioDeviceInfo { */ public static final int TYPE_USB_HEADSET = 22; + /** @hide */ + @IntDef(flag = false, prefix = "TYPE", value = { + TYPE_BUILTIN_EARPIECE, + TYPE_BUILTIN_SPEAKER, + TYPE_WIRED_HEADSET, + TYPE_WIRED_HEADPHONES, + TYPE_BLUETOOTH_SCO, + TYPE_BLUETOOTH_A2DP, + TYPE_HDMI, + TYPE_DOCK, + TYPE_USB_ACCESSORY, + TYPE_USB_DEVICE, + TYPE_USB_HEADSET, + TYPE_TELEPHONY, + TYPE_LINE_ANALOG, + TYPE_HDMI_ARC, + TYPE_LINE_DIGITAL, + TYPE_FM, + TYPE_AUX_LINE, + TYPE_IP } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface AudioDeviceTypeOut {} + + /** @hide */ + /*package*/ static boolean isValidAudioDeviceTypeOut(int type) { + switch (type) { + case TYPE_BUILTIN_EARPIECE: + case TYPE_BUILTIN_SPEAKER: + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + case TYPE_BLUETOOTH_SCO: + case TYPE_BLUETOOTH_A2DP: + case TYPE_HDMI: + case TYPE_DOCK: + case TYPE_USB_ACCESSORY: + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_TELEPHONY: + case TYPE_LINE_ANALOG: + case TYPE_HDMI_ARC: + case TYPE_LINE_DIGITAL: + case TYPE_FM: + case TYPE_AUX_LINE: + case TYPE_IP: + return true; + default: + return false; + } + } + private final AudioDevicePort mPort; AudioDeviceInfo(AudioDevicePort port) { mPort = port; } + /** + * @hide + * @return The underlying {@link AudioDevicePort} instance. + */ + public AudioDevicePort getPort() { + return mPort; + } + /** * @return The internal device ID. */ diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java index 58976ca0..913b5e84 100644 --- a/android/media/AudioManager.java +++ b/android/media/AudioManager.java @@ -16,6 +16,7 @@ package android.media; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -52,6 +53,8 @@ import android.util.Log; import android.util.Slog; import android.view.KeyEvent; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -911,13 +914,28 @@ public class AudioManager { /** * Returns the minimum volume index for a particular stream. - * - * @param streamType The stream type whose minimum volume index is returned. + * @param streamType The stream type whose minimum volume index is returned. Must be one of + * {@link #STREAM_VOICE_CALL}, {@link #STREAM_SYSTEM}, + * {@link #STREAM_RING}, {@link #STREAM_MUSIC}, {@link #STREAM_ALARM}, + * {@link #STREAM_NOTIFICATION}, {@link #STREAM_DTMF} or {@link #STREAM_ACCESSIBILITY}. * @return The minimum valid volume index for the stream. * @see #getStreamVolume(int) - * @hide */ public int getStreamMinVolume(int streamType) { + if (!isPublicStreamType(streamType)) { + throw new IllegalArgumentException("Invalid stream type " + streamType); + } + return getStreamMinVolumeInt(streamType); + } + + /** + * @hide + * Same as {@link #getStreamMinVolume(int)} but without the check on the public stream type. + * @param streamType The stream type whose minimum volume index is returned. + * @return The minimum valid volume index for the stream. + * @see #getStreamVolume(int) + */ + public int getStreamMinVolumeInt(int streamType) { final IAudioService service = getService(); try { return service.getStreamMinVolume(streamType); @@ -943,6 +961,72 @@ public class AudioManager { } } + // keep in sync with frameworks/av/services/audiopolicy/common/include/Volume.h + private static final float VOLUME_MIN_DB = -758.0f; + + /** @hide */ + @IntDef(flag = false, prefix = "STREAM", value = { + STREAM_VOICE_CALL, + STREAM_SYSTEM, + STREAM_RING, + STREAM_MUSIC, + STREAM_ALARM, + STREAM_NOTIFICATION, + STREAM_DTMF, + STREAM_ACCESSIBILITY } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface PublicStreamTypes {} + + /** + * Returns the volume in dB (decibel) for the given stream type at the given volume index, on + * the given type of audio output device. + * @param streamType stream type for which the volume is queried. + * @param index the volume index for which the volume is queried. The index value must be + * between the minimum and maximum index values for the given stream type (see + * {@link #getStreamMinVolume(int)} and {@link #getStreamMaxVolume(int)}). + * @param deviceType the type of audio output device for which volume is queried. + * @return a volume expressed in dB. + * A negative value indicates the audio signal is attenuated. A typical maximum value + * at the maximum volume index is 0 dB (no attenuation nor amplification). Muting is + * reflected by a value of {@link Float#NEGATIVE_INFINITY}. + */ + public float getStreamVolumeDb(@PublicStreamTypes int streamType, int index, + @AudioDeviceInfo.AudioDeviceTypeOut int deviceType) { + if (!isPublicStreamType(streamType)) { + throw new IllegalArgumentException("Invalid stream type " + streamType); + } + if (index > getStreamMaxVolume(streamType) || index < getStreamMinVolume(streamType)) { + throw new IllegalArgumentException("Invalid stream volume index " + index); + } + if (!AudioDeviceInfo.isValidAudioDeviceTypeOut(deviceType)) { + throw new IllegalArgumentException("Invalid audio output device type " + deviceType); + } + final float gain = AudioSystem.getStreamVolumeDB(streamType, index, + AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType)); + if (gain <= VOLUME_MIN_DB) { + return Float.NEGATIVE_INFINITY; + } else { + return gain; + } + } + + private static boolean isPublicStreamType(int streamType) { + switch (streamType) { + case STREAM_VOICE_CALL: + case STREAM_SYSTEM: + case STREAM_RING: + case STREAM_MUSIC: + case STREAM_ALARM: + case STREAM_NOTIFICATION: + case STREAM_DTMF: + case STREAM_ACCESSIBILITY: + return true; + default: + return false; + } + } + /** * Get last audible volume before stream was muted. * @@ -1550,6 +1634,21 @@ public class AudioManager { return AudioSystem.isMicrophoneMuted(); } + /** + * Broadcast Action: microphone muting state changed. + * + * You cannot receive this through components declared + * in manifests, only by explicitly registering for it with + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + * + *

      The intent has no extra values, use {@link #isMicrophoneMute} to check whether the + * microphone is muted. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MICROPHONE_MUTE_CHANGED = + "android.media.action.MICROPHONE_MUTE_CHANGED"; + /** * Sets the audio mode. *

      diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java index 0906ba50..27784e96 100644 --- a/android/media/AudioRecord.java +++ b/android/media/AudioRecord.java @@ -1515,56 +1515,6 @@ public class AudioRecord implements AudioRouting removeOnRoutingChangedListener((AudioRouting.OnRoutingChangedListener) listener); } - /** - * Helper class to handle the forwarding of native events to the appropriate listener - * (potentially) handled in a different thread - */ - private class NativeRoutingEventHandlerDelegate { - private final Handler mHandler; - - NativeRoutingEventHandlerDelegate(final AudioRecord record, - final AudioRouting.OnRoutingChangedListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - // no given handler, use the looper the AudioRecord was created in - looper = mInitializationLooper; - } - - // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - if (record == null) { - return; - } - switch(msg.what) { - case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE: - if (listener != null) { - listener.onRoutingChanged(record); - } - break; - default: - loge("Unknown native event type: " + msg.what); - break; - } - } - }; - } else { - mHandler = null; - } - } - - Handler getHandler() { - return mHandler; - } - } - /** * Sends device list change notification to all listeners. */ @@ -1572,10 +1522,7 @@ public class AudioRecord implements AudioRouting AudioManager.resetAudioPortGeneration(); synchronized (mRoutingChangeListeners) { for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) { - Handler handler = delegate.getHandler(); - if (handler != null) { - handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE); - } + delegate.notifyClient(); } } } diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java index 50145f8a..e535fdf5 100644 --- a/android/media/AudioTrack.java +++ b/android/media/AudioTrack.java @@ -2856,10 +2856,7 @@ public class AudioTrack extends PlayerBase AudioManager.resetAudioPortGeneration(); synchronized (mRoutingChangeListeners) { for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) { - Handler handler = delegate.getHandler(); - if (handler != null) { - handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE); - } + delegate.notifyClient(); } } } @@ -2943,56 +2940,6 @@ public class AudioTrack extends PlayerBase } } - /** - * Helper class to handle the forwarding of native events to the appropriate listener - * (potentially) handled in a different thread - */ - private class NativeRoutingEventHandlerDelegate { - private final Handler mHandler; - - NativeRoutingEventHandlerDelegate(final AudioTrack track, - final AudioRouting.OnRoutingChangedListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - // no given handler, use the looper the AudioTrack was created in - looper = mInitializationLooper; - } - - // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - if (track == null) { - return; - } - switch(msg.what) { - case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE: - if (listener != null) { - listener.onRoutingChanged(track); - } - break; - default: - loge("Unknown native event type: " + msg.what); - break; - } - } - }; - } else { - mHandler = null; - } - } - - Handler getHandler() { - return mHandler; - } - } - //--------------------------------------------------------- // Methods for IPlayer interface //-------------------- diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java index 12e5744d..e2f9b47e 100644 --- a/android/media/MediaDrm.java +++ b/android/media/MediaDrm.java @@ -977,7 +977,7 @@ public final class MediaDrm { public static final String PROPERTY_ALGORITHMS = "algorithms"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "PROPERTY_" }, value = { PROPERTY_VENDOR, PROPERTY_VERSION, PROPERTY_DESCRIPTION, @@ -1010,7 +1010,7 @@ public final class MediaDrm { public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "PROPERTY_" }, value = { PROPERTY_DEVICE_UNIQUE_ID, }) @Retention(RetentionPolicy.SOURCE) diff --git a/android/media/MediaMetadata.java b/android/media/MediaMetadata.java index 31eb948d..94d4d556 100644 --- a/android/media/MediaMetadata.java +++ b/android/media/MediaMetadata.java @@ -45,34 +45,61 @@ public final class MediaMetadata implements Parcelable { /** * @hide */ - @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR, - METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION, - METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI, - METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE, - METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI, - METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_AUTHOR, + METADATA_KEY_WRITER, + METADATA_KEY_COMPOSER, + METADATA_KEY_COMPILATION, + METADATA_KEY_DATE, + METADATA_KEY_GENRE, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI, + METADATA_KEY_DISPLAY_TITLE, + METADATA_KEY_DISPLAY_SUBTITLE, + METADATA_KEY_DISPLAY_DESCRIPTION, + METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_MEDIA_ID, + METADATA_KEY_MEDIA_URI, + }) @Retention(RetentionPolicy.SOURCE) public @interface TextKey {} /** * @hide */ - @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER, - METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_DURATION, + METADATA_KEY_YEAR, + METADATA_KEY_TRACK_NUMBER, + METADATA_KEY_NUM_TRACKS, + METADATA_KEY_DISC_NUMBER, + METADATA_KEY_BT_FOLDER_TYPE, + }) @Retention(RetentionPolicy.SOURCE) public @interface LongKey {} /** * @hide */ - @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_ART, + METADATA_KEY_ALBUM_ART, + METADATA_KEY_DISPLAY_ICON, + }) @Retention(RetentionPolicy.SOURCE) public @interface BitmapKey {} /** * @hide */ - @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING}) + @StringDef(prefix = { "METADATA_KEY_" }, value = { + METADATA_KEY_USER_RATING, + METADATA_KEY_RATING, + }) @Retention(RetentionPolicy.SOURCE) public @interface RatingKey {} diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java index 0b864018..745eb74d 100644 --- a/android/media/MediaMetadataRetriever.java +++ b/android/media/MediaMetadataRetriever.java @@ -226,9 +226,12 @@ public class MediaMetadataRetriever /** * Call this method after setDataSource(). This method finds a * representative frame close to the given time position by considering - * the given option if possible, and returns it as a bitmap. This is - * useful for generating a thumbnail for an input data source or just - * obtain and display a frame at the given time position. + * the given option if possible, and returns it as a bitmap. + * + *

      If you don't need a full-resolution + * frame (for example, because you need a thumbnail image), use + * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this + * method.

      * * @param timeUs The time position where the frame will be retrieved. * When retrieving the frame at the given time position, there is no @@ -315,11 +318,15 @@ public class MediaMetadataRetriever /** * Call this method after setDataSource(). This method finds a * representative frame close to the given time position if possible, - * and returns it as a bitmap. This is useful for generating a thumbnail - * for an input data source. Call this method if one does not care + * and returns it as a bitmap. Call this method if one does not care * how the frame is found as long as it is close to the given time; * otherwise, please call {@link #getFrameAtTime(long, int)}. * + *

      If you don't need a full-resolution + * frame (for example, because you need a thumbnail image), use + * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this + * method.

      + * * @param timeUs The time position where the frame will be retrieved. * When retrieving the frame at the given time position, there is no * guarentee that the data source has a frame located at the position. @@ -339,11 +346,15 @@ public class MediaMetadataRetriever /** * Call this method after setDataSource(). This method finds a * representative frame at any time position if possible, - * and returns it as a bitmap. This is useful for generating a thumbnail - * for an input data source. Call this method if one does not + * and returns it as a bitmap. Call this method if one does not * care about where the frame is located; otherwise, please call * {@link #getFrameAtTime(long)} or {@link #getFrameAtTime(long, int)} * + *

      If you don't need a full-resolution + * frame (for example, because you need a thumbnail image), use + * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this + * method.

      + * * @return A Bitmap containing a representative video frame, which * can be null, if such a frame cannot be retrieved. * diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java index 649c091b..1bc3dfa4 100644 --- a/android/media/MediaPlayer.java +++ b/android/media/MediaPlayer.java @@ -1514,7 +1514,8 @@ public class MediaPlayer extends PlayerBase if (listener != null && !mRoutingChangeListeners.containsKey(listener)) { enableNativeRoutingCallbacksLocked(true); mRoutingChangeListeners.put( - listener, new NativeRoutingEventHandlerDelegate(this, listener, handler)); + listener, new NativeRoutingEventHandlerDelegate(this, listener, + handler != null ? handler : mEventHandler)); } } } @@ -1535,36 +1536,6 @@ public class MediaPlayer extends PlayerBase } } - /** - * Helper class to handle the forwarding of native events to the appropriate listener - * (potentially) handled in a different thread - */ - private class NativeRoutingEventHandlerDelegate { - private MediaPlayer mMediaPlayer; - private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener; - private Handler mHandler; - - NativeRoutingEventHandlerDelegate(final MediaPlayer mediaPlayer, - final AudioRouting.OnRoutingChangedListener listener, Handler handler) { - mMediaPlayer = mediaPlayer; - mOnRoutingChangedListener = listener; - mHandler = handler != null ? handler : mEventHandler; - } - - void notifyClient() { - if (mHandler != null) { - mHandler.post(new Runnable() { - @Override - public void run() { - if (mOnRoutingChangedListener != null) { - mOnRoutingChangedListener.onRoutingChanged(mMediaPlayer); - } - } - }); - } - } - } - private native final boolean native_setOutputDevice(int deviceId); private native final int native_getRoutedDeviceId(); private native final void native_enableDeviceCallback(boolean enabled); diff --git a/android/media/NativeRoutingEventHandlerDelegate.java b/android/media/NativeRoutingEventHandlerDelegate.java new file mode 100644 index 00000000..9a6baf17 --- /dev/null +++ b/android/media/NativeRoutingEventHandlerDelegate.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.os.Handler; + +/** + * Helper class {@link AudioTrack}, {@link AudioRecord}, {@link MediaPlayer} and {@link MediaRecorder} + * to handle the forwarding of native events to the appropriate listener + * (potentially) handled in a different thread. + * @hide + */ +class NativeRoutingEventHandlerDelegate { + private AudioRouting mAudioRouting; + private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener; + private Handler mHandler; + + NativeRoutingEventHandlerDelegate(final AudioRouting audioRouting, + final AudioRouting.OnRoutingChangedListener listener, Handler handler) { + mAudioRouting = audioRouting; + mOnRoutingChangedListener = listener; + mHandler = handler; + } + + void notifyClient() { + if (mHandler != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mOnRoutingChangedListener != null) { + mOnRoutingChangedListener.onRoutingChanged(mAudioRouting); + } + } + }); + } + } +} diff --git a/android/media/session/PlaybackState.java b/android/media/session/PlaybackState.java index 8283c8b9..17d16b89 100644 --- a/android/media/session/PlaybackState.java +++ b/android/media/session/PlaybackState.java @@ -17,6 +17,7 @@ package android.media.session; import android.annotation.DrawableRes; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.Nullable; import android.media.RemoteControlClient; import android.os.Bundle; @@ -41,7 +42,7 @@ public final class PlaybackState implements Parcelable { /** * @hide */ - @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, + @LongDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING, ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH, ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE, diff --git a/android/media/tv/TvContract.java b/android/media/tv/TvContract.java index 0f460960..3bbc2c4e 100644 --- a/android/media/tv/TvContract.java +++ b/android/media/tv/TvContract.java @@ -1650,7 +1650,7 @@ public final class TvContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "TYPE_" }, value = { TYPE_OTHER, TYPE_NTSC, TYPE_PAL, @@ -1863,7 +1863,7 @@ public final class TvContract { public static final String TYPE_PREVIEW = "TYPE_PREVIEW"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "SERVICE_TYPE_" }, value = { SERVICE_TYPE_OTHER, SERVICE_TYPE_AUDIO_VIDEO, SERVICE_TYPE_AUDIO, @@ -1881,7 +1881,7 @@ public final class TvContract { public static final String SERVICE_TYPE_AUDIO = "SERVICE_TYPE_AUDIO"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "VIDEO_FORMAT_" }, value = { VIDEO_FORMAT_240P, VIDEO_FORMAT_360P, VIDEO_FORMAT_480I, @@ -1930,7 +1930,7 @@ public final class TvContract { public static final String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P"; /** @hide */ - @StringDef({ + @StringDef(prefix = { "VIDEO_RESOLUTION_" }, value = { VIDEO_RESOLUTION_SD, VIDEO_RESOLUTION_ED, VIDEO_RESOLUTION_HD, diff --git a/android/mtp/MtpDatabase.java b/android/mtp/MtpDatabase.java index aaf18e7f..a647dcc2 100644 --- a/android/mtp/MtpDatabase.java +++ b/android/mtp/MtpDatabase.java @@ -30,6 +30,7 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.RemoteException; import android.os.SystemProperties; +import android.os.storage.StorageVolume; import android.provider.MediaStore; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Files; @@ -40,21 +41,31 @@ import android.view.WindowManager; import dalvik.system.CloseGuard; +import com.google.android.collect.Sets; + import java.io.File; -import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.IntStream; +import java.util.stream.Stream; /** + * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses + * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File + * operations are also reflected in MediaProvider if possible. + * operations * {@hide} */ public class MtpDatabase implements AutoCloseable { - private static final String TAG = "MtpDatabase"; + private static final String TAG = MtpDatabase.class.getSimpleName(); - private final Context mUserContext; private final Context mContext; - private final String mPackageName; private final ContentProviderClient mMediaProvider; private final String mVolumeName; private final Uri mObjectsUri; @@ -63,86 +74,158 @@ public class MtpDatabase implements AutoCloseable { private final AtomicBoolean mClosed = new AtomicBoolean(); private final CloseGuard mCloseGuard = CloseGuard.get(); - // path to primary storage - private final String mMediaStoragePath; - // if not null, restrict all queries to these subdirectories - private final String[] mSubDirectories; - // where clause for restricting queries to files in mSubDirectories - private String mSubDirectoriesWhere; - // where arguments for restricting queries to files in mSubDirectories - private String[] mSubDirectoriesWhereArgs; - - private final HashMap mStorageMap = new HashMap(); + private final HashMap mStorageMap = new HashMap<>(); // cached property groups for single properties - private final HashMap mPropertyGroupsByProperty - = new HashMap(); + private final HashMap mPropertyGroupsByProperty = new HashMap<>(); // cached property groups for all properties for a given format - private final HashMap mPropertyGroupsByFormat - = new HashMap(); - - // true if the database has been modified in the current MTP session - private boolean mDatabaseModified; + private final HashMap mPropertyGroupsByFormat = new HashMap<>(); // SharedPreferences for writable MTP device properties private SharedPreferences mDeviceProperties; - private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1; - private static final String[] ID_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 + // Cached device properties + private int mBatteryLevel; + private int mBatteryScale; + private int mDeviceType; + + private MtpServer mServer; + private MtpStorageManager mManager; + + private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; + private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID}; + private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA}; + private static final String NO_MEDIA = ".nomedia"; + + static { + System.loadLibrary("media_jni"); + } + + private static final int[] PLAYBACK_FORMATS = { + // allow transferring arbitrary files + MtpConstants.FORMAT_UNDEFINED, + + MtpConstants.FORMAT_ASSOCIATION, + MtpConstants.FORMAT_TEXT, + MtpConstants.FORMAT_HTML, + MtpConstants.FORMAT_WAV, + MtpConstants.FORMAT_MP3, + MtpConstants.FORMAT_MPEG, + MtpConstants.FORMAT_EXIF_JPEG, + MtpConstants.FORMAT_TIFF_EP, + MtpConstants.FORMAT_BMP, + MtpConstants.FORMAT_GIF, + MtpConstants.FORMAT_JFIF, + MtpConstants.FORMAT_PNG, + MtpConstants.FORMAT_TIFF, + MtpConstants.FORMAT_WMA, + MtpConstants.FORMAT_OGG, + MtpConstants.FORMAT_AAC, + MtpConstants.FORMAT_MP4_CONTAINER, + MtpConstants.FORMAT_MP2, + MtpConstants.FORMAT_3GP_CONTAINER, + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, + MtpConstants.FORMAT_WPL_PLAYLIST, + MtpConstants.FORMAT_M3U_PLAYLIST, + MtpConstants.FORMAT_PLS_PLAYLIST, + MtpConstants.FORMAT_XML_DOCUMENT, + MtpConstants.FORMAT_FLAC, + MtpConstants.FORMAT_DNG, + MtpConstants.FORMAT_HEIF, }; - private static final String[] PATH_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 + + private static final int[] FILE_PROPERTIES = { + MtpConstants.PROPERTY_STORAGE_ID, + MtpConstants.PROPERTY_OBJECT_FORMAT, + MtpConstants.PROPERTY_PROTECTION_STATUS, + MtpConstants.PROPERTY_OBJECT_SIZE, + MtpConstants.PROPERTY_OBJECT_FILE_NAME, + MtpConstants.PROPERTY_DATE_MODIFIED, + MtpConstants.PROPERTY_PERSISTENT_UID, + MtpConstants.PROPERTY_PARENT_OBJECT, + MtpConstants.PROPERTY_NAME, + MtpConstants.PROPERTY_DISPLAY_NAME, + MtpConstants.PROPERTY_DATE_ADDED, }; - private static final String[] FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.FORMAT, // 1 + + private static final int[] AUDIO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_ALBUM_ARTIST, + MtpConstants.PROPERTY_TRACK, + MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_GENRE, + MtpConstants.PROPERTY_COMPOSER, + MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, + MtpConstants.PROPERTY_BITRATE_TYPE, + MtpConstants.PROPERTY_AUDIO_BITRATE, + MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, + MtpConstants.PROPERTY_SAMPLE_RATE, }; - private static final String[] PATH_FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 - Files.FileColumns.FORMAT, // 2 + + private static final int[] VIDEO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_DESCRIPTION, }; - private static final String[] OBJECT_INFO_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.STORAGE_ID, // 1 - Files.FileColumns.FORMAT, // 2 - Files.FileColumns.PARENT, // 3 - Files.FileColumns.DATA, // 4 - Files.FileColumns.DATE_ADDED, // 5 - Files.FileColumns.DATE_MODIFIED, // 6 + + private static final int[] IMAGE_PROPERTIES = { + MtpConstants.PROPERTY_DESCRIPTION, }; - private static final String ID_WHERE = Files.FileColumns._ID + "=?"; - private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; - private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?"; - private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?"; - private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; - private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND " - + Files.FileColumns.FORMAT + "=?"; - private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; - private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; - private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND " - + Files.FileColumns.PARENT + "=?"; + private static final int[] DEVICE_PROPERTIES = { + MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, + MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, + MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, + MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, + MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, + }; - private MtpServer mServer; + private int[] getSupportedObjectProperties(int format) { + switch (format) { + case MtpConstants.FORMAT_MP3: + case MtpConstants.FORMAT_WAV: + case MtpConstants.FORMAT_WMA: + case MtpConstants.FORMAT_OGG: + case MtpConstants.FORMAT_AAC: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(AUDIO_PROPERTIES)).toArray(); + case MtpConstants.FORMAT_MPEG: + case MtpConstants.FORMAT_3GP_CONTAINER: + case MtpConstants.FORMAT_WMV: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(VIDEO_PROPERTIES)).toArray(); + case MtpConstants.FORMAT_EXIF_JPEG: + case MtpConstants.FORMAT_GIF: + case MtpConstants.FORMAT_PNG: + case MtpConstants.FORMAT_BMP: + case MtpConstants.FORMAT_DNG: + case MtpConstants.FORMAT_HEIF: + return IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(IMAGE_PROPERTIES)).toArray(); + default: + return FILE_PROPERTIES; + } + } - // read from native code - private int mBatteryLevel; - private int mBatteryScale; + private int[] getSupportedDeviceProperties() { + return DEVICE_PROPERTIES; + } - private int mDeviceType; + private int[] getSupportedPlaybackFormats() { + return PLAYBACK_FORMATS; + } - static { - System.loadLibrary("media_jni"); + private int[] getSupportedCaptureFormats() { + // no capture formats yet + return null; } private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { - @Override + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { @@ -160,61 +243,42 @@ public class MtpDatabase implements AutoCloseable { } }; - public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath, + public MtpDatabase(Context context, Context userContext, String volumeName, String[] subDirectories) { native_setup(); - mContext = context; - mUserContext = userContext; - mPackageName = context.getPackageName(); mMediaProvider = userContext.getContentResolver() .acquireContentProviderClient(MediaStore.AUTHORITY); mVolumeName = volumeName; - mMediaStoragePath = storagePath; mObjectsUri = Files.getMtpObjectsUri(volumeName); mMediaScanner = new MediaScanner(context, mVolumeName); - - mSubDirectories = subDirectories; - if (subDirectories != null) { - // Compute "where" string for restricting queries to subdirectories - StringBuilder builder = new StringBuilder(); - builder.append("("); - int count = subDirectories.length; - for (int i = 0; i < count; i++) { - builder.append(Files.FileColumns.DATA + "=? OR " - + Files.FileColumns.DATA + " LIKE ?"); - if (i != count - 1) { - builder.append(" OR "); - } + mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() { + @Override + public void sendObjectAdded(int id) { + if (MtpDatabase.this.mServer != null) + MtpDatabase.this.mServer.sendObjectAdded(id); } - builder.append(")"); - mSubDirectoriesWhere = builder.toString(); - - // Compute "where" arguments for restricting queries to subdirectories - mSubDirectoriesWhereArgs = new String[count * 2]; - for (int i = 0, j = 0; i < count; i++) { - String path = subDirectories[i]; - mSubDirectoriesWhereArgs[j++] = path; - mSubDirectoriesWhereArgs[j++] = path + "/%"; + + @Override + public void sendObjectRemoved(int id) { + if (MtpDatabase.this.mServer != null) + MtpDatabase.this.mServer.sendObjectRemoved(id); } - } + }, subDirectories == null ? null : Sets.newHashSet(subDirectories)); initDeviceProperties(context); mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0); - mCloseGuard.open("close"); } public void setServer(MtpServer server) { mServer = server; - // always unregister before registering try { mContext.unregisterReceiver(mBatteryReceiver); } catch (IllegalArgumentException e) { // wasn't previously registered, ignore } - // register for battery notifications when we are connected if (server != null) { mContext.registerReceiver(mBatteryReceiver, @@ -224,6 +288,7 @@ public class MtpDatabase implements AutoCloseable { @Override public void close() { + mManager.close(); mCloseGuard.close(); if (mClosed.compareAndSet(false, true)) { mMediaScanner.close(); @@ -238,24 +303,32 @@ public class MtpDatabase implements AutoCloseable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } - close(); } finally { super.finalize(); } } - public void addStorage(MtpStorage storage) { - mStorageMap.put(storage.getPath(), storage); + public void addStorage(StorageVolume storage) { + MtpStorage mtpStorage = mManager.addMtpStorage(storage); + mStorageMap.put(storage.getPath(), mtpStorage); + mServer.addStorage(mtpStorage); } - public void removeStorage(MtpStorage storage) { + public void removeStorage(StorageVolume storage) { + MtpStorage mtpStorage = mStorageMap.get(storage.getPath()); + if (mtpStorage == null) { + return; + } + mServer.removeStorage(mtpStorage); + mManager.removeMtpStorage(mtpStorage); mStorageMap.remove(storage.getPath()); } private void initDeviceProperties(Context context) { final String devicePropertiesName = "device-properties"; - mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE); + mDeviceProperties = context.getSharedPreferences(devicePropertiesName, + Context.MODE_PRIVATE); File databaseFile = context.getDatabasePath(devicePropertiesName); if (databaseFile.exists()) { @@ -266,7 +339,7 @@ public class MtpDatabase implements AutoCloseable { try { db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); if (db != null) { - c = db.query("properties", new String[] { "_id", "code", "value" }, + c = db.query("properties", new String[]{"_id", "code", "value"}, null, null, null, null, null); if (c != null) { SharedPreferences.Editor e = mDeviceProperties.edit(); @@ -288,602 +361,371 @@ public class MtpDatabase implements AutoCloseable { } } - // check to see if the path is contained in one of our storage subdirectories - // returns true if we have no special subdirectories - private boolean inStorageSubDirectory(String path) { - if (mSubDirectories == null) return true; - if (path == null) return false; - - boolean allowed = false; - int pathLength = path.length(); - for (int i = 0; i < mSubDirectories.length && !allowed; i++) { - String subdir = mSubDirectories[i]; - int subdirLength = subdir.length(); - if (subdirLength < pathLength && - path.charAt(subdirLength) == '/' && - path.startsWith(subdir)) { - allowed = true; - } + private int beginSendObject(String path, int format, int parent, int storageId) { + MtpStorageManager.MtpObject parentObj = + parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent); + if (parentObj == null) { + return -1; } - return allowed; + + Path objPath = Paths.get(path); + return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format); } - // check to see if the path matches one of our storage subdirectories - // returns true if we have no special subdirectories - private boolean isStorageSubDirectory(String path) { - if (mSubDirectories == null) return false; - for (int i = 0; i < mSubDirectories.length; i++) { - if (path.equals(mSubDirectories[i])) { - return true; - } + private void endSendObject(int handle, boolean succeeded) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null || !mManager.endSendObject(obj, succeeded)) { + Log.e(TAG, "Failed to successfully end send object"); + return; } - return false; - } + // Add the new file to MediaProvider + if (succeeded) { + String path = obj.getPath().toString(); + int format = obj.getFormat(); + // Get parent info from MediaProvider, since the id is different from MTP's + ContentValues values = new ContentValues(); + values.put(Files.FileColumns.DATA, path); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); + try { + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(obj.getParent().getPath()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The parent isn't in MediaProvider. Don't add the new file. + return; + } + } - // returns true if the path is in the storage root - private boolean inStorageRoot(String path) { - try { - File f = new File(path); - String canonical = f.getCanonicalPath(); - for (String root: mStorageMap.keySet()) { - if (canonical.startsWith(root)) { - return true; + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format); } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in beginSendObject", e); } - } catch (IOException e) { - // ignore } - return false; } - private int beginSendObject(String path, int format, int parent, - int storageId, long size, long modified) { - // if the path is outside of the storage root, do not allow access - if (!inStorageRoot(path)) { - Log.e(TAG, "attempt to put file outside of storage area: " + path); - return -1; - } - // if mSubDirectories is not null, do not allow copying files to any other locations - if (!inStorageSubDirectory(path)) return -1; + private void rescanFile(String path, int handle, int format) { + // handle abstract playlists separately + // they do not exist in the file system so don't use the media scanner here + if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { + // extract name from path + String name = path; + int lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + // strip trailing ".pla" from the name + if (name.endsWith(".pla")) { + name = name.substring(0, name.length() - 4); + } - // make sure the object does not exist - if (path != null) { - Cursor c = null; + ContentValues values = new ContentValues(1); + values.put(Audio.Playlists.DATA, path); + values.put(Audio.Playlists.NAME, name); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); + values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); try { - c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, - new String[] { path }, null, null); - if (c != null && c.getCount() > 0) { - Log.w(TAG, "file already exists in beginSendObject: " + path); - return -1; - } + mMediaProvider.insert( + Audio.Playlists.EXTERNAL_CONTENT_URI, values); } catch (RemoteException e) { - Log.e(TAG, "RemoteException in beginSendObject", e); - } finally { - if (c != null) { - c.close(); - } + Log.e(TAG, "RemoteException in endSendObject", e); } + } else { + mMediaScanner.scanMtpFile(path, handle, format); } + } - mDatabaseModified = true; - ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, path); - values.put(Files.FileColumns.FORMAT, format); - values.put(Files.FileColumns.PARENT, parent); - values.put(Files.FileColumns.STORAGE_ID, storageId); - values.put(Files.FileColumns.SIZE, size); - values.put(Files.FileColumns.DATE_MODIFIED, modified); - - try { - Uri uri = mMediaProvider.insert(mObjectsUri, values); - if (uri != null) { - return Integer.parseInt(uri.getPathSegments().get(2)); - } else { - return -1; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in beginSendObject", e); - return -1; + private int[] getObjectList(int storageID, int format, int parent) { + Stream objectStream = mManager.getObjects(parent, + format, storageID); + if (objectStream == null) { + return null; } + return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray(); } - private void endSendObject(String path, int handle, int format, boolean succeeded) { - if (succeeded) { - // handle abstract playlists separately - // they do not exist in the file system so don't use the media scanner here - if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { - // extract name from path - String name = path; - int lastSlash = name.lastIndexOf('/'); - if (lastSlash >= 0) { - name = name.substring(lastSlash + 1); - } - // strip trailing ".pla" from the name - if (name.endsWith(".pla")) { - name = name.substring(0, name.length() - 4); - } - - ContentValues values = new ContentValues(1); - values.put(Audio.Playlists.DATA, path); - values.put(Audio.Playlists.NAME, name); - values.put(Files.FileColumns.FORMAT, format); - values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); - values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); - try { - Uri uri = mMediaProvider.insert( - Audio.Playlists.EXTERNAL_CONTENT_URI, values); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in endSendObject", e); - } - } else { - mMediaScanner.scanMtpFile(path, handle, format); - } - } else { - deleteFile(handle); + private int getNumObjects(int storageID, int format, int parent) { + Stream objectStream = mManager.getObjects(parent, + format, storageID); + if (objectStream == null) { + return -1; } + return (int) objectStream.count(); } - private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException { - String where; - String[] whereArgs; - - if (storageID == 0xFFFFFFFF) { - // query all stores - if (format == 0) { - // query all formats - if (parent == 0) { - // query all objects - where = null; - whereArgs = null; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - } - where = PARENT_WHERE; - whereArgs = new String[] { Integer.toString(parent) }; - } - } else { - // query specific format - if (parent == 0) { - // query all objects - where = FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(format) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - } - where = FORMAT_PARENT_WHERE; - whereArgs = new String[] { Integer.toString(format), - Integer.toString(parent) }; - } - } - } else { - // query specific store - if (format == 0) { - // query all formats - if (parent == 0) { - // query all objects - where = STORAGE_WHERE; - whereArgs = new String[] { Integer.toString(storageID) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - where = STORAGE_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(storageID), - Integer.toString(parent)}; - } else { - // If a parent is specified, the storage is redundant - where = PARENT_WHERE; - whereArgs = new String[]{Integer.toString(parent)}; - } - } - } else { - // query specific format - if (parent == 0) { - // query all objects - where = STORAGE_FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(storageID), - Integer.toString(format) }; - } else { - if (parent == 0xFFFFFFFF) { - // all objects in root of store - parent = 0; - where = STORAGE_FORMAT_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(storageID), - Integer.toString(format), - Integer.toString(parent)}; - } else { - // If a parent is specified, the storage is redundant - where = FORMAT_PARENT_WHERE; - whereArgs = new String[]{Integer.toString(format), - Integer.toString(parent)}; - } - } + private MtpPropertyList getObjectPropertyList(int handle, int format, int property, + int groupCode, int depth) { + // FIXME - implement group support + if (property == 0) { + if (groupCode == 0) { + return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED); } + return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); } - - // if we are restricting queries to mSubDirectories, we need to add the restriction - // onto our "where" arguments - if (mSubDirectoriesWhere != null) { - if (where == null) { - where = mSubDirectoriesWhere; - whereArgs = mSubDirectoriesWhereArgs; - } else { - where = where + " AND " + mSubDirectoriesWhere; - - // create new array to hold whereArgs and mSubDirectoriesWhereArgs - String[] newWhereArgs = - new String[whereArgs.length + mSubDirectoriesWhereArgs.length]; - int i, j; - for (i = 0; i < whereArgs.length; i++) { - newWhereArgs[i] = whereArgs[i]; - } - for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) { - newWhereArgs[i] = mSubDirectoriesWhereArgs[j]; - } - whereArgs = newWhereArgs; - } + if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) { + // request all objects starting at root + handle = 0xFFFFFFFF; + depth = 0; } - - return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, - whereArgs, null, null); - } - - private int[] getObjectList(int storageID, int format, int parent) { - Cursor c = null; - try { - c = createObjectQuery(storageID, format, parent); - if (c == null) { - return null; + if (!(depth == 0 || depth == 1)) { + // we only support depth 0 and 1 + // depth 0: single object, depth 1: immediate children + return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED); + } + Stream objectStream = Stream.of(); + if (handle == 0xFFFFFFFF) { + // All objects are requested + objectStream = mManager.getObjects(0, format, 0xFFFFFFFF); + if (objectStream == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - int count = c.getCount(); - if (count > 0) { - int[] result = new int[count]; - for (int i = 0; i < count; i++) { - c.moveToNext(); - result[i] = c.getInt(0); - } - return result; + } else if (handle != 0) { + // Add the requested object if format matches + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectList", e); - } finally { - if (c != null) { - c.close(); + if (obj.getFormat() == format || format == 0) { + objectStream = Stream.of(obj); } } - return null; - } - - private int getNumObjects(int storageID, int format, int parent) { - Cursor c = null; - try { - c = createObjectQuery(storageID, format, parent); - if (c != null) { - return c.getCount(); + if (handle == 0 || depth == 1) { + if (handle == 0) { + handle = 0xFFFFFFFF; } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getNumObjects", e); - } finally { - if (c != null) { - c.close(); + // Get the direct children of root or this object. + Stream childStream = mManager.getObjects(handle, format, + 0xFFFFFFFF); + if (childStream == null) { + return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } - } - return -1; - } - - private int[] getSupportedPlaybackFormats() { - return new int[] { - // allow transfering arbitrary files - MtpConstants.FORMAT_UNDEFINED, - - MtpConstants.FORMAT_ASSOCIATION, - MtpConstants.FORMAT_TEXT, - MtpConstants.FORMAT_HTML, - MtpConstants.FORMAT_WAV, - MtpConstants.FORMAT_MP3, - MtpConstants.FORMAT_MPEG, - MtpConstants.FORMAT_EXIF_JPEG, - MtpConstants.FORMAT_TIFF_EP, - MtpConstants.FORMAT_BMP, - MtpConstants.FORMAT_GIF, - MtpConstants.FORMAT_JFIF, - MtpConstants.FORMAT_PNG, - MtpConstants.FORMAT_TIFF, - MtpConstants.FORMAT_WMA, - MtpConstants.FORMAT_OGG, - MtpConstants.FORMAT_AAC, - MtpConstants.FORMAT_MP4_CONTAINER, - MtpConstants.FORMAT_MP2, - MtpConstants.FORMAT_3GP_CONTAINER, - MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, - MtpConstants.FORMAT_WPL_PLAYLIST, - MtpConstants.FORMAT_M3U_PLAYLIST, - MtpConstants.FORMAT_PLS_PLAYLIST, - MtpConstants.FORMAT_XML_DOCUMENT, - MtpConstants.FORMAT_FLAC, - MtpConstants.FORMAT_DNG, - MtpConstants.FORMAT_HEIF, - }; - } - - private int[] getSupportedCaptureFormats() { - // no capture formats yet - return null; - } - - static final int[] FILE_PROPERTIES = { - // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES - // and IMAGE_PROPERTIES below - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - }; - - static final int[] AUDIO_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // audio specific properties - MtpConstants.PROPERTY_ARTIST, - MtpConstants.PROPERTY_ALBUM_NAME, - MtpConstants.PROPERTY_ALBUM_ARTIST, - MtpConstants.PROPERTY_TRACK, - MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, - MtpConstants.PROPERTY_DURATION, - MtpConstants.PROPERTY_GENRE, - MtpConstants.PROPERTY_COMPOSER, - MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, - MtpConstants.PROPERTY_BITRATE_TYPE, - MtpConstants.PROPERTY_AUDIO_BITRATE, - MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, - MtpConstants.PROPERTY_SAMPLE_RATE, - }; - - static final int[] VIDEO_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // video specific properties - MtpConstants.PROPERTY_ARTIST, - MtpConstants.PROPERTY_ALBUM_NAME, - MtpConstants.PROPERTY_DURATION, - MtpConstants.PROPERTY_DESCRIPTION, - }; - - static final int[] IMAGE_PROPERTIES = { - // NOTE must match FILE_PROPERTIES above - MtpConstants.PROPERTY_STORAGE_ID, - MtpConstants.PROPERTY_OBJECT_FORMAT, - MtpConstants.PROPERTY_PROTECTION_STATUS, - MtpConstants.PROPERTY_OBJECT_SIZE, - MtpConstants.PROPERTY_OBJECT_FILE_NAME, - MtpConstants.PROPERTY_DATE_MODIFIED, - MtpConstants.PROPERTY_PARENT_OBJECT, - MtpConstants.PROPERTY_PERSISTENT_UID, - MtpConstants.PROPERTY_NAME, - MtpConstants.PROPERTY_DISPLAY_NAME, - MtpConstants.PROPERTY_DATE_ADDED, - - // image specific properties - MtpConstants.PROPERTY_DESCRIPTION, - }; - - private int[] getSupportedObjectProperties(int format) { - switch (format) { - case MtpConstants.FORMAT_MP3: - case MtpConstants.FORMAT_WAV: - case MtpConstants.FORMAT_WMA: - case MtpConstants.FORMAT_OGG: - case MtpConstants.FORMAT_AAC: - return AUDIO_PROPERTIES; - case MtpConstants.FORMAT_MPEG: - case MtpConstants.FORMAT_3GP_CONTAINER: - case MtpConstants.FORMAT_WMV: - return VIDEO_PROPERTIES; - case MtpConstants.FORMAT_EXIF_JPEG: - case MtpConstants.FORMAT_GIF: - case MtpConstants.FORMAT_PNG: - case MtpConstants.FORMAT_BMP: - case MtpConstants.FORMAT_DNG: - case MtpConstants.FORMAT_HEIF: - return IMAGE_PROPERTIES; - default: - return FILE_PROPERTIES; - } - } - - private int[] getSupportedDeviceProperties() { - return new int[] { - MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, - MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, - MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, - MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, - MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, - }; - } - - private MtpPropertyList getObjectPropertyList(int handle, int format, int property, - int groupCode, int depth) { - // FIXME - implement group support - if (groupCode != 0) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); + objectStream = Stream.concat(objectStream, childStream); } + MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK); MtpPropertyGroup propertyGroup; - if (property == 0xffffffff) { - if (format == 0 && handle != 0 && handle != 0xffffffff) { - // return properties based on the object's format - format = getObjectFormat(handle); - } - propertyGroup = mPropertyGroupsByFormat.get(format); - if (propertyGroup == null) { - int[] propertyList = getSupportedObjectProperties(format); - propertyGroup = new MtpPropertyGroup(this, mMediaProvider, - mVolumeName, propertyList); - mPropertyGroupsByFormat.put(format, propertyGroup); + Iterator iter = objectStream.iterator(); + while (iter.hasNext()) { + MtpStorageManager.MtpObject obj = iter.next(); + if (property == 0xffffffff) { + // Get all properties supported by this object + propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat()); + if (propertyGroup == null) { + int[] propertyList = getSupportedObjectProperties(format); + propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName, + propertyList); + mPropertyGroupsByFormat.put(format, propertyGroup); + } + } else { + // Get this property value + final int[] propertyList = new int[]{property}; + propertyGroup = mPropertyGroupsByProperty.get(property); + if (propertyGroup == null) { + propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName, + propertyList); + mPropertyGroupsByProperty.put(property, propertyGroup); + } } - } else { - propertyGroup = mPropertyGroupsByProperty.get(property); - if (propertyGroup == null) { - final int[] propertyList = new int[] { property }; - propertyGroup = new MtpPropertyGroup( - this, mMediaProvider, mVolumeName, propertyList); - mPropertyGroupsByProperty.put(property, propertyGroup); + int err = propertyGroup.getPropertyList(obj, ret); + if (err != MtpConstants.RESPONSE_OK) { + return new MtpPropertyList(err); } } - - return propertyGroup.getPropertyList(handle, format, depth); + return ret; } private int renameFile(int handle, String newName) { - Cursor c = null; - - // first compute current path - String path = null; - String[] whereArgs = new String[] { Integer.toString(handle) }; - try { - c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, - whereArgs, null, null); - if (c != null && c.moveToNext()) { - path = c.getString(1); - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); - } - } - if (path == null) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } - - // do not allow renaming any of the special subdirectories - if (isStorageSubDirectory(path)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; - } + Path oldPath = obj.getPath(); // now rename the file. make sure this succeeds before updating database - File oldFile = new File(path); - int lastSlash = path.lastIndexOf('/'); - if (lastSlash <= 1) { + if (!mManager.beginRenameObject(obj, newName)) return MtpConstants.RESPONSE_GENERAL_ERROR; + Path newPath = obj.getPath(); + boolean success = oldPath.toFile().renameTo(newPath.toFile()); + if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) { + Log.e(TAG, "Failed to end rename object"); } - String newPath = path.substring(0, lastSlash + 1) + newName; - File newFile = new File(newPath); - boolean success = oldFile.renameTo(newFile); if (!success) { - Log.w(TAG, "renaming "+ path + " to " + newPath + " failed"); return MtpConstants.RESPONSE_GENERAL_ERROR; } - // finally update database + // finally update MediaProvider ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, newPath); - int updated = 0; + values.put(Files.FileColumns.DATA, newPath.toString()); + String[] whereArgs = new String[]{oldPath.toString()}; try { // note - we are relying on a special case in MediaProvider.update() to update // the paths for all children in the case where this is a directory. - updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); + mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs); } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); } - if (updated == 0) { - Log.e(TAG, "Unable to update path for " + path + " to " + newPath); - // this shouldn't happen, but if it does we need to rename the file to its original name - newFile.renameTo(oldFile); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } // check if nomedia status changed - if (newFile.isDirectory()) { + if (obj.isDir()) { // for directories, check if renamed from something hidden to something non-hidden - if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) { + if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) { // directory was unhidden try { - mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null); + mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + newPath); } } } else { // for files, check if renamed from .nomedia to something else - if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia") - && !newPath.toLowerCase(Locale.US).equals(".nomedia")) { + if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA) + && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) { try { - mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null); + mMediaProvider.call(MediaStore.UNHIDE_CALL, + oldPath.getParent().toString(), null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + newPath); } } } - return MtpConstants.RESPONSE_OK; } - private int moveObject(int handle, int newParent, int newStorage, String newPath) { - String[] whereArgs = new String[] { Integer.toString(handle) }; + private int beginMoveObject(int handle, int newParent, int newStorage) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + MtpStorageManager.MtpObject parent = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + if (obj == null || parent == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + + boolean allowed = mManager.beginMoveObject(obj, parent); + return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR; + } - // do not allow renaming any of the special subdirectories - if (isStorageSubDirectory(newPath)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; + private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage, + int objId, boolean success) { + MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ? + mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent); + MtpStorageManager.MtpObject newParentObj = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + MtpStorageManager.MtpObject obj = mManager.getObject(objId); + String name = obj.getName(); + if (newParentObj == null || oldParentObj == null + ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) { + Log.e(TAG, "Failed to end move object"); + return; } - // update database + obj = mManager.getObject(objId); + if (!success || obj == null) + return; + // Get parent info from MediaProvider, since the id is different from MTP's ContentValues values = new ContentValues(); - values.put(Files.FileColumns.DATA, newPath); - values.put(Files.FileColumns.PARENT, newParent); - values.put(Files.FileColumns.STORAGE_ID, newStorage); - int updated = 0; + Path path = newParentObj.getPath().resolve(name); + Path oldPath = oldParentObj.getPath().resolve(name); + values.put(Files.FileColumns.DATA, path.toString()); + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(path.getParent()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The new parent isn't in MediaProvider, so delete the object instead + deleteFromMedia(oldPath, obj.isDir()); + return; + } + } + // update MediaProvider + Cursor c = null; + String[] whereArgs = new String[]{oldPath.toString()}; try { - // note - we are relying on a special case in MediaProvider.update() to update - // the paths for all children in the case where this is a directory. - updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); + int parentId = -1; + if (!oldParentObj.isRoot()) { + parentId = findInMedia(oldPath.getParent()); + } + if (oldParentObj.isRoot() || parentId != -1) { + // Old parent exists in MediaProvider - perform a move + // note - we are relying on a special case in MediaProvider.update() to update + // the paths for all children in the case where this is a directory. + mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs); + } else { + // Old parent doesn't exist - add the object + values.put(Files.FileColumns.FORMAT, obj.getFormat()); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path.toString(), + Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat()); + } + } } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); } - if (updated == 0) { - Log.e(TAG, "Unable to update path for " + handle + " to " + newPath); - return MtpConstants.RESPONSE_GENERAL_ERROR; + } + + private int beginCopyObject(int handle, int newParent, int newStorage) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + MtpStorageManager.MtpObject parent = newParent == 0 ? + mManager.getStorageRoot(newStorage) : mManager.getObject(newParent); + if (obj == null || parent == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + return mManager.beginCopyObject(obj, parent); + } + + private void endCopyObject(int handle, boolean success) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null || !mManager.endCopyObject(obj, success)) { + Log.e(TAG, "Failed to end copy object"); + return; + } + if (!success) { + return; + } + String path = obj.getPath().toString(); + int format = obj.getFormat(); + // Get parent info from MediaProvider, since the id is different from MTP's + ContentValues values = new ContentValues(); + values.put(Files.FileColumns.DATA, path); + values.put(Files.FileColumns.FORMAT, format); + values.put(Files.FileColumns.SIZE, obj.getSize()); + values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime()); + try { + if (obj.getParent().isRoot()) { + values.put(Files.FileColumns.PARENT, 0); + } else { + int parentId = findInMedia(obj.getParent().getPath()); + if (parentId != -1) { + values.put(Files.FileColumns.PARENT, parentId); + } else { + // The parent isn't in MediaProvider. Don't add the new file. + return; + } + } + if (obj.isDir()) { + mMediaScanner.scanDirectories(new String[]{path}); + } else { + Uri uri = mMediaProvider.insert(mObjectsUri, values); + if (uri != null) { + rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format); + } + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in beginSendObject", e); } - return MtpConstants.RESPONSE_OK; } private int setObjectProperty(int handle, int property, - long intValue, String stringValue) { + long intValue, String stringValue) { switch (property) { case MtpConstants.PROPERTY_OBJECT_FILE_NAME: return renameFile(handle, stringValue); @@ -906,24 +748,23 @@ public class MtpDatabase implements AutoCloseable { value.getChars(0, length, outStringValue, 0); outStringValue[length] = 0; return MtpConstants.RESPONSE_OK; - case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE: // use screen size as max image size - Display display = ((WindowManager)mContext.getSystemService( + Display display = ((WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); int width = display.getMaximumSizeDimension(); int height = display.getMaximumSizeDimension(); - String imageSize = Integer.toString(width) + "x" + Integer.toString(height); + String imageSize = Integer.toString(width) + "x" + Integer.toString(height); imageSize.getChars(0, imageSize.length(), outStringValue, 0); outStringValue[imageSize.length()] = 0; return MtpConstants.RESPONSE_OK; - case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE: outIntValue[0] = mDeviceType; return MtpConstants.RESPONSE_OK; - - // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code - + case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL: + outIntValue[0] = mBatteryLevel; + outIntValue[1] = mBatteryScale; + return MtpConstants.RESPONSE_OK; default: return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; } @@ -944,179 +785,144 @@ public class MtpDatabase implements AutoCloseable { } private boolean getObjectInfo(int handle, int[] outStorageFormatParent, - char[] outName, long[] outCreatedModified) { - Cursor c = null; - try { - c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - outStorageFormatParent[0] = c.getInt(1); - outStorageFormatParent[1] = c.getInt(2); - outStorageFormatParent[2] = c.getInt(3); - - // extract name from path - String path = c.getString(4); - int lastSlash = path.lastIndexOf('/'); - int start = (lastSlash >= 0 ? lastSlash + 1 : 0); - int end = path.length(); - if (end - start > 255) { - end = start + 255; - } - path.getChars(start, end, outName, 0); - outName[end - start] = 0; - - outCreatedModified[0] = c.getLong(5); - outCreatedModified[1] = c.getLong(6); - // use modification date as creation date if date added is not set - if (outCreatedModified[0] == 0) { - outCreatedModified[0] = outCreatedModified[1]; - } - return true; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectInfo", e); - } finally { - if (c != null) { - c.close(); - } + char[] outName, long[] outCreatedModified) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return false; } - return false; + outStorageFormatParent[0] = obj.getStorageId(); + outStorageFormatParent[1] = obj.getFormat(); + outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId(); + + int nameLen = Integer.min(obj.getName().length(), 255); + obj.getName().getChars(0, nameLen, outName, 0); + outName[nameLen] = 0; + + outCreatedModified[0] = obj.getModifiedTime(); + outCreatedModified[1] = obj.getModifiedTime(); + return true; } private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) { - if (handle == 0) { - // special case root directory - mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0); - outFilePath[mMediaStoragePath.length()] = 0; - outFileLengthFormat[0] = 0; - outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION; - return MtpConstants.RESPONSE_OK; + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } - Cursor c = null; - try { - c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - String path = c.getString(1); - path.getChars(0, path.length(), outFilePath, 0); - outFilePath[path.length()] = 0; - // File transfers from device to host will likely fail if the size is incorrect. - // So to be safe, use the actual file size here. - outFileLengthFormat[0] = new File(path).length(); - outFileLengthFormat[1] = c.getLong(2); - return MtpConstants.RESPONSE_OK; - } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); + + String path = obj.getPath().toString(); + int pathLen = Integer.min(path.length(), 4096); + path.getChars(0, pathLen, outFilePath, 0); + outFilePath[pathLen] = 0; + + outFileLengthFormat[0] = obj.getSize(); + outFileLengthFormat[1] = obj.getFormat(); + return MtpConstants.RESPONSE_OK; + } + + private int getObjectFormat(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return -1; + } + return obj.getFormat(); + } + + private int beginDeleteObject(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + } + if (!mManager.beginRemoveObject(obj)) { return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); - } } + return MtpConstants.RESPONSE_OK; } - private int getObjectFormat(int handle) { + private void endDeleteObject(int handle, boolean success) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return; + } + if (!mManager.endRemoveObject(obj, success)) + Log.e(TAG, "Failed to end remove object"); + if (success) + deleteFromMedia(obj.getPath(), obj.isDir()); + } + + private int findInMedia(Path path) { + int ret = -1; Cursor c = null; try { - c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); + c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, + new String[]{path.toString()}, null, null); if (c != null && c.moveToNext()) { - return c.getInt(1); - } else { - return -1; + ret = c.getInt(0); } } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); - return -1; + Log.e(TAG, "Error finding " + path + " in MediaProvider"); } finally { - if (c != null) { + if (c != null) c.close(); - } } + return ret; } - private int deleteFile(int handle) { - mDatabaseModified = true; - String path = null; - int format = 0; - - Cursor c = null; + private void deleteFromMedia(Path path, boolean isDir) { try { - c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, null, null); - if (c != null && c.moveToNext()) { - // don't convert to media path here, since we will be matching - // against paths in the database matching /data/media - path = c.getString(1); - format = c.getInt(2); - } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - - if (path == null || format == 0) { - return MtpConstants.RESPONSE_GENERAL_ERROR; - } - - // do not allow deleting any of the special subdirectories - if (isStorageSubDirectory(path)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; - } - - if (format == MtpConstants.FORMAT_ASSOCIATION) { + // Delete the object(s) from MediaProvider, but ignore errors. + if (isDir) { // recursive case - delete all children first - Uri uri = Files.getMtpObjectsUri(mVolumeName); - int count = mMediaProvider.delete(uri, - // the 'like' makes it use the index, the 'lower()' makes it correct - // when the path contains sqlite wildcard characters - "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", - new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"}); + mMediaProvider.delete(mObjectsUri, + // the 'like' makes it use the index, the 'lower()' makes it correct + // when the path contains sqlite wildcard characters + "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", + new String[]{path + "/%", Integer.toString(path.toString().length() + 1), + path.toString() + "/"}); } - Uri uri = Files.getMtpObjectsUri(mVolumeName, handle); - if (mMediaProvider.delete(uri, null, null) > 0) { - if (format != MtpConstants.FORMAT_ASSOCIATION - && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { + String[] whereArgs = new String[]{path.toString()}; + if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) { + if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) { try { - String parentPath = path.substring(0, path.lastIndexOf("/")); + String parentPath = path.getParent().toString(); mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null); } catch (RemoteException e) { Log.e(TAG, "failed to unhide/rescan for " + path); } } - return MtpConstants.RESPONSE_OK; } else { - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in deleteFile", e); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); + Log.i(TAG, "Mediaprovider didn't delete " + path); } + } catch (Exception e) { + Log.d(TAG, "Failed to delete " + path + " from MediaProvider"); } } private int[] getObjectReferences(int handle) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) + return null; + // Translate this handle to the MediaProvider Handle + handle = findInMedia(obj.getPath()); + if (handle == -1) + return null; Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); Cursor c = null; try { - c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null); + c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null); if (c == null) { return null; } - int count = c.getCount(); - if (count > 0) { - int[] result = new int[count]; - for (int i = 0; i < count; i++) { - c.moveToNext(); - result[i] = c.getInt(0); + ArrayList result = new ArrayList<>(); + while (c.moveToNext()) { + // Translate result handles back into handles for this session. + String refPath = c.getString(0); + MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath); + if (refObj != null) { + result.add(refObj.getId()); + } } - return result; - } + return result.stream().mapToInt(Integer::intValue).toArray(); } catch (RemoteException e) { Log.e(TAG, "RemoteException in getObjectList", e); } finally { @@ -1128,17 +934,29 @@ public class MtpDatabase implements AutoCloseable { } private int setObjectReferences(int handle, int[] references) { - mDatabaseModified = true; + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) + return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; + // Translate this handle to the MediaProvider Handle + handle = findInMedia(obj.getPath()); + if (handle == -1) + return MtpConstants.RESPONSE_GENERAL_ERROR; Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); - int count = references.length; - ContentValues[] valuesList = new ContentValues[count]; - for (int i = 0; i < count; i++) { + ArrayList valuesList = new ArrayList<>(); + for (int id : references) { + // Translate each reference id to the MediaProvider Id + MtpStorageManager.MtpObject refObj = mManager.getObject(id); + if (refObj == null) + continue; + int refHandle = findInMedia(refObj.getPath()); + if (refHandle == -1) + continue; ContentValues values = new ContentValues(); - values.put(Files.FileColumns._ID, references[i]); - valuesList[i] = values; + values.put(Files.FileColumns._ID, refHandle); + valuesList.add(values); } try { - if (mMediaProvider.bulkInsert(uri, valuesList) > 0) { + if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) { return MtpConstants.RESPONSE_OK; } } catch (RemoteException e) { @@ -1147,17 +965,6 @@ public class MtpDatabase implements AutoCloseable { return MtpConstants.RESPONSE_GENERAL_ERROR; } - private void sessionStarted() { - mDatabaseModified = false; - } - - private void sessionEnded() { - if (mDatabaseModified) { - mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END)); - mDatabaseModified = false; - } - } - // used by the JNI code private long mNativeContext; diff --git a/android/mtp/MtpPropertyGroup.java b/android/mtp/MtpPropertyGroup.java index dea30083..77d0f34f 100644 --- a/android/mtp/MtpPropertyGroup.java +++ b/android/mtp/MtpPropertyGroup.java @@ -23,22 +23,21 @@ import android.os.RemoteException; import android.provider.MediaStore.Audio; import android.provider.MediaStore.Files; import android.provider.MediaStore.Images; -import android.provider.MediaStore.MediaColumns; import android.util.Log; import java.util.ArrayList; +/** + * MtpPropertyGroup represents a list of MTP properties. + * {@hide} + */ class MtpPropertyGroup { - - private static final String TAG = "MtpPropertyGroup"; + private static final String TAG = MtpPropertyGroup.class.getSimpleName(); private class Property { - // MTP property code - int code; - // MTP data type - int type; - // column index for our query - int column; + int code; + int type; + int column; Property(int code, int type, int column) { this.code = code; @@ -47,32 +46,26 @@ class MtpPropertyGroup { } } - private final MtpDatabase mDatabase; private final ContentProviderClient mProvider; private final String mVolumeName; private final Uri mUri; // list of all properties in this group - private final Property[] mProperties; + private final Property[] mProperties; // list of columns for database query - private String[] mColumns; + private String[] mColumns; + + private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; - private static final String ID_WHERE = Files.FileColumns._ID + "=?"; - private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?"; - private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE; - private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; - private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE; // constructs a property group for a list of properties - public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName, - int[] properties) { - mDatabase = database; + public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) { mProvider = provider; mVolumeName = volumeName; mUri = Files.getMtpObjectsUri(volumeName); int count = properties.length; - ArrayList columns = new ArrayList(count); + ArrayList columns = new ArrayList<>(count); columns.add(Files.FileColumns._ID); mProperties = new Property[count]; @@ -90,37 +83,29 @@ class MtpPropertyGroup { String column = null; int type; - switch (code) { + switch (code) { case MtpConstants.PROPERTY_STORAGE_ID: - column = Files.FileColumns.STORAGE_ID; type = MtpConstants.TYPE_UINT32; break; - case MtpConstants.PROPERTY_OBJECT_FORMAT: - column = Files.FileColumns.FORMAT; + case MtpConstants.PROPERTY_OBJECT_FORMAT: type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_PROTECTION_STATUS: - // protection status is always 0 type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_OBJECT_SIZE: - column = Files.FileColumns.SIZE; type = MtpConstants.TYPE_UINT64; break; case MtpConstants.PROPERTY_OBJECT_FILE_NAME: - column = Files.FileColumns.DATA; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_NAME: - column = MediaColumns.TITLE; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_DATE_MODIFIED: - column = Files.FileColumns.DATE_MODIFIED; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_DATE_ADDED: - column = Files.FileColumns.DATE_ADDED; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: @@ -128,12 +113,9 @@ class MtpPropertyGroup { type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_PARENT_OBJECT: - column = Files.FileColumns.PARENT; type = MtpConstants.TYPE_UINT32; break; case MtpConstants.PROPERTY_PERSISTENT_UID: - // PUID is concatenation of storageID and object handle - column = Files.FileColumns.STORAGE_ID; type = MtpConstants.TYPE_UINT128; break; case MtpConstants.PROPERTY_DURATION: @@ -145,7 +127,6 @@ class MtpPropertyGroup { type = MtpConstants.TYPE_UINT16; break; case MtpConstants.PROPERTY_DISPLAY_NAME: - column = MediaColumns.DISPLAY_NAME; type = MtpConstants.TYPE_STR; break; case MtpConstants.PROPERTY_ARTIST: @@ -195,40 +176,19 @@ class MtpPropertyGroup { } } - private String queryString(int id, String column) { - Cursor c = null; - try { - // for now we are only reading properties from the "objects" table - c = mProvider.query(mUri, - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); - if (c != null && c.moveToNext()) { - return c.getString(1); - } else { - return ""; - } - } catch (Exception e) { - return null; - } finally { - if (c != null) { - c.close(); - } - } - } - - private String queryAudio(int id, String column) { + private String queryAudio(String path, String column) { Cursor c = null; try { c = mProvider.query(Audio.Media.getContentUri(mVolumeName), - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); + new String [] { column }, + PATH_WHERE, new String[] {path}, null, null); if (c != null && c.moveToNext()) { - return c.getString(1); + return c.getString(0); } else { return ""; } } catch (Exception e) { - return null; + return ""; } finally { if (c != null) { c.close(); @@ -236,21 +196,19 @@ class MtpPropertyGroup { } } - private String queryGenre(int id) { + private String queryGenre(String path) { Cursor c = null; try { - Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id); - c = mProvider.query(uri, - new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME }, - null, null, null, null); + c = mProvider.query(Audio.Genres.getContentUri(mVolumeName), + new String [] { Audio.GenresColumns.NAME }, + PATH_WHERE, new String[] {path}, null, null); if (c != null && c.moveToNext()) { - return c.getString(1); + return c.getString(0); } else { return ""; } } catch (Exception e) { - Log.e(TAG, "queryGenre exception", e); - return null; + return ""; } finally { if (c != null) { c.close(); @@ -258,211 +216,127 @@ class MtpPropertyGroup { } } - private Long queryLong(int id, String column) { - Cursor c = null; - try { - // for now we are only reading properties from the "objects" table - c = mProvider.query(mUri, - new String [] { Files.FileColumns._ID, column }, - ID_WHERE, new String[] { Integer.toString(id) }, null, null); - if (c != null && c.moveToNext()) { - return new Long(c.getLong(1)); - } - } catch (Exception e) { - } finally { - if (c != null) { - c.close(); - } - } - return null; - } - - private static String nameFromPath(String path) { - // extract name from full path - int start = 0; - int lastSlash = path.lastIndexOf('/'); - if (lastSlash >= 0) { - start = lastSlash + 1; - } - int end = path.length(); - if (end - start > 255) { - end = start + 255; - } - return path.substring(start, end); - } - - MtpPropertyList getPropertyList(int handle, int format, int depth) { - //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth); - if (depth > 1) { - // we only support depth 0 and 1 - // depth 0: single object, depth 1: immediate children - return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED); - } - - String where; - String[] whereArgs; - if (format == 0) { - if (handle == 0xFFFFFFFF) { - // select all objects - where = null; - whereArgs = null; - } else { - whereArgs = new String[] { Integer.toString(handle) }; - if (depth == 1) { - where = PARENT_WHERE; - } else { - where = ID_WHERE; - } - } - } else { - if (handle == 0xFFFFFFFF) { - // select all objects with given format - where = FORMAT_WHERE; - whereArgs = new String[] { Integer.toString(format) }; - } else { - whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) }; - if (depth == 1) { - where = PARENT_FORMAT_WHERE; - } else { - where = ID_FORMAT_WHERE; - } - } - } - + /** + * Gets the values of the properties represented by this property group for the given + * object and adds them to the given property list. + * @return Response_OK if the operation succeeded. + */ + public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) { Cursor c = null; - try { - // don't query if not necessary - if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) { - c = mProvider.query(mUri, mColumns, where, whereArgs, null, null); - if (c == null) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); + int id = object.getId(); + String path = object.getPath().toString(); + for (Property property : mProperties) { + if (property.column != -1 && c == null) { + try { + // Look up the entry in MediaProvider only if one of those properties is needed. + c = mProvider.query(mUri, mColumns, + PATH_WHERE, new String[] {path}, null, null); + if (c != null && !c.moveToNext()) { + c.close(); + c = null; + } + } catch (RemoteException e) { + Log.e(TAG, "Mediaprovider lookup failed"); } } - - int count = (c == null ? 1 : c.getCount()); - MtpPropertyList result = new MtpPropertyList(count * mProperties.length, - MtpConstants.RESPONSE_OK); - - // iterate over all objects in the query - for (int objectIndex = 0; objectIndex < count; objectIndex++) { - if (c != null) { - c.moveToNext(); - handle = (int)c.getLong(0); - } - - // iterate over all properties in the query for the given object - for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) { - Property property = mProperties[propertyIndex]; - int propertyCode = property.code; - int column = property.column; - - // handle some special cases - switch (propertyCode) { - case MtpConstants.PROPERTY_PROTECTION_STATUS: - // protection status is always 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0); - break; - case MtpConstants.PROPERTY_OBJECT_FILE_NAME: - // special case - need to extract file name from full path - String value = c.getString(column); - if (value != null) { - result.append(handle, propertyCode, nameFromPath(value)); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_NAME: - // first try title - String name = c.getString(column); - // then try name - if (name == null) { - name = queryString(handle, Audio.PlaylistsColumns.NAME); - } - // if title and name fail, extract name from full path - if (name == null) { - name = queryString(handle, Files.FileColumns.DATA); - if (name != null) { - name = nameFromPath(name); - } - } - if (name != null) { - result.append(handle, propertyCode, name); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_DATE_MODIFIED: - case MtpConstants.PROPERTY_DATE_ADDED: - // convert from seconds to DateTime - result.append(handle, propertyCode, format_date_time(c.getInt(column))); - break; - case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: - // release date is stored internally as just the year - int year = c.getInt(column); - String dateTime = Integer.toString(year) + "0101T000000"; - result.append(handle, propertyCode, dateTime); - break; - case MtpConstants.PROPERTY_PERSISTENT_UID: - // PUID is concatenation of storageID and object handle - long puid = c.getLong(column); - puid <<= 32; - puid += handle; - result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid); - break; - case MtpConstants.PROPERTY_TRACK: - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, - c.getInt(column) % 1000); - break; - case MtpConstants.PROPERTY_ARTIST: - result.append(handle, propertyCode, - queryAudio(handle, Audio.AudioColumns.ARTIST)); - break; - case MtpConstants.PROPERTY_ALBUM_NAME: - result.append(handle, propertyCode, - queryAudio(handle, Audio.AudioColumns.ALBUM)); - break; - case MtpConstants.PROPERTY_GENRE: - String genre = queryGenre(handle); - if (genre != null) { - result.append(handle, propertyCode, genre); - } else { - result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); - } - break; - case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC: - case MtpConstants.PROPERTY_AUDIO_BITRATE: - case MtpConstants.PROPERTY_SAMPLE_RATE: - // we don't have these in our database, so return 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0); + switch (property.code) { + case MtpConstants.PROPERTY_PROTECTION_STATUS: + // protection status is always 0 + list.append(id, property.code, property.type, 0); + break; + case MtpConstants.PROPERTY_NAME: + case MtpConstants.PROPERTY_OBJECT_FILE_NAME: + case MtpConstants.PROPERTY_DISPLAY_NAME: + list.append(id, property.code, object.getName()); + break; + case MtpConstants.PROPERTY_DATE_MODIFIED: + case MtpConstants.PROPERTY_DATE_ADDED: + // convert from seconds to DateTime + list.append(id, property.code, + format_date_time(object.getModifiedTime())); + break; + case MtpConstants.PROPERTY_STORAGE_ID: + list.append(id, property.code, property.type, object.getStorageId()); + break; + case MtpConstants.PROPERTY_OBJECT_FORMAT: + list.append(id, property.code, property.type, object.getFormat()); + break; + case MtpConstants.PROPERTY_OBJECT_SIZE: + list.append(id, property.code, property.type, object.getSize()); + break; + case MtpConstants.PROPERTY_PARENT_OBJECT: + list.append(id, property.code, property.type, + object.getParent().isRoot() ? 0 : object.getParent().getId()); + break; + case MtpConstants.PROPERTY_PERSISTENT_UID: + // The persistent uid must be unique and never reused among all objects, + // and remain the same between sessions. + long puid = (object.getPath().toString().hashCode() << 32) + + object.getModifiedTime(); + list.append(id, property.code, property.type, puid); + break; + case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: + // release date is stored internally as just the year + int year = 0; + if (c != null) + year = c.getInt(property.column); + String dateTime = Integer.toString(year) + "0101T000000"; + list.append(id, property.code, dateTime); + break; + case MtpConstants.PROPERTY_TRACK: + int track = 0; + if (c != null) + track = c.getInt(property.column); + list.append(id, property.code, MtpConstants.TYPE_UINT16, + track % 1000); + break; + case MtpConstants.PROPERTY_ARTIST: + list.append(id, property.code, + queryAudio(path, Audio.AudioColumns.ARTIST)); + break; + case MtpConstants.PROPERTY_ALBUM_NAME: + list.append(id, property.code, + queryAudio(path, Audio.AudioColumns.ALBUM)); + break; + case MtpConstants.PROPERTY_GENRE: + String genre = queryGenre(path); + if (genre != null) { + list.append(id, property.code, genre); + } + break; + case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC: + case MtpConstants.PROPERTY_AUDIO_BITRATE: + case MtpConstants.PROPERTY_SAMPLE_RATE: + // we don't have these in our database, so return 0 + list.append(id, property.code, MtpConstants.TYPE_UINT32, 0); + break; + case MtpConstants.PROPERTY_BITRATE_TYPE: + case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS: + // we don't have these in our database, so return 0 + list.append(id, property.code, MtpConstants.TYPE_UINT16, 0); + break; + default: + switch(property.type) { + case MtpConstants.TYPE_UNDEFINED: + list.append(id, property.code, property.type, 0); break; - case MtpConstants.PROPERTY_BITRATE_TYPE: - case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS: - // we don't have these in our database, so return 0 - result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0); + case MtpConstants.TYPE_STR: + String value = ""; + if (c != null) + value = c.getString(property.column); + list.append(id, property.code, value); break; default: - if (property.type == MtpConstants.TYPE_STR) { - result.append(handle, propertyCode, c.getString(column)); - } else if (property.type == MtpConstants.TYPE_UNDEFINED) { - result.append(handle, propertyCode, property.type, 0); - } else { - result.append(handle, propertyCode, property.type, - c.getLong(column)); - } - break; + long longValue = 0L; + if (c != null) + longValue = c.getLong(property.column); + list.append(id, property.code, property.type, longValue); } - } - } - - return result; - } catch (RemoteException e) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR); - } finally { - if (c != null) { - c.close(); } } - // impossible to get here, so no return statement + if (c != null) + c.close(); + return MtpConstants.RESPONSE_OK; } private native String format_date_time(long seconds); diff --git a/android/mtp/MtpPropertyList.java b/android/mtp/MtpPropertyList.java index f9bc603e..ede90dac 100644 --- a/android/mtp/MtpPropertyList.java +++ b/android/mtp/MtpPropertyList.java @@ -16,6 +16,9 @@ package android.mtp; +import java.util.ArrayList; +import java.util.List; + /** * Encapsulates the ObjectPropList dataset used by the GetObjectPropList command. * The fields of this class are read by JNI code in android_media_MtpDatabase.cpp @@ -23,56 +26,70 @@ package android.mtp; class MtpPropertyList { - // number of results returned - private int mCount; - // maximum number of results - private final int mMaxCount; - // result code for GetObjectPropList - public int mResult; // list of object handles (first field in quadruplet) - public final int[] mObjectHandles; - // list of object propery codes (second field in quadruplet) - public final int[] mPropertyCodes; + private List mObjectHandles; + // list of object property codes (second field in quadruplet) + private List mPropertyCodes; // list of data type codes (third field in quadruplet) - public final int[] mDataTypes; + private List mDataTypes; // list of long int property values (fourth field in quadruplet, when value is integer type) - public long[] mLongValues; + private List mLongValues; // list of long int property values (fourth field in quadruplet, when value is string type) - public String[] mStringValues; - - // constructor only called from MtpDatabase - public MtpPropertyList(int maxCount, int result) { - mMaxCount = maxCount; - mResult = result; - mObjectHandles = new int[maxCount]; - mPropertyCodes = new int[maxCount]; - mDataTypes = new int[maxCount]; - // mLongValues and mStringValues are created lazily since both might not be necessary + private List mStringValues; + + // Return value of this operation + private int mCode; + + public MtpPropertyList(int code) { + mCode = code; + mObjectHandles = new ArrayList<>(); + mPropertyCodes = new ArrayList<>(); + mDataTypes = new ArrayList<>(); + mLongValues = new ArrayList<>(); + mStringValues = new ArrayList<>(); } public void append(int handle, int property, int type, long value) { - int index = mCount++; - if (mLongValues == null) { - mLongValues = new long[mMaxCount]; - } - mObjectHandles[index] = handle; - mPropertyCodes[index] = property; - mDataTypes[index] = type; - mLongValues[index] = value; + mObjectHandles.add(handle); + mPropertyCodes.add(property); + mDataTypes.add(type); + mLongValues.add(value); + mStringValues.add(null); } public void append(int handle, int property, String value) { - int index = mCount++; - if (mStringValues == null) { - mStringValues = new String[mMaxCount]; - } - mObjectHandles[index] = handle; - mPropertyCodes[index] = property; - mDataTypes[index] = MtpConstants.TYPE_STR; - mStringValues[index] = value; + mObjectHandles.add(handle); + mPropertyCodes.add(property); + mDataTypes.add(MtpConstants.TYPE_STR); + mStringValues.add(value); + mLongValues.add(0L); + } + + public int getCode() { + return mCode; + } + + public int getCount() { + return mObjectHandles.size(); + } + + public int[] getObjectHandles() { + return mObjectHandles.stream().mapToInt(Integer::intValue).toArray(); + } + + public int[] getPropertyCodes() { + return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray(); + } + + public int[] getDataTypes() { + return mDataTypes.stream().mapToInt(Integer::intValue).toArray(); + } + + public long[] getLongValues() { + return mLongValues.stream().mapToLong(Long::longValue).toArray(); } - public void setResult(int result) { - mResult = result; + public String[] getStringValues() { + return mStringValues.toArray(new String[0]); } } diff --git a/android/mtp/MtpStorage.java b/android/mtp/MtpStorage.java index 6ca442c7..c72b827d 100644 --- a/android/mtp/MtpStorage.java +++ b/android/mtp/MtpStorage.java @@ -31,15 +31,13 @@ public class MtpStorage { private final int mStorageId; private final String mPath; private final String mDescription; - private final long mReserveSpace; private final boolean mRemovable; private final long mMaxFileSize; - public MtpStorage(StorageVolume volume, Context context) { - mStorageId = volume.getStorageId(); + public MtpStorage(StorageVolume volume, int storageId) { + mStorageId = storageId; mPath = volume.getPath(); - mDescription = volume.getDescription(context); - mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L; + mDescription = volume.getDescription(null); mRemovable = volume.isRemovable(); mMaxFileSize = volume.getMaxFileSize(); } @@ -71,16 +69,6 @@ public class MtpStorage { return mDescription; } - /** - * Returns the amount of space to reserve on the storage file system. - * This can be set to a non-zero value to prevent MTP from filling up the entire storage. - * - * @return reserved space in bytes. - */ - public final long getReserveSpace() { - return mReserveSpace; - } - /** * Returns true if the storage is removable. * diff --git a/android/mtp/MtpStorageManager.java b/android/mtp/MtpStorageManager.java new file mode 100644 index 00000000..bdc87413 --- /dev/null +++ b/android/mtp/MtpStorageManager.java @@ -0,0 +1,1210 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.mtp; + +import android.media.MediaFile; +import android.os.FileObserver; +import android.os.storage.StorageVolume; +import android.util.Log; + +import java.io.IOException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +/** + * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of + * filesystem changes. As directories are listed, this class will cache the results, + * and send events when objects are added/removed from cached directories. + * {@hide} + */ +public class MtpStorageManager { + private static final String TAG = MtpStorageManager.class.getSimpleName(); + public static boolean sDebug = false; + + // Inotify flags not provided by FileObserver + private static final int IN_ONLYDIR = 0x01000000; + private static final int IN_Q_OVERFLOW = 0x00004000; + private static final int IN_IGNORED = 0x00008000; + private static final int IN_ISDIR = 0x40000000; + + private class MtpObjectObserver extends FileObserver { + MtpObject mObject; + + MtpObjectObserver(MtpObject object) { + super(object.getPath().toString(), + MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR); + mObject = object; + } + + @Override + public void onEvent(int event, String path) { + synchronized (MtpStorageManager.this) { + if ((event & IN_Q_OVERFLOW) != 0) { + // We are out of space in the inotify queue. + Log.e(TAG, "Received Inotify overflow event!"); + } + MtpObject obj = mObject.getChild(path); + if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) { + if (sDebug) + Log.i(TAG, "Got inotify added event for " + path + " " + event); + handleAddedObject(mObject, path, (event & IN_ISDIR) != 0); + } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) { + if (obj == null) { + Log.w(TAG, "Object was null in event " + path); + return; + } + if (sDebug) + Log.i(TAG, "Got inotify removed event for " + path + " " + event); + handleRemovedObject(obj); + } else if ((event & IN_IGNORED) != 0) { + if (sDebug) + Log.i(TAG, "inotify for " + mObject.getPath() + " deleted"); + if (mObject.mObserver != null) + mObject.mObserver.stopWatching(); + mObject.mObserver = null; + } else { + Log.w(TAG, "Got unrecognized event " + path + " " + event); + } + } + } + + @Override + public void finalize() { + // If the server shuts down and starts up again, the new server's observers can be + // invalidated by the finalize() calls of the previous server's observers. + // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and + // always call stopWatching() manually whenever an observer should be shut down. + } + } + + /** + * Describes how the object is being acted on, to determine how events are handled. + */ + private enum MtpObjectState { + NORMAL, + FROZEN, // Object is going to be modified in this session. + FROZEN_ADDED, // Object was frozen, and has been added. + FROZEN_REMOVED, // Object was frozen, and has been removed. + FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen. + FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed. + } + + /** + * Describes the current operation being done on an object. Determines whether observers are + * created on new folders. + */ + private enum MtpOperation { + NONE, // Any new folders not added as part of the session are immediately observed. + ADD, // New folders added as part of the session are immediately observed. + RENAME, // Renamed or moved folders are not immediately observed. + COPY, // Copied folders are immediately observed iff the original was. + DELETE, // Exists for debugging purposes only. + } + + /** MtpObject represents either a file or directory in an associated storage. **/ + public static class MtpObject { + // null for root objects + private MtpObject mParent; + + private String mName; + private int mId; + private MtpObjectState mState; + private MtpOperation mOp; + + private boolean mVisited; + private boolean mIsDir; + + // null if not a directory + private HashMap mChildren; + // null if not both a directory and visited + private FileObserver mObserver; + + MtpObject(String name, int id, MtpObject parent, boolean isDir) { + mId = id; + mName = name; + mParent = parent; + mObserver = null; + mVisited = false; + mState = MtpObjectState.NORMAL; + mIsDir = isDir; + mOp = MtpOperation.NONE; + + mChildren = mIsDir ? new HashMap<>() : null; + } + + /** Public methods for getting object info **/ + + public String getName() { + return mName; + } + + public int getId() { + return mId; + } + + public boolean isDir() { + return mIsDir; + } + + public int getFormat() { + return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null); + } + + public int getStorageId() { + return getRoot().getId(); + } + + public long getModifiedTime() { + return getPath().toFile().lastModified() / 1000; + } + + public MtpObject getParent() { + return mParent; + } + + public MtpObject getRoot() { + return isRoot() ? this : mParent.getRoot(); + } + + public long getSize() { + return mIsDir ? 0 : getPath().toFile().length(); + } + + public Path getPath() { + return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName); + } + + public boolean isRoot() { + return mParent == null; + } + + /** For MtpStorageManager only **/ + + private void setName(String name) { + mName = name; + } + + private void setId(int id) { + mId = id; + } + + private boolean isVisited() { + return mVisited; + } + + private void setParent(MtpObject parent) { + mParent = parent; + } + + private void setDir(boolean dir) { + if (dir != mIsDir) { + mIsDir = dir; + mChildren = mIsDir ? new HashMap<>() : null; + } + } + + private void setVisited(boolean visited) { + mVisited = visited; + } + + private MtpObjectState getState() { + return mState; + } + + private void setState(MtpObjectState state) { + mState = state; + if (mState == MtpObjectState.NORMAL) + mOp = MtpOperation.NONE; + } + + private MtpOperation getOperation() { + return mOp; + } + + private void setOperation(MtpOperation op) { + mOp = op; + } + + private FileObserver getObserver() { + return mObserver; + } + + private void setObserver(FileObserver observer) { + mObserver = observer; + } + + private void addChild(MtpObject child) { + mChildren.put(child.getName(), child); + } + + private MtpObject getChild(String name) { + return mChildren.get(name); + } + + private Collection getChildren() { + return mChildren.values(); + } + + private boolean exists() { + return getPath().toFile().exists(); + } + + private MtpObject copy(boolean recursive) { + MtpObject copy = new MtpObject(mName, mId, mParent, mIsDir); + copy.mIsDir = mIsDir; + copy.mVisited = mVisited; + copy.mState = mState; + copy.mChildren = mIsDir ? new HashMap<>() : null; + if (recursive && mIsDir) { + for (MtpObject child : mChildren.values()) { + MtpObject childCopy = child.copy(true); + childCopy.setParent(copy); + copy.addChild(childCopy); + } + } + return copy; + } + } + + /** + * A class that processes generated filesystem events. + */ + public static abstract class MtpNotifier { + /** + * Called when an object is added. + */ + public abstract void sendObjectAdded(int id); + + /** + * Called when an object is deleted. + */ + public abstract void sendObjectRemoved(int id); + } + + private MtpNotifier mMtpNotifier; + + // A cache of MtpObjects. The objects in the cache are keyed by object id. + // The root object of each storage isn't in this map since they all have ObjectId 0. + // Instead, they can be found in mRoots keyed by storageId. + private HashMap mObjects; + + // A cache of the root MtpObject for each storage, keyed by storage id. + private HashMap mRoots; + + // Object and Storage ids are allocated incrementally and not to be reused. + private int mNextObjectId; + private int mNextStorageId; + + // Special subdirectories. When set, only return objects rooted in these directories, and do + // not allow them to be modified. + private Set mSubdirectories; + + private volatile boolean mCheckConsistency; + private Thread mConsistencyThread; + + public MtpStorageManager(MtpNotifier notifier, Set subdirectories) { + mMtpNotifier = notifier; + mSubdirectories = subdirectories; + mObjects = new HashMap<>(); + mRoots = new HashMap<>(); + mNextObjectId = 1; + mNextStorageId = 1; + + mCheckConsistency = false; // Set to true to turn on automatic consistency checking + mConsistencyThread = new Thread(() -> { + while (mCheckConsistency) { + try { + Thread.sleep(15 * 1000); + } catch (InterruptedException e) { + return; + } + if (MtpStorageManager.this.checkConsistency()) { + Log.v(TAG, "Cache is consistent"); + } else { + Log.w(TAG, "Cache is not consistent"); + } + } + }); + if (mCheckConsistency) + mConsistencyThread.start(); + } + + /** + * Clean up resources used by the storage manager. + */ + public synchronized void close() { + Stream objs = Stream.concat(mRoots.values().stream(), + mObjects.values().stream()); + + Iterator iter = objs.iterator(); + while (iter.hasNext()) { + // Close all FileObservers. + MtpObject obj = iter.next(); + if (obj.getObserver() != null) { + obj.getObserver().stopWatching(); + obj.setObserver(null); + } + } + + // Shut down the consistency checking thread + if (mCheckConsistency) { + mCheckConsistency = false; + mConsistencyThread.interrupt(); + try { + mConsistencyThread.join(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Sets the special subdirectories, which are the subdirectories of root storage that queries + * are restricted to. Must be done before any root storages are accessed. + * @param subDirs Subdirectories to set, or null to reset. + */ + public synchronized void setSubdirectories(Set subDirs) { + mSubdirectories = subDirs; + } + + /** + * Allocates an MTP storage id for the given volume and add it to current roots. + * @param volume Storage to add. + * @return the associated MtpStorage + */ + public synchronized MtpStorage addMtpStorage(StorageVolume volume) { + int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1; + MtpObject root = new MtpObject(volume.getPath(), storageId, null, true); + MtpStorage storage = new MtpStorage(volume, storageId); + mRoots.put(storageId, root); + return storage; + } + + /** + * Removes the given storage and all associated items from the cache. + * @param storage Storage to remove. + */ + public synchronized void removeMtpStorage(MtpStorage storage) { + removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true); + } + + /** + * Checks if the given object can be renamed, moved, or deleted. + * If there are special subdirectories, they cannot be modified. + * @param obj Object to check. + * @return Whether object can be modified. + */ + private synchronized boolean isSpecialSubDir(MtpObject obj) { + return obj.getParent().isRoot() && mSubdirectories != null + && !mSubdirectories.contains(obj.getName()); + } + + /** + * Get the object with the specified path. Visit any necessary directories on the way. + * @param path Full path of the object to find. + * @return The desired object, or null if it cannot be found. + */ + public synchronized MtpObject getByPath(String path) { + MtpObject obj = null; + for (MtpObject root : mRoots.values()) { + if (path.startsWith(root.getName())) { + obj = root; + path = path.substring(root.getName().length()); + } + } + for (String name : path.split("/")) { + if (obj == null || !obj.isDir()) + return null; + if ("".equals(name)) + continue; + if (!obj.isVisited()) + getChildren(obj); + obj = obj.getChild(name); + } + return obj; + } + + /** + * Get the object with specified id. + * @param id Id of object. must not be 0 or 0xFFFFFFFF + * @return Object, or null if error. + */ + public synchronized MtpObject getObject(int id) { + if (id == 0 || id == 0xFFFFFFFF) { + Log.w(TAG, "Can't get root storages with getObject()"); + return null; + } + if (!mObjects.containsKey(id)) { + Log.w(TAG, "Id " + id + " doesn't exist"); + return null; + } + return mObjects.get(id); + } + + /** + * Get the storage with specified id. + * @param id Storage id. + * @return Object that is the root of the storage, or null if error. + */ + public MtpObject getStorageRoot(int id) { + if (!mRoots.containsKey(id)) { + Log.w(TAG, "StorageId " + id + " doesn't exist"); + return null; + } + return mRoots.get(id); + } + + private int getNextObjectId() { + int ret = mNextObjectId; + // Treat the id as unsigned int + mNextObjectId = (int) ((long) mNextObjectId + 1); + return ret; + } + + private int getNextStorageId() { + return mNextStorageId++; + } + + /** + * Get all objects matching the given parent, format, and storage + * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root + * @param format format of returned objects. 0 for any format + * @param storageId storage id to look in. 0xFFFFFFFF for all storages + * @return A stream of matched objects, or null if error + */ + public synchronized Stream getObjects(int parent, int format, int storageId) { + boolean recursive = parent == 0; + if (parent == 0xFFFFFFFF) + parent = 0; + if (storageId == 0xFFFFFFFF) { + // query all stores + if (parent == 0) { + // Get the objects of this format and parent in each store. + ArrayList> streamList = new ArrayList<>(); + for (MtpObject root : mRoots.values()) { + streamList.add(getObjects(root, format, recursive)); + } + return Stream.of(streamList).flatMap(Collection::stream).reduce(Stream::concat) + .orElseGet(Stream::empty); + } + } + MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent); + if (obj == null) + return null; + return getObjects(obj, format, recursive); + } + + private synchronized Stream getObjects(MtpObject parent, int format, boolean rec) { + Collection children = getChildren(parent); + if (children == null) + return null; + Stream ret = Stream.of(children).flatMap(Collection::stream); + + if (format != 0) { + ret = ret.filter(o -> o.getFormat() == format); + } + if (rec) { + // Get all objects recursively. + ArrayList> streamList = new ArrayList<>(); + streamList.add(ret); + for (MtpObject o : children) { + if (o.isDir()) + streamList.add(getObjects(o, format, true)); + } + ret = Stream.of(streamList).filter(Objects::nonNull).flatMap(Collection::stream) + .reduce(Stream::concat).orElseGet(Stream::empty); + } + return ret; + } + + /** + * Return the children of the given object. If the object hasn't been visited yet, add + * its children to the cache and start observing it. + * @param object the parent object + * @return The collection of child objects or null if error + */ + private synchronized Collection getChildren(MtpObject object) { + if (object == null || !object.isDir()) { + Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId())); + return null; + } + if (!object.isVisited()) { + Path dir = object.getPath(); + /* + * If a file is added after the observer starts watching the directory, but before + * the contents are listed, it will generate an event that will get processed + * after this synchronized function returns. We handle this by ignoring object + * added events if an object at that path already exists. + */ + if (object.getObserver() != null) + Log.e(TAG, "Observer is not null!"); + object.setObserver(new MtpObjectObserver(object)); + object.getObserver().startWatching(); + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + for (Path file : stream) { + addObjectToCache(object, file.getFileName().toString(), + file.toFile().isDirectory()); + } + } catch (IOException | DirectoryIteratorException e) { + Log.e(TAG, e.toString()); + object.getObserver().stopWatching(); + object.setObserver(null); + return null; + } + object.setVisited(true); + } + return object.getChildren(); + } + + /** + * Create a new object from the given path and add it to the cache. + * @param parent The parent object + * @param newName Path of the new object + * @return the new object if success, else null + */ + private synchronized MtpObject addObjectToCache(MtpObject parent, String newName, + boolean isDir) { + if (!parent.isRoot() && getObject(parent.getId()) != parent) + // parent object has been removed + return null; + if (parent.getChild(newName) != null) { + // Object already exists + return null; + } + if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) { + // Not one of the restricted subdirectories. + return null; + } + + MtpObject obj = new MtpObject(newName, getNextObjectId(), parent, isDir); + mObjects.put(obj.getId(), obj); + parent.addChild(obj); + return obj; + } + + /** + * Remove the given path from the cache. + * @param removed The removed object + * @param removeGlobal Whether to remove the object from the global id map + * @param recursive Whether to also remove its children recursively. + * @return true if successfully removed + */ + private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal, + boolean recursive) { + boolean ret = removed.isRoot() + || removed.getParent().mChildren.remove(removed.getName(), removed); + if (!ret && sDebug) + Log.w(TAG, "Failed to remove from parent " + removed.getPath()); + if (removed.isRoot()) { + ret = mRoots.remove(removed.getId(), removed) && ret; + } else if (removeGlobal) { + ret = mObjects.remove(removed.getId(), removed) && ret; + } + if (!ret && sDebug) + Log.w(TAG, "Failed to remove from global cache " + removed.getPath()); + if (removed.getObserver() != null) { + removed.getObserver().stopWatching(); + removed.setObserver(null); + } + if (removed.isDir() && recursive) { + // Remove all descendants from cache recursively + Collection children = new ArrayList<>(removed.getChildren()); + for (MtpObject child : children) { + ret = removeObjectFromCache(child, removeGlobal, true) && ret; + } + } + return ret; + } + + private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) { + MtpOperation op = MtpOperation.NONE; + MtpObject obj = parent.getChild(path); + if (obj != null) { + MtpObjectState state = obj.getState(); + op = obj.getOperation(); + if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED) + Log.d(TAG, "Inconsistent directory info! " + obj.getPath()); + obj.setDir(isDir); + switch (state) { + case FROZEN: + case FROZEN_REMOVED: + obj.setState(MtpObjectState.FROZEN_ADDED); + break; + case FROZEN_ONESHOT_ADD: + obj.setState(MtpObjectState.NORMAL); + break; + case NORMAL: + case FROZEN_ADDED: + // This can happen when handling listed object in a new directory. + return; + default: + Log.w(TAG, "Unexpected state in add " + path + " " + state); + } + if (sDebug) + Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); + } else { + obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir); + if (obj != null) { + MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId()); + } else { + if (sDebug) + Log.w(TAG, "object " + path + " already exists"); + return; + } + } + if (isDir) { + // If this was added as part of a rename do not visit or send events. + if (op == MtpOperation.RENAME) + return; + + // If it was part of a copy operation, then only add observer if it was visited before. + if (op == MtpOperation.COPY && !obj.isVisited()) + return; + + if (obj.getObserver() != null) { + Log.e(TAG, "Observer is not null!"); + return; + } + obj.setObserver(new MtpObjectObserver(obj)); + obj.getObserver().startWatching(); + obj.setVisited(true); + + // It's possible that objects were added to a watched directory before the watch can be + // created, so manually handle those. + try (DirectoryStream stream = Files.newDirectoryStream(obj.getPath())) { + for (Path file : stream) { + if (sDebug) + Log.i(TAG, "Manually handling event for " + file.getFileName().toString()); + handleAddedObject(obj, file.getFileName().toString(), + file.toFile().isDirectory()); + } + } catch (IOException | DirectoryIteratorException e) { + Log.e(TAG, e.toString()); + obj.getObserver().stopWatching(); + obj.setObserver(null); + } + } + } + + private synchronized void handleRemovedObject(MtpObject obj) { + MtpObjectState state = obj.getState(); + MtpOperation op = obj.getOperation(); + switch (state) { + case FROZEN_ADDED: + obj.setState(MtpObjectState.FROZEN_REMOVED); + break; + case FROZEN_ONESHOT_DEL: + removeObjectFromCache(obj, op != MtpOperation.RENAME, false); + break; + case FROZEN: + obj.setState(MtpObjectState.FROZEN_REMOVED); + break; + case NORMAL: + if (MtpStorageManager.this.removeObjectFromCache(obj, true, true)) + MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId()); + break; + default: + // This shouldn't happen; states correspond to objects that don't exist + Log.e(TAG, "Got unexpected object remove for " + obj.getName()); + } + if (sDebug) + Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); + } + + /** + * Block the caller until all events currently in the event queue have been + * read and processed. Used for testing purposes. + */ + public void flushEvents() { + try { + // TODO make this smarter + Thread.sleep(500); + } catch (InterruptedException e) { + + } + } + + /** + * Dumps a representation of the cache to log. + */ + public synchronized void dump() { + for (int key : mObjects.keySet()) { + MtpObject obj = mObjects.get(key); + Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null") + + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj") + + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState()); + } + } + + /** + * Checks consistency of the cache. This checks whether all objects have correct links + * to their parent, and whether directories are missing or have extraneous objects. + * @return true iff cache is consistent + */ + public synchronized boolean checkConsistency() { + Stream objs = Stream.concat(mRoots.values().stream(), + mObjects.values().stream()); + Iterator iter = objs.iterator(); + boolean ret = true; + while (iter.hasNext()) { + MtpObject obj = iter.next(); + if (!obj.exists()) { + Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId()); + ret = false; + } + if (obj.getState() != MtpObjectState.NORMAL) { + Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState()); + ret = false; + } + if (obj.getOperation() != MtpOperation.NONE) { + Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation()); + ret = false; + } + if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) { + Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly"); + ret = false; + } + if (obj.getParent() != null) { + if (obj.getParent().isRoot() && obj.getParent() + != mRoots.get(obj.getParent().getId())) { + Log.w(TAG, "Root parent is not in root mapping " + obj.getPath()); + ret = false; + } + if (!obj.getParent().isRoot() && obj.getParent() + != mObjects.get(obj.getParent().getId())) { + Log.w(TAG, "Parent is not in object mapping " + obj.getPath()); + ret = false; + } + if (obj.getParent().getChild(obj.getName()) != obj) { + Log.w(TAG, "Child does not exist in parent " + obj.getPath()); + ret = false; + } + } + if (obj.isDir()) { + if (obj.isVisited() == (obj.getObserver() == null)) { + Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ") + + " visited but observer is " + obj.getObserver()); + ret = false; + } + if (!obj.isVisited() && obj.getChildren().size() > 0) { + Log.w(TAG, obj.getPath() + " is not visited but has children"); + ret = false; + } + try (DirectoryStream stream = Files.newDirectoryStream(obj.getPath())) { + Set files = new HashSet<>(); + for (Path file : stream) { + if (obj.isVisited() && + obj.getChild(file.getFileName().toString()) == null && + (mSubdirectories == null || !obj.isRoot() || + mSubdirectories.contains(file.getFileName().toString()))) { + Log.w(TAG, "File exists in fs but not in children " + file); + ret = false; + } + files.add(file.toString()); + } + for (MtpObject child : obj.getChildren()) { + if (!files.contains(child.getPath().toString())) { + Log.w(TAG, "File in children doesn't exist in fs " + child.getPath()); + ret = false; + } + if (child != mObjects.get(child.getId())) { + Log.w(TAG, "Child is not in object map " + child.getPath()); + ret = false; + } + } + } catch (IOException | DirectoryIteratorException e) { + Log.w(TAG, e.toString()); + ret = false; + } + } + } + return ret; + } + + /** + * Informs MtpStorageManager that an object with the given path is about to be added. + * @param parent The parent object of the object to be added. + * @param name Filename of object to add. + * @return Object id of the added object, or -1 if it cannot be added. + */ + public synchronized int beginSendObject(MtpObject parent, String name, int format) { + if (sDebug) + Log.v(TAG, "beginSendObject " + name); + if (!parent.isDir()) + return -1; + if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) + return -1; + getChildren(parent); // Ensure parent is visited + MtpObject obj = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION); + if (obj == null) + return -1; + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(MtpOperation.ADD); + return obj.getId(); + } + + /** + * Clean up the object state after a sendObject operation. + * @param obj The object, returned from beginAddObject(). + * @param succeeded Whether the file was successfully created. + * @return Whether cache state was successfully cleaned up. + */ + public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) { + if (sDebug) + Log.v(TAG, "endSendObject " + succeeded); + return generalEndAddObject(obj, succeeded, true); + } + + /** + * Informs MtpStorageManager that the given object is about to be renamed. + * If this returns true, it must be followed with an endRenameObject() + * @param obj Object to be renamed. + * @param newName New name of the object. + * @return Whether renaming is allowed. + */ + public synchronized boolean beginRenameObject(MtpObject obj, String newName) { + if (sDebug) + Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName); + if (obj.isRoot()) + return false; + if (isSpecialSubDir(obj)) + return false; + if (obj.getParent().getChild(newName) != null) + // Object already exists in parent with that name. + return false; + + MtpObject oldObj = obj.copy(false); + obj.setName(newName); + obj.getParent().addChild(obj); + oldObj.getParent().addChild(oldObj); + return generalBeginRenameObject(oldObj, obj); + } + + /** + * Cleans up cache state after a rename operation and sends any events that were missed. + * @param obj The object being renamed, the same one that was passed in beginRenameObject(). + * @param oldName The previous name of the object. + * @param success Whether the rename operation succeeded. + * @return Whether state was successfully cleaned up. + */ + public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) { + if (sDebug) + Log.v(TAG, "endRenameObject " + success); + MtpObject parent = obj.getParent(); + MtpObject oldObj = parent.getChild(oldName); + if (!success) { + // If the rename failed, we want oldObj to be the original and obj to be the dummy. + // Switch the objects, except for their name and state. + MtpObject temp = oldObj; + MtpObjectState oldState = oldObj.getState(); + temp.setName(obj.getName()); + temp.setState(obj.getState()); + oldObj = obj; + oldObj.setName(oldName); + oldObj.setState(oldState); + obj = temp; + parent.addChild(obj); + parent.addChild(oldObj); + } + return generalEndRenameObject(oldObj, obj, success); + } + + /** + * Informs MtpStorageManager that the given object is about to be deleted by the initiator, + * so don't send an event. + * @param obj Object to be deleted. + * @return Whether cache deletion is allowed. + */ + public synchronized boolean beginRemoveObject(MtpObject obj) { + if (sDebug) + Log.v(TAG, "beginRemoveObject " + obj.getName()); + return !obj.isRoot() && !isSpecialSubDir(obj) + && generalBeginRemoveObject(obj, MtpOperation.DELETE); + } + + /** + * Clean up cache state after a delete operation and send any events that were missed. + * @param obj Object to be deleted, same one passed in beginRemoveObject(). + * @param success Whether operation was completed successfully. + * @return Whether cache state is correct. + */ + public synchronized boolean endRemoveObject(MtpObject obj, boolean success) { + if (sDebug) + Log.v(TAG, "endRemoveObject " + success); + boolean ret = true; + if (obj.isDir()) { + for (MtpObject child : new ArrayList<>(obj.getChildren())) + if (child.getOperation() == MtpOperation.DELETE) + ret = endRemoveObject(child, success) && ret; + } + return generalEndRemoveObject(obj, success, true) && ret; + } + + /** + * Informs MtpStorageManager that the given object is about to be moved to a new parent. + * @param obj Object to be moved. + * @param newParent The new parent object. + * @return Whether the move is allowed. + */ + public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) { + if (sDebug) + Log.v(TAG, "beginMoveObject " + newParent.getPath()); + if (obj.isRoot()) + return false; + if (isSpecialSubDir(obj)) + return false; + getChildren(newParent); // Ensure parent is visited + if (newParent.getChild(obj.getName()) != null) + // Object already exists in parent with that name. + return false; + if (obj.getStorageId() != newParent.getStorageId()) { + /* + * The move is occurring across storages. The observers will not remain functional + * after the move, and the move will not be atomic. We have to copy the file tree + * to the destination and recreate the observers once copy is complete. + */ + MtpObject newObj = obj.copy(true); + newObj.setParent(newParent); + newParent.addChild(newObj); + return generalBeginRemoveObject(obj, MtpOperation.RENAME) + && generalBeginCopyObject(newObj, false); + } + // Move obj to new parent, create a dummy object in the old parent. + MtpObject oldObj = obj.copy(false); + obj.setParent(newParent); + oldObj.getParent().addChild(oldObj); + obj.getParent().addChild(obj); + return generalBeginRenameObject(oldObj, obj); + } + + /** + * Clean up cache state after a move operation and send any events that were missed. + * @param oldParent The old parent object. + * @param newParent The new parent object. + * @param name The name of the object being moved. + * @param success Whether operation was completed successfully. + * @return Whether cache state is correct. + */ + public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name, + boolean success) { + if (sDebug) + Log.v(TAG, "endMoveObject " + success); + MtpObject oldObj = oldParent.getChild(name); + MtpObject newObj = newParent.getChild(name); + if (oldObj == null || newObj == null) + return false; + if (oldParent.getStorageId() != newObj.getStorageId()) { + boolean ret = endRemoveObject(oldObj, success); + return generalEndCopyObject(newObj, success, true) && ret; + } + if (!success) { + // If the rename failed, we want oldObj to be the original and obj to be the dummy. + // Switch the objects, except for their parent and state. + MtpObject temp = oldObj; + MtpObjectState oldState = oldObj.getState(); + temp.setParent(newObj.getParent()); + temp.setState(newObj.getState()); + oldObj = newObj; + oldObj.setParent(oldParent); + oldObj.setState(oldState); + newObj = temp; + newObj.getParent().addChild(newObj); + oldParent.addChild(oldObj); + } + return generalEndRenameObject(oldObj, newObj, success); + } + + /** + * Informs MtpStorageManager that the given object is about to be copied recursively. + * @param object Object to be copied + * @param newParent New parent for the object. + * @return The object id for the new copy, or -1 if error. + */ + public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) { + if (sDebug) + Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath()); + String name = object.getName(); + if (!newParent.isDir()) + return -1; + if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) + return -1; + getChildren(newParent); // Ensure parent is visited + if (newParent.getChild(name) != null) + return -1; + MtpObject newObj = object.copy(object.isDir()); + newParent.addChild(newObj); + newObj.setParent(newParent); + if (!generalBeginCopyObject(newObj, true)) + return -1; + return newObj.getId(); + } + + /** + * Cleans up cache state after a copy operation. + * @param object Object that was copied. + * @param success Whether the operation was successful. + * @return Whether cache state is consistent. + */ + public synchronized boolean endCopyObject(MtpObject object, boolean success) { + if (sDebug) + Log.v(TAG, "endCopyObject " + object.getName() + " " + success); + return generalEndCopyObject(object, success, false); + } + + private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded, + boolean removeGlobal) { + switch (obj.getState()) { + case FROZEN: + // Object was never created. + if (succeeded) { + // The operation was successful so the event must still be in the queue. + obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD); + } else { + // The operation failed and never created the file. + if (!removeObjectFromCache(obj, removeGlobal, false)) { + return false; + } + } + break; + case FROZEN_ADDED: + obj.setState(MtpObjectState.NORMAL); + if (!succeeded) { + MtpObject parent = obj.getParent(); + // The operation failed but some other process created the file. Send an event. + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + handleAddedObject(parent, obj.getName(), obj.isDir()); + } + // else: The operation successfully created the object. + break; + case FROZEN_REMOVED: + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + if (succeeded) { + // Some other process deleted the object. Send an event. + mMtpNotifier.sendObjectRemoved(obj.getId()); + } + // else: Mtp deleted the object as part of cleanup. Don't send an event. + break; + default: + return false; + } + return true; + } + + private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success, + boolean removeGlobal) { + switch (obj.getState()) { + case FROZEN: + if (success) { + // Object was deleted successfully, and event is still in the queue. + obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL); + } else { + // Object was not deleted. + obj.setState(MtpObjectState.NORMAL); + } + break; + case FROZEN_ADDED: + // Object was deleted, and then readded. + obj.setState(MtpObjectState.NORMAL); + if (success) { + // Some other process readded the object. + MtpObject parent = obj.getParent(); + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + handleAddedObject(parent, obj.getName(), obj.isDir()); + } + // else : Object still exists after failure. + break; + case FROZEN_REMOVED: + if (!removeObjectFromCache(obj, removeGlobal, false)) + return false; + if (!success) { + // Some other process deleted the object. + mMtpNotifier.sendObjectRemoved(obj.getId()); + } + // else : This process deleted the object as part of the operation. + break; + default: + return false; + } + return true; + } + + private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) { + fromObj.setState(MtpObjectState.FROZEN); + toObj.setState(MtpObjectState.FROZEN); + fromObj.setOperation(MtpOperation.RENAME); + toObj.setOperation(MtpOperation.RENAME); + return true; + } + + private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj, + boolean success) { + boolean ret = generalEndRemoveObject(fromObj, success, !success); + return generalEndAddObject(toObj, success, success) && ret; + } + + private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) { + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(op); + if (obj.isDir()) { + for (MtpObject child : obj.getChildren()) + generalBeginRemoveObject(child, op); + } + return true; + } + + private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) { + obj.setState(MtpObjectState.FROZEN); + obj.setOperation(MtpOperation.COPY); + if (newId) { + obj.setId(getNextObjectId()); + mObjects.put(obj.getId(), obj); + } + if (obj.isDir()) + for (MtpObject child : obj.getChildren()) + if (!generalBeginCopyObject(child, newId)) + return false; + return true; + } + + private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) { + if (success && addGlobal) + mObjects.put(obj.getId(), obj); + boolean ret = true; + if (obj.isDir()) { + for (MtpObject child : new ArrayList<>(obj.getChildren())) { + if (child.getOperation() == MtpOperation.COPY) + ret = generalEndCopyObject(child, success, addGlobal) && ret; + } + } + ret = generalEndAddObject(obj, success, success || !addGlobal) && ret; + return ret; + } +} diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java index 8071e8b8..11d338d0 100644 --- a/android/net/ConnectivityManager.java +++ b/android/net/ConnectivityManager.java @@ -1794,7 +1794,7 @@ public class ConnectivityManager { ITelephony it = ITelephony.Stub.asInterface(b); int subId = SubscriptionManager.getDefaultDataSubscriptionId(); Log.d("ConnectivityManager", "getMobileDataEnabled()+ subId=" + subId); - boolean retVal = it.getDataEnabled(subId); + boolean retVal = it.isUserDataEnabled(subId); Log.d("ConnectivityManager", "getMobileDataEnabled()- subId=" + subId + " retVal=" + retVal); return retVal; diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java index d6e62cf1..f82627b9 100644 --- a/android/net/IpSecAlgorithm.java +++ b/android/net/IpSecAlgorithm.java @@ -21,6 +21,7 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.HexDump; import java.lang.annotation.Retention; @@ -34,6 +35,8 @@ import java.util.Arrays; * Internet Protocol */ public final class IpSecAlgorithm implements Parcelable { + private static final String TAG = "IpSecAlgorithm"; + /** * AES-CBC Encryption/Ciphering Algorithm. * @@ -45,6 +48,7 @@ public final class IpSecAlgorithm implements Parcelable { * MD5 HMAC Authentication/Integrity Algorithm. This algorithm is not recommended for use in * new applications and is provided for legacy compatibility with 3gpp infrastructure. * + *

      Keys for this algorithm must be 128 bits in length. *

      Valid truncation lengths are multiples of 8 bits from 96 to (default) 128. */ public static final String AUTH_HMAC_MD5 = "hmac(md5)"; @@ -53,6 +57,7 @@ public final class IpSecAlgorithm implements Parcelable { * SHA1 HMAC Authentication/Integrity Algorithm. This algorithm is not recommended for use in * new applications and is provided for legacy compatibility with 3gpp infrastructure. * + *

      Keys for this algorithm must be 160 bits in length. *

      Valid truncation lengths are multiples of 8 bits from 96 to (default) 160. */ public static final String AUTH_HMAC_SHA1 = "hmac(sha1)"; @@ -60,6 +65,7 @@ public final class IpSecAlgorithm implements Parcelable { /** * SHA256 HMAC Authentication/Integrity Algorithm. * + *

      Keys for this algorithm must be 256 bits in length. *

      Valid truncation lengths are multiples of 8 bits from 96 to (default) 256. */ public static final String AUTH_HMAC_SHA256 = "hmac(sha256)"; @@ -67,6 +73,7 @@ public final class IpSecAlgorithm implements Parcelable { /** * SHA384 HMAC Authentication/Integrity Algorithm. * + *

      Keys for this algorithm must be 384 bits in length. *

      Valid truncation lengths are multiples of 8 bits from 192 to (default) 384. */ public static final String AUTH_HMAC_SHA384 = "hmac(sha384)"; @@ -74,6 +81,7 @@ public final class IpSecAlgorithm implements Parcelable { /** * SHA512 HMAC Authentication/Integrity Algorithm. * + *

      Keys for this algorithm must be 512 bits in length. *

      Valid truncation lengths are multiples of 8 bits from 256 to (default) 512. */ public static final String AUTH_HMAC_SHA512 = "hmac(sha512)"; @@ -130,12 +138,10 @@ public final class IpSecAlgorithm implements Parcelable { * @param truncLenBits number of bits of output hash to use. */ public IpSecAlgorithm(@AlgorithmName String algorithm, @NonNull byte[] key, int truncLenBits) { - if (!isTruncationLengthValid(algorithm, truncLenBits)) { - throw new IllegalArgumentException("Unknown algorithm or invalid length"); - } mName = algorithm; mKey = key.clone(); - mTruncLenBits = Math.min(truncLenBits, key.length * 8); + mTruncLenBits = truncLenBits; + checkValidOrThrow(mName, mKey.length * 8, mTruncLenBits); } /** Get the algorithm name */ @@ -169,7 +175,11 @@ public final class IpSecAlgorithm implements Parcelable { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public IpSecAlgorithm createFromParcel(Parcel in) { - return new IpSecAlgorithm(in); + final String name = in.readString(); + final byte[] key = in.createByteArray(); + final int truncLenBits = in.readInt(); + + return new IpSecAlgorithm(name, key, truncLenBits); } public IpSecAlgorithm[] newArray(int size) { @@ -177,30 +187,47 @@ public final class IpSecAlgorithm implements Parcelable { } }; - private IpSecAlgorithm(Parcel in) { - mName = in.readString(); - mKey = in.createByteArray(); - mTruncLenBits = in.readInt(); - } + private static void checkValidOrThrow(String name, int keyLen, int truncLen) { + boolean isValidLen = true; + boolean isValidTruncLen = true; - private static boolean isTruncationLengthValid(String algo, int truncLenBits) { - switch (algo) { + switch(name) { case CRYPT_AES_CBC: - return (truncLenBits == 128 || truncLenBits == 192 || truncLenBits == 256); + isValidLen = keyLen == 128 || keyLen == 192 || keyLen == 256; + break; case AUTH_HMAC_MD5: - return (truncLenBits >= 96 && truncLenBits <= 128); + isValidLen = keyLen == 128; + isValidTruncLen = truncLen >= 96 && truncLen <= 128; + break; case AUTH_HMAC_SHA1: - return (truncLenBits >= 96 && truncLenBits <= 160); + isValidLen = keyLen == 160; + isValidTruncLen = truncLen >= 96 && truncLen <= 160; + break; case AUTH_HMAC_SHA256: - return (truncLenBits >= 96 && truncLenBits <= 256); + isValidLen = keyLen == 256; + isValidTruncLen = truncLen >= 96 && truncLen <= 256; + break; case AUTH_HMAC_SHA384: - return (truncLenBits >= 192 && truncLenBits <= 384); + isValidLen = keyLen == 384; + isValidTruncLen = truncLen >= 192 && truncLen <= 384; + break; case AUTH_HMAC_SHA512: - return (truncLenBits >= 256 && truncLenBits <= 512); + isValidLen = keyLen == 512; + isValidTruncLen = truncLen >= 256 && truncLen <= 512; + break; case AUTH_CRYPT_AES_GCM: - return (truncLenBits == 64 || truncLenBits == 96 || truncLenBits == 128); + // The keying material for GCM is a key plus a 32-bit salt + isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32; + break; default: - return false; + throw new IllegalArgumentException("Couldn't find an algorithm: " + name); + } + + if (!isValidLen) { + throw new IllegalArgumentException("Invalid key material keyLength: " + keyLen); + } + if (!isValidTruncLen) { + throw new IllegalArgumentException("Invalid truncation keyLength: " + truncLen); } } @@ -217,8 +244,9 @@ public final class IpSecAlgorithm implements Parcelable { .toString(); } - /** package */ - static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) { + /** @hide */ + @VisibleForTesting + public static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) { if (lhs == null || rhs == null) return (lhs == rhs); return (lhs.mName.equals(rhs.mName) && Arrays.equals(lhs.mKey, rhs.mKey) diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java index a9e60ec8..6a4b8914 100644 --- a/android/net/IpSecManager.java +++ b/android/net/IpSecManager.java @@ -46,7 +46,7 @@ import java.net.Socket; * to create a VPN should use {@link VpnService}. * * @see RFC 4301, Security Architecture for the - * Internet Protocol + * Internet Protocol */ @SystemService(Context.IPSEC_SERVICE) public final class IpSecManager { @@ -59,8 +59,7 @@ public final class IpSecManager { * * @hide */ - @TestApi - public static final int INVALID_SECURITY_PARAMETER_INDEX = 0; + @TestApi public static final int INVALID_SECURITY_PARAMETER_INDEX = 0; /** @hide */ public interface Status { @@ -78,7 +77,7 @@ public final class IpSecManager { *

      The combination of remote {@code InetAddress} and SPI must be unique across all apps on * one device. If this error is encountered, a new SPI is required before a transform may be * created. This error can be avoided by calling {@link - * IpSecManager#reserveSecurityParameterIndex}. + * IpSecManager#allocateSecurityParameterIndex}. */ public static final class SpiUnavailableException extends AndroidException { private final int mSpi; @@ -121,7 +120,7 @@ public final class IpSecManager { * This class represents a reserved SPI. * *

      Objects of this type are used to track reserved security parameter indices. They can be - * obtained by calling {@link IpSecManager#reserveSecurityParameterIndex} and must be released + * obtained by calling {@link IpSecManager#allocateSecurityParameterIndex} and must be released * by calling {@link #close()} when they are no longer needed. */ public static final class SecurityParameterIndex implements AutoCloseable { @@ -170,7 +169,7 @@ public final class IpSecManager { mRemoteAddress = remoteAddress; try { IpSecSpiResponse result = - mService.reserveSecurityParameterIndex( + mService.allocateSecurityParameterIndex( direction, remoteAddress.getHostAddress(), spi, new Binder()); if (result == null) { @@ -228,7 +227,7 @@ public final class IpSecManager { * for this user * @throws SpiUnavailableException indicating that a particular SPI cannot be reserved */ - public SecurityParameterIndex reserveSecurityParameterIndex( + public SecurityParameterIndex allocateSecurityParameterIndex( int direction, InetAddress remoteAddress) throws ResourceUnavailableException { try { return new SecurityParameterIndex( @@ -255,7 +254,7 @@ public final class IpSecManager { * for this user * @throws SpiUnavailableException indicating that the requested SPI could not be reserved */ - public SecurityParameterIndex reserveSecurityParameterIndex( + public SecurityParameterIndex allocateSecurityParameterIndex( int direction, InetAddress remoteAddress, int requestedSpi) throws SpiUnavailableException, ResourceUnavailableException { if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) { @@ -273,16 +272,18 @@ public final class IpSecManager { * unprotected traffic can resume on that socket. * *

      For security reasons, the destination address of any traffic on the socket must match the - * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any + * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any * other IP address will result in an IOException. In addition, reads and writes on the socket * will throw IOException if the user deactivates the transform (by calling {@link * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}. * - *

      Rekey Procedure

      When applying a new tranform to a socket, the previous transform - * will be removed. However, inbound traffic on the old transform will continue to be decrypted - * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap - * allows rekey procedures where both transforms are valid until both endpoints are using the - * new transform and all in-flight packets have been received. + *

      Rekey Procedure

      + * + *

      When applying a new tranform to a socket, the previous transform will be removed. However, + * inbound traffic on the old transform will continue to be decrypted until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures + * where both transforms are valid until both endpoints are using the new transform and all + * in-flight packets have been received. * * @param socket a stream socket * @param transform a transport mode {@code IpSecTransform} @@ -310,11 +311,13 @@ public final class IpSecManager { * will throw IOException if the user deactivates the transform (by calling {@link * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}. * - *

      Rekey Procedure

      When applying a new tranform to a socket, the previous transform - * will be removed. However, inbound traffic on the old transform will continue to be decrypted - * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap - * allows rekey procedures where both transforms are valid until both endpoints are using the - * new transform and all in-flight packets have been received. + *

      Rekey Procedure

      + * + *

      When applying a new tranform to a socket, the previous transform will be removed. However, + * inbound traffic on the old transform will continue to be decrypted until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures + * where both transforms are valid until both endpoints are using the new transform and all + * in-flight packets have been received. * * @param socket a datagram socket * @param transform a transport mode {@code IpSecTransform} @@ -342,11 +345,13 @@ public final class IpSecManager { * will throw IOException if the user deactivates the transform (by calling {@link * IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}. * - *

      Rekey Procedure

      When applying a new tranform to a socket, the previous transform - * will be removed. However, inbound traffic on the old transform will continue to be decrypted - * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap - * allows rekey procedures where both transforms are valid until both endpoints are using the - * new transform and all in-flight packets have been received. + *

      Rekey Procedure

      + * + *

      When applying a new tranform to a socket, the previous transform will be removed. However, + * inbound traffic on the old transform will continue to be decrypted until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures + * where both transforms are valid until both endpoints are using the new transform and all + * in-flight packets have been received. * * @param socket a socket file descriptor * @param transform a transport mode {@code IpSecTransform} @@ -379,7 +384,8 @@ public final class IpSecManager { * Applications should probably not use this API directly. Instead, they should use {@link * VpnService} to provide VPN capability in a more generic fashion. * - * TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked. + *

      TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked. + * * @param net a {@link Network} that will be tunneled via IP Sec. * @param transform an {@link IpSecTransform}, which must be an active Tunnel Mode transform. * @hide @@ -469,7 +475,8 @@ public final class IpSecManager { * all traffic that cannot be routed to the Tunnel's outbound interface. If that interface is * lost, all traffic will drop. * - * TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked. + *

      TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked. + * * @param net a network that currently has transform applied to it. * @param transform a Tunnel Mode IPsec Transform that has been previously applied to the given * network diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java index cda4ec76..7cd742b4 100644 --- a/android/net/IpSecTransform.java +++ b/android/net/IpSecTransform.java @@ -47,7 +47,7 @@ import java.net.InetAddress; * system resources. * * @see RFC 4301, Security Architecture for the - * Internet Protocol + * Internet Protocol */ public final class IpSecTransform implements AutoCloseable { private static final String TAG = "IpSecTransform"; @@ -116,8 +116,7 @@ public final class IpSecTransform implements AutoCloseable { } /** - * Checks the result status and throws an appropriate exception if - * the status is not Status.OK. + * Checks the result status and throws an appropriate exception if the status is not Status.OK. */ private void checkResultStatus(int status) throws IOException, IpSecManager.ResourceUnavailableException, @@ -267,9 +266,7 @@ public final class IpSecTransform implements AutoCloseable { return; } - /** - * This class is used to build {@link IpSecTransform} objects. - */ + /** This class is used to build {@link IpSecTransform} objects. */ public static class Builder { private Context mContext; private IpSecConfig mConfig; @@ -339,7 +336,7 @@ public final class IpSecTransform implements AutoCloseable { * *

      Because IPsec operates at the IP layer, this 32-bit identifier uniquely identifies * packets to a given destination address. To prevent SPI collisions, values should be - * reserved by calling {@link IpSecManager#reserveSecurityParameterIndex}. + * reserved by calling {@link IpSecManager#allocateSecurityParameterIndex}. * *

      If the SPI and algorithms are omitted for one direction, traffic in that direction * will not be encrypted or authenticated. @@ -374,10 +371,9 @@ public final class IpSecTransform implements AutoCloseable { *

      This allows IPsec traffic to pass through a NAT. * * @see RFC 3948, UDP Encapsulation of IPsec - * ESP Packets + * ESP Packets * @see RFC 7296 section 2.23, - * NAT Traversal of IKEv2 - * + * NAT Traversal of IKEv2 * @param localSocket a socket for sending and receiving encapsulated traffic * @param remotePort the UDP port number of the remote host that will send and receive * encapsulated traffic. In the case of IKEv2, this should be port 4500. @@ -402,7 +398,6 @@ public final class IpSecTransform implements AutoCloseable { * * @param intervalSeconds the maximum number of seconds between keepalive packets. Must be * between 20s and 3600s. - * * @hide */ @SystemApi @@ -418,7 +413,6 @@ public final class IpSecTransform implements AutoCloseable { * will not affect any network traffic until it has been applied to one or more sockets. * * @see IpSecManager#applyTransportModeTransform - * * @param remoteAddress the remote {@code InetAddress} of traffic on sockets that will use * this transform * @throws IllegalArgumentException indicating that a particular combination of transform @@ -453,8 +447,8 @@ public final class IpSecTransform implements AutoCloseable { */ public IpSecTransform buildTunnelModeTransform( InetAddress localAddress, InetAddress remoteAddress) { - //FIXME: argument validation here - //throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation"); + // FIXME: argument validation here + // throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation"); mConfig.setLocalAddress(localAddress.getHostAddress()); mConfig.setRemoteAddress(remoteAddress.getHostAddress()); mConfig.setMode(MODE_TUNNEL); diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java index f6a69bac..d6992aae 100644 --- a/android/net/MacAddress.java +++ b/android/net/MacAddress.java @@ -16,95 +16,126 @@ package android.net; +import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; import com.android.internal.util.BitUtils; +import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Random; -import java.util.StringJoiner; /** - * Represents a mac address. + * Representation of a MAC address. * - * @hide + * This class only supports 48 bits long addresses and does not support 64 bits long addresses. + * Instances of this class are immutable. */ public final class MacAddress implements Parcelable { private static final int ETHER_ADDR_LEN = 6; private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); - /** The broadcast mac address. */ - public static final MacAddress BROADCAST_ADDRESS = new MacAddress(ETHER_ADDR_BROADCAST); + /** + * The MacAddress representing the unique broadcast MAC address. + */ + public static final MacAddress BROADCAST_ADDRESS = MacAddress.fromBytes(ETHER_ADDR_BROADCAST); - /** The zero mac address. */ + /** + * The MacAddress zero MAC address. + * @hide + */ public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0); - /** Represents categories of mac addresses. */ - public enum MacAddressType { - UNICAST, - MULTICAST, - BROADCAST; - } - - private static final long VALID_LONG_MASK = BROADCAST_ADDRESS.mAddr; - private static final long LOCALLY_ASSIGNED_MASK = new MacAddress("2:0:0:0:0:0").mAddr; - private static final long MULTICAST_MASK = new MacAddress("1:0:0:0:0:0").mAddr; - private static final long OUI_MASK = new MacAddress("ff:ff:ff:0:0:0").mAddr; - private static final long NIC_MASK = new MacAddress("0:0:0:ff:ff:ff").mAddr; - private static final MacAddress BASE_ANDROID_MAC = new MacAddress("da:a1:19:0:0:0"); - - // Internal representation of the mac address as a single 8 byte long. + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_UNKNOWN, + TYPE_UNICAST, + TYPE_MULTICAST, + TYPE_BROADCAST, + }) + public @interface MacAddressType { } + + /** Indicates a MAC address of unknown type. */ + public static final int TYPE_UNKNOWN = 0; + /** Indicates a MAC address is a unicast address. */ + public static final int TYPE_UNICAST = 1; + /** Indicates a MAC address is a multicast address. */ + public static final int TYPE_MULTICAST = 2; + /** Indicates a MAC address is the broadcast address. */ + public static final int TYPE_BROADCAST = 3; + + private static final long VALID_LONG_MASK = (1L << 48) - 1; + private static final long LOCALLY_ASSIGNED_MASK = MacAddress.fromString("2:0:0:0:0:0").mAddr; + private static final long MULTICAST_MASK = MacAddress.fromString("1:0:0:0:0:0").mAddr; + private static final long OUI_MASK = MacAddress.fromString("ff:ff:ff:0:0:0").mAddr; + private static final long NIC_MASK = MacAddress.fromString("0:0:0:ff:ff:ff").mAddr; + private static final MacAddress BASE_GOOGLE_MAC = MacAddress.fromString("da:a1:19:0:0:0"); + + // Internal representation of the MAC address as a single 8 byte long. // The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the - // mac address are encoded in the 6 least significant bytes of the long, where the first + // MAC address are encoded in the 6 least significant bytes of the long, where the first // byte of the array is mapped to the 3rd highest logical byte of the long, the second // byte of the array is mapped to the 4th highest logical byte of the long, and so on. private final long mAddr; private MacAddress(long addr) { - mAddr = addr; - } - - /** Creates a MacAddress for the given byte representation. */ - public MacAddress(byte[] addr) { - this(longAddrFromByteAddr(addr)); + mAddr = (VALID_LONG_MASK & addr); } - /** Creates a MacAddress for the given string representation. */ - public MacAddress(String addr) { - this(longAddrFromByteAddr(byteAddrFromStringAddr(addr))); - } - - /** Returns the MacAddressType of this MacAddress. */ - public MacAddressType addressType() { + /** + * Returns the type of this address. + * + * @return the int constant representing the MAC address type of this MacAddress. + */ + public @MacAddressType int addressType() { if (equals(BROADCAST_ADDRESS)) { - return MacAddressType.BROADCAST; + return TYPE_BROADCAST; } if (isMulticastAddress()) { - return MacAddressType.MULTICAST; + return TYPE_MULTICAST; } - return MacAddressType.UNICAST; + return TYPE_UNICAST; } - /** Returns true if this MacAddress corresponds to a multicast address. */ + /** + * @return true if this MacAddress is a multicast address. + * @hide + */ public boolean isMulticastAddress() { return (mAddr & MULTICAST_MASK) != 0; } - /** Returns true if this MacAddress corresponds to a locally assigned address. */ + /** + * @return true if this MacAddress is a locally assigned address. + */ public boolean isLocallyAssigned() { return (mAddr & LOCALLY_ASSIGNED_MASK) != 0; } - /** Returns a byte array representation of this MacAddress. */ + /** + * @return a byte array representation of this MacAddress. + */ public byte[] toByteArray() { return byteAddrFromLongAddr(mAddr); } @Override public String toString() { - return stringAddrFromByteAddr(byteAddrFromLongAddr(mAddr)); + return stringAddrFromLongAddr(mAddr); + } + + /** + * @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal + * numbers in [0,ff] joined by ':' characters. + */ + public String toOuiString() { + return String.format( + "%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff); } @Override @@ -138,27 +169,50 @@ public final class MacAddress implements Parcelable { } }; - /** Return true if the given byte array is not null and has the length of a mac address. */ + /** + * Returns true if the given byte array is an valid MAC address. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array. + * @return true if the given byte array is not null and has the length of a MAC address. + * + * @hide + */ public static boolean isMacAddress(byte[] addr) { return addr != null && addr.length == ETHER_ADDR_LEN; } /** - * Return the MacAddressType of the mac address represented by the given byte array, - * or null if the given byte array does not represent an mac address. + * Returns the MAC address type of the MAC address represented by the given byte array, + * or null if the given byte array does not represent a MAC address. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representing a MAC address. + * @return the int constant representing the MAC address type of the MAC address represented + * by the given byte array, or type UNKNOWN if the byte array is not a valid MAC address. + * + * @hide */ - public static MacAddressType macAddressType(byte[] addr) { + public static int macAddressType(byte[] addr) { if (!isMacAddress(addr)) { - return null; + return TYPE_UNKNOWN; } - return new MacAddress(addr).addressType(); + return MacAddress.fromBytes(addr).addressType(); } - /** DOCME */ + /** + * Converts a String representation of a MAC address to a byte array representation. + * A valid String representation for a MacAddress is a series of 6 values in the + * range [0,ff] printed in hexadecimal and joined by ':' characters. + * + * @param addr a String representation of a MAC address. + * @return the byte representation of the MAC address. + * @throws IllegalArgumentException if the given String is not a valid representation. + * + * @hide + */ public static byte[] byteAddrFromStringAddr(String addr) { - if (addr == null) { - throw new IllegalArgumentException("cannot convert the null String"); - } + Preconditions.checkNotNull(addr); String[] parts = addr.split(":"); if (parts.length != ETHER_ADDR_LEN) { throw new IllegalArgumentException(addr + " was not a valid MAC address"); @@ -174,20 +228,26 @@ public final class MacAddress implements Parcelable { return bytes; } - /** DOCME */ + /** + * Converts a byte array representation of a MAC address to a String representation made + * of 6 hexadecimal numbers in [0,ff] joined by ':' characters. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representation of a MAC address. + * @return the String representation of the MAC address. + * @throws IllegalArgumentException if the given byte array is not a valid representation. + * + * @hide + */ public static String stringAddrFromByteAddr(byte[] addr) { if (!isMacAddress(addr)) { return null; } - StringJoiner j = new StringJoiner(":"); - for (byte b : addr) { - j.add(Integer.toHexString(BitUtils.uint8(b))); - } - return j.toString(); + return String.format("%02x:%02x:%02x:%02x:%02x:%02x", + addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); } - /** @hide */ - public static byte[] byteAddrFromLongAddr(long addr) { + private static byte[] byteAddrFromLongAddr(long addr) { byte[] bytes = new byte[ETHER_ADDR_LEN]; int index = ETHER_ADDR_LEN; while (index-- > 0) { @@ -197,8 +257,8 @@ public final class MacAddress implements Parcelable { return bytes; } - /** @hide */ - public static long longAddrFromByteAddr(byte[] addr) { + private static long longAddrFromByteAddr(byte[] addr) { + Preconditions.checkNotNull(addr); if (!isMacAddress(addr)) { throw new IllegalArgumentException( Arrays.toString(addr) + " was not a valid MAC address"); @@ -210,19 +270,17 @@ public final class MacAddress implements Parcelable { return longAddr; } - /** @hide */ - public static long longAddrFromStringAddr(String addr) { - if (addr == null) { - throw new IllegalArgumentException("cannot convert the null String"); - } + // Internal conversion function equivalent to longAddrFromByteAddr(byteAddrFromStringAddr(addr)) + // that avoids the allocation of an intermediary byte[]. + private static long longAddrFromStringAddr(String addr) { + Preconditions.checkNotNull(addr); String[] parts = addr.split(":"); if (parts.length != ETHER_ADDR_LEN) { throw new IllegalArgumentException(addr + " was not a valid MAC address"); } long longAddr = 0; - int index = ETHER_ADDR_LEN; - while (index-- > 0) { - int x = Integer.valueOf(parts[index], 16); + for (int i = 0; i < parts.length; i++) { + int x = Integer.valueOf(parts[i], 16); if (x < 0 || 0xff < x) { throw new IllegalArgumentException(addr + "was not a valid MAC address"); } @@ -231,32 +289,74 @@ public final class MacAddress implements Parcelable { return longAddr; } - /** @hide */ - public static String stringAddrFromLongAddr(long addr) { - addr = Long.reverseBytes(addr) >> 16; - StringJoiner j = new StringJoiner(":"); - for (int i = 0; i < ETHER_ADDR_LEN; i++) { - j.add(Integer.toHexString((byte) addr)); - addr = addr >> 8; - } - return j.toString(); + // Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr)) + // that avoids the allocation of an intermediary byte[]. + private static String stringAddrFromLongAddr(long addr) { + return String.format("%02x:%02x:%02x:%02x:%02x:%02x", + (addr >> 40) & 0xff, + (addr >> 32) & 0xff, + (addr >> 24) & 0xff, + (addr >> 16) & 0xff, + (addr >> 8) & 0xff, + addr & 0xff); + } + + /** + * Creates a MacAddress from the given String representation. A valid String representation + * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal + * and joined by ':' characters. + * + * @param addr a String representation of a MAC address. + * @return the MacAddress corresponding to the given String representation. + * @throws IllegalArgumentException if the given String is not a valid representation. + */ + public static MacAddress fromString(String addr) { + return new MacAddress(longAddrFromStringAddr(addr)); + } + + /** + * Creates a MacAddress from the given byte array representation. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representation of a MAC address. + * @return the MacAddress corresponding to the given byte array representation. + * @throws IllegalArgumentException if the given byte array is not a valid representation. + */ + public static MacAddress fromBytes(byte[] addr) { + return new MacAddress(longAddrFromByteAddr(addr)); } /** - * Returns a randomely generated mac address with the Android OUI value "DA-A1-19". - * The locally assigned bit is always set to 1. + * Returns a generated MAC address whose 24 least significant bits constituting the + * NIC part of the address are randomly selected. + * + * The locally assigned bit is always set to 1. The multicast bit is always set to 0. + * + * @return a random locally assigned MacAddress. + * + * @hide */ - public static MacAddress getRandomAddress() { - return getRandomAddress(BASE_ANDROID_MAC, new Random()); + public static MacAddress createRandomUnicastAddress() { + return createRandomUnicastAddress(BASE_GOOGLE_MAC, new Random()); } /** - * Returns a randomely generated mac address using the given Random object and the same - * OUI values as the given MacAddress. The locally assigned bit is always set to 1. + * Returns a randomly generated MAC address using the given Random object and the same + * OUI values as the given MacAddress. + * + * The locally assigned bit is always set to 1. The multicast bit is always set to 0. + * + * @param base a base MacAddress whose OUI is used for generating the random address. + * @param r a standard Java Random object used for generating the random address. + * @return a random locally assigned MacAddress. + * + * @hide */ - public static MacAddress getRandomAddress(MacAddress base, Random r) { - long longAddr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong()) | LOCALLY_ASSIGNED_MASK; - return new MacAddress(longAddr); + public static MacAddress createRandomUnicastAddress(MacAddress base, Random r) { + long addr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong()); + addr = addr | LOCALLY_ASSIGNED_MASK; + addr = addr & ~MULTICAST_MASK; + return new MacAddress(addr); } // Convenience function for working around the lack of byte literals. diff --git a/android/net/TrafficStats.java b/android/net/TrafficStats.java index c339856f..196a3bc9 100644 --- a/android/net/TrafficStats.java +++ b/android/net/TrafficStats.java @@ -17,7 +17,9 @@ package android.net; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.DownloadManager; import android.app.backup.BackupManager; import android.app.usage.NetworkStatsManager; @@ -30,6 +32,8 @@ import com.android.server.NetworkManagementSocketTagger; import dalvik.system.SocketTagger; +import java.io.FileDescriptor; +import java.io.IOException; import java.net.DatagramSocket; import java.net.Socket; import java.net.SocketException; @@ -151,6 +155,8 @@ public class TrafficStats { private static Object sProfilingLock = new Object(); + private static final String LOOPBACK_IFACE = "lo"; + /** * Set active tag to use when accounting {@link Socket} traffic originating * from the current thread. Only one active tag per thread is supported. @@ -263,15 +269,26 @@ public class TrafficStats { NetworkManagementSocketTagger.setThreadSocketStatsUid(uid); } + /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread as the calling UID. Designed for use + * when another application is performing operations on your behalf. + *

      + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + */ + public static void setThreadStatsUidSelf() { + setThreadStatsUid(android.os.Process.myUid()); + } + /** * Clear any active UID set to account {@link Socket} traffic originating * from the current thread. * * @see #setThreadStatsUid(int) - * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) + @SuppressLint("Doclava125") public static void clearThreadStatsUid() { NetworkManagementSocketTagger.setThreadSocketStatsUid(-1); } @@ -315,6 +332,27 @@ public class TrafficStats { SocketTagger.get().untag(socket); } + /** + * Tag the given {@link FileDescriptor} socket with any statistics + * parameters active for the current thread. Subsequent calls always replace + * any existing parameters. When finished, call + * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics + * parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().tag(fd); + } + + /** + * Remove any statistics parameters from the given {@link FileDescriptor} + * socket. + */ + public static void untagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().untag(fd); + } + /** * Start profiling data usage for current UID. Only one profiling session * can be active at a time. @@ -467,7 +505,12 @@ public class TrafficStats { public static long getMobileTcpRxPackets() { long total = 0; for (String iface : getMobileIfaces()) { - final long stat = nativeGetIfaceStat(iface, TYPE_TCP_RX_PACKETS); + long stat = UNSUPPORTED; + try { + stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } if (stat != UNSUPPORTED) { total += stat; } @@ -479,7 +522,12 @@ public class TrafficStats { public static long getMobileTcpTxPackets() { long total = 0; for (String iface : getMobileIfaces()) { - final long stat = nativeGetIfaceStat(iface, TYPE_TCP_TX_PACKETS); + long stat = UNSUPPORTED; + try { + stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } if (stat != UNSUPPORTED) { total += stat; } @@ -489,22 +537,78 @@ public class TrafficStats { /** {@hide} */ public static long getTxPackets(String iface) { - return nativeGetIfaceStat(iface, TYPE_TX_PACKETS); + try { + return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** {@hide} */ public static long getRxPackets(String iface) { - return nativeGetIfaceStat(iface, TYPE_RX_PACKETS); + try { + return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** {@hide} */ public static long getTxBytes(String iface) { - return nativeGetIfaceStat(iface, TYPE_TX_BYTES); + try { + return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** {@hide} */ public static long getRxBytes(String iface) { - return nativeGetIfaceStat(iface, TYPE_RX_BYTES); + try { + return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackTxPackets() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackRxPackets() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackTxBytes() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackRxBytes() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -517,7 +621,11 @@ public class TrafficStats { * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalTxPackets() { - return nativeGetTotalStat(TYPE_TX_PACKETS); + try { + return getStatsService().getTotalStats(TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -530,7 +638,11 @@ public class TrafficStats { * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalRxPackets() { - return nativeGetTotalStat(TYPE_RX_PACKETS); + try { + return getStatsService().getTotalStats(TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -543,7 +655,11 @@ public class TrafficStats { * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalTxBytes() { - return nativeGetTotalStat(TYPE_TX_BYTES); + try { + return getStatsService().getTotalStats(TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -556,7 +672,11 @@ public class TrafficStats { * return {@link #UNSUPPORTED} on devices where statistics aren't available. */ public static long getTotalRxBytes() { - return nativeGetTotalStat(TYPE_RX_BYTES); + try { + return getStatsService().getTotalStats(TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -582,7 +702,11 @@ public class TrafficStats { // unsupported value. The real filtering is done at the kernel level. final int callingUid = android.os.Process.myUid(); if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) { - return nativeGetUidStat(uid, TYPE_TX_BYTES); + try { + return getStatsService().getUidStats(uid, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } else { return UNSUPPORTED; } @@ -611,7 +735,11 @@ public class TrafficStats { // unsupported value. The real filtering is done at the kernel level. final int callingUid = android.os.Process.myUid(); if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) { - return nativeGetUidStat(uid, TYPE_RX_BYTES); + try { + return getStatsService().getUidStats(uid, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } else { return UNSUPPORTED; } @@ -640,7 +768,11 @@ public class TrafficStats { // unsupported value. The real filtering is done at the kernel level. final int callingUid = android.os.Process.myUid(); if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) { - return nativeGetUidStat(uid, TYPE_TX_PACKETS); + try { + return getStatsService().getUidStats(uid, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } else { return UNSUPPORTED; } @@ -669,7 +801,11 @@ public class TrafficStats { // unsupported value. The real filtering is done at the kernel level. final int callingUid = android.os.Process.myUid(); if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) { - return nativeGetUidStat(uid, TYPE_RX_PACKETS); + try { + return getStatsService().getUidStats(uid, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } else { return UNSUPPORTED; } @@ -797,8 +933,4 @@ public class TrafficStats { private static final int TYPE_TX_PACKETS = 3; private static final int TYPE_TCP_RX_PACKETS = 4; private static final int TYPE_TCP_TX_PACKETS = 5; - - private static native long nativeGetTotalStat(int type); - private static native long nativeGetIfaceStat(String iface, int type); - private static native long nativeGetUidStat(int uid, int type); } diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java index 1925c39e..6cf4fa9a 100644 --- a/android/net/ip/ConnectivityPacketTracker.java +++ b/android/net/ip/ConnectivityPacketTracker.java @@ -19,7 +19,7 @@ package android.net.ip; import static android.system.OsConstants.*; import android.net.NetworkUtils; -import android.net.util.BlockingSocketReader; +import android.net.util.PacketReader; import android.net.util.ConnectivityPacketSummary; import android.os.Handler; import android.system.ErrnoException; @@ -65,7 +65,7 @@ public class ConnectivityPacketTracker { private final String mTag; private final LocalLog mLog; - private final BlockingSocketReader mPacketListener; + private final PacketReader mPacketListener; private boolean mRunning; private String mDisplayName; @@ -101,7 +101,7 @@ public class ConnectivityPacketTracker { mDisplayName = null; } - private final class PacketListener extends BlockingSocketReader { + private final class PacketListener extends PacketReader { private final int mIfIndex; private final byte mHwAddr[]; diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java index 70983c86..fdb366c5 100644 --- a/android/net/ip/IpClient.java +++ b/android/net/ip/IpClient.java @@ -815,6 +815,15 @@ public class IpClient extends StateMachine { pw.println(Objects.toString(provisioningConfig, "N/A")); pw.decreaseIndent(); + final IpReachabilityMonitor iprm = mIpReachabilityMonitor; + if (iprm != null) { + pw.println(); + pw.println(mTag + " current IpReachabilityMonitor state:"); + pw.increaseIndent(); + iprm.dump(pw); + pw.decreaseIndent(); + } + pw.println(); pw.println(mTag + " StateMachine dump:"); pw.increaseIndent(); @@ -1237,6 +1246,7 @@ public class IpClient extends StateMachine { mIpReachabilityMonitor = new IpReachabilityMonitor( mContext, mInterfaceName, + getHandler(), mLog, new IpReachabilityMonitor.Callback() { @Override diff --git a/android/net/ip/IpNeighborMonitor.java b/android/net/ip/IpNeighborMonitor.java new file mode 100644 index 00000000..68073347 --- /dev/null +++ b/android/net/ip/IpNeighborMonitor.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.ip; + +import android.net.netlink.NetlinkConstants; +import android.net.netlink.NetlinkErrorMessage; +import android.net.netlink.NetlinkMessage; +import android.net.netlink.NetlinkSocket; +import android.net.netlink.RtNetlinkNeighborMessage; +import android.net.netlink.StructNdMsg; +import android.net.netlink.StructNlMsgHdr; +import android.net.util.PacketReader; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.NetlinkSocketAddress; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import com.android.internal.util.BitUtils; + +import libcore.io.IoUtils; +import libcore.io.Libcore; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.StringJoiner; + + +/** + * IpNeighborMonitor. + * + * Monitors the kernel rtnetlink neighbor notifications and presents to callers + * NeighborEvents describing each event. Callers can provide a consumer instance + * to both filter (e.g. by interface index and IP address) and handle the + * generated NeighborEvents. + * + * @hide + */ +public class IpNeighborMonitor extends PacketReader { + private static final String TAG = IpNeighborMonitor.class.getSimpleName(); + private static final boolean DBG = false; + private static final boolean VDBG = false; + + /** + * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND) + * for the given IP address on the specified interface index. + * + * @return 0 if the request was successfully passed to the kernel; otherwise return + * a non-zero error code. + */ + public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) { + final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex; + if (DBG) { Log.d(TAG, msgSnippet); } + + final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage( + 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null); + + try { + NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg); + } catch (ErrnoException e) { + Log.e(TAG, "Error " + msgSnippet + ": " + e); + return -e.errno; + } + + return 0; + } + + public static class NeighborEvent { + final long elapsedMs; + final short msgType; + final int ifindex; + final InetAddress ip; + final short nudState; + final byte[] linkLayerAddr; + + public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip, + short nudState, byte[] linkLayerAddr) { + this.elapsedMs = elapsedMs; + this.msgType = msgType; + this.ifindex = ifindex; + this.ip = ip; + this.nudState = nudState; + this.linkLayerAddr = linkLayerAddr; + } + + boolean isConnected() { + return (msgType != NetlinkConstants.RTM_DELNEIGH) && + StructNdMsg.isNudStateConnected(nudState); + } + + boolean isValid() { + return (msgType != NetlinkConstants.RTM_DELNEIGH) && + StructNdMsg.isNudStateValid(nudState); + } + + @Override + public String toString() { + final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}"); + return j.add("@" + elapsedMs) + .add(NetlinkConstants.stringForNlMsgType(msgType)) + .add("if=" + ifindex) + .add(ip.getHostAddress()) + .add(StructNdMsg.stringForNudState(nudState)) + .add("[" + NetlinkConstants.hexify(linkLayerAddr) + "]") + .toString(); + } + } + + public interface NeighborEventConsumer { + // Every neighbor event received on the netlink socket is passed in + // here. Subclasses should filter for events of interest. + public void accept(NeighborEvent event); + } + + private final SharedLog mLog; + private final NeighborEventConsumer mConsumer; + + public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) { + super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE); + mLog = log.forSubComponent(TAG); + mConsumer = (cb != null) ? cb : (event) -> { /* discard */ }; + } + + @Override + protected FileDescriptor createFd() { + FileDescriptor fd = null; + + try { + fd = NetlinkSocket.forProto(OsConstants.NETLINK_ROUTE); + Os.bind(fd, (SocketAddress)(new NetlinkSocketAddress(0, OsConstants.RTMGRP_NEIGH))); + Os.connect(fd, (SocketAddress)(new NetlinkSocketAddress(0, 0))); + + if (VDBG) { + final NetlinkSocketAddress nlAddr = (NetlinkSocketAddress) Os.getsockname(fd); + Log.d(TAG, "bound to sockaddr_nl{" + + BitUtils.uint32(nlAddr.getPortId()) + ", " + + nlAddr.getGroupsMask() + + "}"); + } + } catch (ErrnoException|SocketException e) { + logError("Failed to create rtnetlink socket", e); + IoUtils.closeQuietly(fd); + return null; + } + + return fd; + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + final long whenMs = SystemClock.elapsedRealtime(); + + final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length); + byteBuffer.order(ByteOrder.nativeOrder()); + + parseNetlinkMessageBuffer(byteBuffer, whenMs); + } + + private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) { + while (byteBuffer.remaining() > 0) { + final int position = byteBuffer.position(); + final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer); + if (nlMsg == null || nlMsg.getHeader() == null) { + byteBuffer.position(position); + mLog.e("unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer)); + break; + } + + final int srcPortId = nlMsg.getHeader().nlmsg_pid; + if (srcPortId != 0) { + mLog.e("non-kernel source portId: " + BitUtils.uint32(srcPortId)); + break; + } + + if (nlMsg instanceof NetlinkErrorMessage) { + mLog.e("netlink error: " + nlMsg); + continue; + } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) { + mLog.i("non-rtnetlink neighbor msg: " + nlMsg); + continue; + } + + evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs); + } + } + + private void evaluateRtNetlinkNeighborMessage( + RtNetlinkNeighborMessage neighMsg, long whenMs) { + final short msgType = neighMsg.getHeader().nlmsg_type; + final StructNdMsg ndMsg = neighMsg.getNdHeader(); + if (ndMsg == null) { + mLog.e("RtNetlinkNeighborMessage without ND message header!"); + return; + } + + final int ifindex = ndMsg.ndm_ifindex; + final InetAddress destination = neighMsg.getDestination(); + final short nudState = + (msgType == NetlinkConstants.RTM_DELNEIGH) + ? StructNdMsg.NUD_NONE + : ndMsg.ndm_state; + + final NeighborEvent event = new NeighborEvent( + whenMs, msgType, ifindex, destination, nudState, neighMsg.getLinkLayerAddress()); + + if (VDBG) { + Log.d(TAG, neighMsg.toString()); + } + if (DBG) { + Log.d(TAG, event.toString()); + } + + mConsumer.accept(event); + } +} diff --git a/android/net/ip/IpReachabilityMonitor.java b/android/net/ip/IpReachabilityMonitor.java index 714b35a0..b31ffbba 100644 --- a/android/net/ip/IpReachabilityMonitor.java +++ b/android/net/ip/IpReachabilityMonitor.java @@ -22,30 +22,27 @@ import android.net.LinkProperties; import android.net.LinkProperties.ProvisioningChange; import android.net.ProxyInfo; import android.net.RouteInfo; +import android.net.ip.IpNeighborMonitor.NeighborEvent; import android.net.metrics.IpConnectivityLog; import android.net.metrics.IpReachabilityEvent; -import android.net.netlink.NetlinkConstants; -import android.net.netlink.NetlinkErrorMessage; -import android.net.netlink.NetlinkMessage; -import android.net.netlink.NetlinkSocket; -import android.net.netlink.RtNetlinkNeighborMessage; import android.net.netlink.StructNdMsg; -import android.net.netlink.StructNdaCacheInfo; -import android.net.netlink.StructNlMsgHdr; import android.net.util.MultinetworkPolicyTracker; import android.net.util.SharedLog; +import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.system.ErrnoException; -import android.system.NetlinkSocketAddress; import android.system.OsConstants; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DumpUtils; +import com.android.internal.util.DumpUtils.Dump; import java.io.InterruptedIOException; +import java.io.PrintWriter; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -134,6 +131,8 @@ import java.util.Set; * state it may be best for the link to disconnect completely and * reconnect afresh. * + * Accessing an instance of this class from multiple threads is NOT safe. + * * @hide */ public class IpReachabilityMonitor { @@ -169,64 +168,33 @@ public class IpReachabilityMonitor { } } - private final Object mLock = new Object(); private final String mInterfaceName; private final int mInterfaceIndex; + private final IpNeighborMonitor mIpNeighborMonitor; private final SharedLog mLog; private final Callback mCallback; private final Dependencies mDependencies; private final MultinetworkPolicyTracker mMultinetworkPolicyTracker; - private final NetlinkSocketObserver mNetlinkSocketObserver; - private final Thread mObserverThread; private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); - @GuardedBy("mLock") private LinkProperties mLinkProperties = new LinkProperties(); - // TODO: consider a map to a private NeighborState class holding more - // information than a single NUD state entry. - @GuardedBy("mLock") - private Map mIpWatchList = new HashMap<>(); - @GuardedBy("mLock") - private int mIpWatchListVersion; - private volatile boolean mRunning; + private Map mNeighborWatchList = new HashMap<>(); // Time in milliseconds of the last forced probe request. private volatile long mLastProbeTimeMs; - /** - * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND) - * for the given IP address on the specified interface index. - * - * @return 0 if the request was successfully passed to the kernel; otherwise return - * a non-zero error code. - */ - private static int probeNeighbor(int ifIndex, InetAddress ip) { - final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex; - if (DBG) { Log.d(TAG, msgSnippet); } - - final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage( - 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null); - - try { - NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg); - } catch (ErrnoException e) { - Log.e(TAG, "Error " + msgSnippet + ": " + e); - return -e.errno; - } - - return 0; + public IpReachabilityMonitor( + Context context, String ifName, Handler h, SharedLog log, Callback callback) { + this(context, ifName, h, log, callback, null); } - public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback) { - this(context, ifName, log, callback, null); - } - - public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback, + public IpReachabilityMonitor( + Context context, String ifName, Handler h, SharedLog log, Callback callback, MultinetworkPolicyTracker tracker) { - this(ifName, getInterfaceIndex(ifName), log, callback, tracker, + this(ifName, getInterfaceIndex(ifName), h, log, callback, tracker, Dependencies.makeDefault(context, ifName)); } @VisibleForTesting - IpReachabilityMonitor(String ifName, int ifIndex, SharedLog log, Callback callback, + IpReachabilityMonitor(String ifName, int ifIndex, Handler h, SharedLog log, Callback callback, MultinetworkPolicyTracker tracker, Dependencies dependencies) { mInterfaceName = ifName; mLog = log.forSubComponent(TAG); @@ -234,45 +202,54 @@ public class IpReachabilityMonitor { mMultinetworkPolicyTracker = tracker; mInterfaceIndex = ifIndex; mDependencies = dependencies; - mNetlinkSocketObserver = new NetlinkSocketObserver(); - mObserverThread = new Thread(mNetlinkSocketObserver); - mObserverThread.start(); + + mIpNeighborMonitor = new IpNeighborMonitor(h, mLog, + (NeighborEvent event) -> { + if (mInterfaceIndex != event.ifindex) return; + if (!mNeighborWatchList.containsKey(event.ip)) return; + + final NeighborEvent prev = mNeighborWatchList.put(event.ip, event); + + // TODO: Consider what to do with other states that are not within + // NeighborEvent#isValid() (i.e. NUD_NONE, NUD_INCOMPLETE). + if (event.nudState == StructNdMsg.NUD_FAILED) { + mLog.w("ALERT neighbor went from: " + prev + " to: " + event); + handleNeighborLost(event); + } + }); + mIpNeighborMonitor.start(); } public void stop() { - mRunning = false; + mIpNeighborMonitor.stop(); clearLinkProperties(); - mNetlinkSocketObserver.clearNetlinkSocket(); } - // TODO: add a public dump() method that can be called during a bug report. - - private String describeWatchList() { - final String delimiter = ", "; - StringBuilder sb = new StringBuilder(); - synchronized (mLock) { - sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}, "); - sb.append("v{" + mIpWatchListVersion + "}, "); - sb.append("ntable=["); - boolean firstTime = true; - for (Map.Entry entry : mIpWatchList.entrySet()) { - if (firstTime) { - firstTime = false; - } else { - sb.append(delimiter); - } - sb.append(entry.getKey().getHostAddress() + "/" + - StructNdMsg.stringForNudState(entry.getValue())); - } - sb.append("]"); - } - return sb.toString(); + public void dump(PrintWriter pw) { + DumpUtils.dumpAsync( + mIpNeighborMonitor.getHandler(), + new Dump() { + @Override + public void dump(PrintWriter pw, String prefix) { + pw.println(describeWatchList("\n")); + } + }, + pw, "", 1000); } - private boolean isWatching(InetAddress ip) { - synchronized (mLock) { - return mRunning && mIpWatchList.containsKey(ip); + private String describeWatchList() { return describeWatchList(" "); } + + private String describeWatchList(String sep) { + final StringBuilder sb = new StringBuilder(); + sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}," + sep); + sb.append("ntable=[" + sep); + String delimiter = ""; + for (Map.Entry entry : mNeighborWatchList.entrySet()) { + sb.append(delimiter).append(entry.getKey().getHostAddress() + "/" + entry.getValue()); + delimiter = "," + sep; } + sb.append("]"); + return sb.toString(); } private static boolean isOnLink(List routes, InetAddress ip) { @@ -284,13 +261,6 @@ public class IpReachabilityMonitor { return false; } - private short getNeighborStateLocked(InetAddress ip) { - if (mIpWatchList.containsKey(ip)) { - return mIpWatchList.get(ip); - } - return StructNdMsg.NUD_NONE; - } - public void updateLinkProperties(LinkProperties lp) { if (!mInterfaceName.equals(lp.getInterfaceName())) { // TODO: figure out whether / how to cope with interface changes. @@ -299,70 +269,63 @@ public class IpReachabilityMonitor { return; } - synchronized (mLock) { - mLinkProperties = new LinkProperties(lp); - Map newIpWatchList = new HashMap<>(); + mLinkProperties = new LinkProperties(lp); + Map newNeighborWatchList = new HashMap<>(); - final List routes = mLinkProperties.getRoutes(); - for (RouteInfo route : routes) { - if (route.hasGateway()) { - InetAddress gw = route.getGateway(); - if (isOnLink(routes, gw)) { - newIpWatchList.put(gw, getNeighborStateLocked(gw)); - } + final List routes = mLinkProperties.getRoutes(); + for (RouteInfo route : routes) { + if (route.hasGateway()) { + InetAddress gw = route.getGateway(); + if (isOnLink(routes, gw)) { + newNeighborWatchList.put(gw, mNeighborWatchList.getOrDefault(gw, null)); } } + } - for (InetAddress nameserver : lp.getDnsServers()) { - if (isOnLink(routes, nameserver)) { - newIpWatchList.put(nameserver, getNeighborStateLocked(nameserver)); - } + for (InetAddress dns : lp.getDnsServers()) { + if (isOnLink(routes, dns)) { + newNeighborWatchList.put(dns, mNeighborWatchList.getOrDefault(dns, null)); } - - mIpWatchList = newIpWatchList; - mIpWatchListVersion++; } + + mNeighborWatchList = newNeighborWatchList; if (DBG) { Log.d(TAG, "watch: " + describeWatchList()); } } public void clearLinkProperties() { - synchronized (mLock) { - mLinkProperties.clear(); - mIpWatchList.clear(); - mIpWatchListVersion++; - } + mLinkProperties.clear(); + mNeighborWatchList.clear(); if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); } } - private void handleNeighborLost(String msg) { - InetAddress ip = null; - final ProvisioningChange delta; - synchronized (mLock) { - LinkProperties whatIfLp = new LinkProperties(mLinkProperties); + private void handleNeighborLost(NeighborEvent event) { + final LinkProperties whatIfLp = new LinkProperties(mLinkProperties); - for (Map.Entry entry : mIpWatchList.entrySet()) { - if (entry.getValue() != StructNdMsg.NUD_FAILED) { - continue; - } - - ip = entry.getKey(); - for (RouteInfo route : mLinkProperties.getRoutes()) { - if (ip.equals(route.getGateway())) { - whatIfLp.removeRoute(route); - } - } - - if (avoidingBadLinks() || !(ip instanceof Inet6Address)) { - // We should do this unconditionally, but alas we cannot: b/31827713. - whatIfLp.removeDnsServer(ip); + InetAddress ip = null; + for (Map.Entry entry : mNeighborWatchList.entrySet()) { + // TODO: Consider using NeighborEvent#isValid() here; it's more + // strict but may interact badly if other entries are somehow in + // NUD_INCOMPLETE (say, during network attach). + if (entry.getValue().nudState != StructNdMsg.NUD_FAILED) continue; + + ip = entry.getKey(); + for (RouteInfo route : mLinkProperties.getRoutes()) { + if (ip.equals(route.getGateway())) { + whatIfLp.removeRoute(route); } } - delta = LinkProperties.compareProvisioning(mLinkProperties, whatIfLp); + if (avoidingBadLinks() || !(ip instanceof Inet6Address)) { + // We should do this unconditionally, but alas we cannot: b/31827713. + whatIfLp.removeDnsServer(ip); + } } + final ProvisioningChange delta = LinkProperties.compareProvisioning( + mLinkProperties, whatIfLp); + if (delta == ProvisioningChange.LOST_PROVISIONING) { - final String logMsg = "FAILURE: LOST_PROVISIONING, " + msg; + final String logMsg = "FAILURE: LOST_PROVISIONING, " + event; Log.w(TAG, logMsg); if (mCallback != null) { // TODO: remove |ip| when the callback signature no longer has @@ -378,12 +341,9 @@ public class IpReachabilityMonitor { } public void probeAll() { - final List ipProbeList; - synchronized (mLock) { - ipProbeList = new ArrayList<>(mIpWatchList.keySet()); - } + final List ipProbeList = new ArrayList<>(mNeighborWatchList.keySet()); - if (!ipProbeList.isEmpty() && mRunning) { + if (!ipProbeList.isEmpty()) { // Keep the CPU awake long enough to allow all ARP/ND // probes a reasonable chance at success. See b/23197666. // @@ -394,13 +354,10 @@ public class IpReachabilityMonitor { } for (InetAddress target : ipProbeList) { - if (!mRunning) { - break; - } - final int returnValue = probeNeighbor(mInterfaceIndex, target); + final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceIndex, target); mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)", - target.getHostAddress(), returnValue)); - logEvent(IpReachabilityEvent.PROBE, returnValue); + target.getHostAddress(), rval)); + logEvent(IpReachabilityEvent.PROBE, rval); } mLastProbeTimeMs = SystemClock.elapsedRealtime(); } @@ -446,153 +403,4 @@ public class IpReachabilityMonitor { int eventType = IpReachabilityEvent.nudFailureEventType(isFromProbe, isProvisioningLost); mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType)); } - - // TODO: simplify the number of objects by making this extend Thread. - private final class NetlinkSocketObserver implements Runnable { - private NetlinkSocket mSocket; - - @Override - public void run() { - if (VDBG) { Log.d(TAG, "Starting observing thread."); } - mRunning = true; - - try { - setupNetlinkSocket(); - } catch (ErrnoException | SocketException e) { - Log.e(TAG, "Failed to suitably initialize a netlink socket", e); - mRunning = false; - } - - while (mRunning) { - final ByteBuffer byteBuffer; - try { - byteBuffer = recvKernelReply(); - } catch (ErrnoException e) { - if (mRunning) { Log.w(TAG, "ErrnoException: ", e); } - break; - } - final long whenMs = SystemClock.elapsedRealtime(); - if (byteBuffer == null) { - continue; - } - parseNetlinkMessageBuffer(byteBuffer, whenMs); - } - - clearNetlinkSocket(); - - mRunning = false; // Not a no-op when ErrnoException happened. - if (VDBG) { Log.d(TAG, "Finishing observing thread."); } - } - - private void clearNetlinkSocket() { - if (mSocket != null) { - mSocket.close(); - } - } - - // TODO: Refactor the main loop to recreate the socket upon recoverable errors. - private void setupNetlinkSocket() throws ErrnoException, SocketException { - clearNetlinkSocket(); - mSocket = new NetlinkSocket(OsConstants.NETLINK_ROUTE); - - final NetlinkSocketAddress listenAddr = new NetlinkSocketAddress( - 0, OsConstants.RTMGRP_NEIGH); - mSocket.bind(listenAddr); - - if (VDBG) { - final NetlinkSocketAddress nlAddr = mSocket.getLocalAddress(); - Log.d(TAG, "bound to sockaddr_nl{" - + ((long) (nlAddr.getPortId() & 0xffffffff)) + ", " - + nlAddr.getGroupsMask() - + "}"); - } - } - - private ByteBuffer recvKernelReply() throws ErrnoException { - try { - return mSocket.recvMessage(0); - } catch (InterruptedIOException e) { - // Interruption or other error, e.g. another thread closed our file descriptor. - } catch (ErrnoException e) { - if (e.errno != OsConstants.EAGAIN) { - throw e; - } - } - return null; - } - - private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) { - while (byteBuffer.remaining() > 0) { - final int position = byteBuffer.position(); - final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer); - if (nlMsg == null || nlMsg.getHeader() == null) { - byteBuffer.position(position); - Log.e(TAG, "unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer)); - break; - } - - final int srcPortId = nlMsg.getHeader().nlmsg_pid; - if (srcPortId != 0) { - Log.e(TAG, "non-kernel source portId: " + ((long) (srcPortId & 0xffffffff))); - break; - } - - if (nlMsg instanceof NetlinkErrorMessage) { - Log.e(TAG, "netlink error: " + nlMsg); - continue; - } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) { - if (DBG) { - Log.d(TAG, "non-rtnetlink neighbor msg: " + nlMsg); - } - continue; - } - - evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs); - } - } - - private void evaluateRtNetlinkNeighborMessage( - RtNetlinkNeighborMessage neighMsg, long whenMs) { - final StructNdMsg ndMsg = neighMsg.getNdHeader(); - if (ndMsg == null || ndMsg.ndm_ifindex != mInterfaceIndex) { - return; - } - - final InetAddress destination = neighMsg.getDestination(); - if (!isWatching(destination)) { - return; - } - - final short msgType = neighMsg.getHeader().nlmsg_type; - final short nudState = ndMsg.ndm_state; - final String eventMsg = "NeighborEvent{" - + "elapsedMs=" + whenMs + ", " - + destination.getHostAddress() + ", " - + "[" + NetlinkConstants.hexify(neighMsg.getLinkLayerAddress()) + "], " - + NetlinkConstants.stringForNlMsgType(msgType) + ", " - + StructNdMsg.stringForNudState(nudState) - + "}"; - - if (VDBG) { - Log.d(TAG, neighMsg.toString()); - } else if (DBG) { - Log.d(TAG, eventMsg); - } - - synchronized (mLock) { - if (mIpWatchList.containsKey(destination)) { - final short value = - (msgType == NetlinkConstants.RTM_DELNEIGH) - ? StructNdMsg.NUD_NONE - : nudState; - mIpWatchList.put(destination, value); - } - } - - if (nudState == StructNdMsg.NUD_FAILED) { - Log.w(TAG, "ALERT: " + eventMsg); - handleNeighborLost(eventMsg); - } - } - } } diff --git a/android/net/metrics/DefaultNetworkEvent.java b/android/net/metrics/DefaultNetworkEvent.java index 8ff8e4f3..6f383b4d 100644 --- a/android/net/metrics/DefaultNetworkEvent.java +++ b/android/net/metrics/DefaultNetworkEvent.java @@ -74,7 +74,7 @@ public class DefaultNetworkEvent { j.add("final_score=" + finalScore); } j.add(String.format("duration=%.0fs", durationMs / 1000.0)); - j.add(String.format("validation=%4.1f%%", (validatedMs * 100.0) / durationMs)); + j.add(String.format("validation=%04.1f%%", (validatedMs * 100.0) / durationMs)); return j.toString(); } diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java index 23c1f20f..7277ba34 100644 --- a/android/net/metrics/WakeupStats.java +++ b/android/net/metrics/WakeupStats.java @@ -16,6 +16,7 @@ package android.net.metrics; +import android.net.MacAddress; import android.os.Process; import android.os.SystemClock; import android.util.SparseIntArray; @@ -80,13 +81,13 @@ public class WakeupStats { } switch (ev.dstHwAddr.addressType()) { - case UNICAST: + case MacAddress.TYPE_UNICAST: l2UnicastCount++; break; - case MULTICAST: + case MacAddress.TYPE_MULTICAST: l2MulticastCount++; break; - case BROADCAST: + case MacAddress.TYPE_BROADCAST: l2BroadcastCount++; break; default: diff --git a/android/net/netlink/NetlinkSocket.java b/android/net/netlink/NetlinkSocket.java index f5f211d8..5af3c299 100644 --- a/android/net/netlink/NetlinkSocket.java +++ b/android/net/netlink/NetlinkSocket.java @@ -16,16 +16,24 @@ package android.net.netlink; +import static android.system.OsConstants.AF_NETLINK; +import static android.system.OsConstants.EIO; +import static android.system.OsConstants.EPROTO; +import static android.system.OsConstants.ETIMEDOUT; +import static android.system.OsConstants.SO_RCVBUF; +import static android.system.OsConstants.SO_RCVTIMEO; +import static android.system.OsConstants.SO_SNDTIMEO; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOL_SOCKET; + import android.system.ErrnoException; import android.system.NetlinkSocketAddress; import android.system.Os; -import android.system.OsConstants; import android.system.StructTimeval; import android.util.Log; import libcore.io.IoUtils; import libcore.io.Libcore; -import java.io.Closeable; import java.io.FileDescriptor; import java.io.InterruptedIOException; import java.net.SocketAddress; @@ -37,28 +45,27 @@ import java.nio.ByteOrder; /** * NetlinkSocket * - * A small wrapper class to assist with AF_NETLINK socket operations. + * A small static class to assist with AF_NETLINK socket operations. * * @hide */ -public class NetlinkSocket implements Closeable { +public class NetlinkSocket { private static final String TAG = "NetlinkSocket"; - private static final int SOCKET_RECV_BUFSIZE = 64 * 1024; - private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024; - final private FileDescriptor mDescriptor; - private NetlinkSocketAddress mAddr; - private long mLastRecvTimeoutMs; - private long mLastSendTimeoutMs; + public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024; + public static final int SOCKET_RECV_BUFSIZE = 64 * 1024; public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException { final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage"; + final long IO_TIMEOUT = 300L; + + FileDescriptor fd; - try (NetlinkSocket nlSocket = new NetlinkSocket(nlProto)) { - final long IO_TIMEOUT = 300L; - nlSocket.connectToKernel(); - nlSocket.sendMessage(msg, 0, msg.length, IO_TIMEOUT); - final ByteBuffer bytes = nlSocket.recvMessage(IO_TIMEOUT); + try { + fd = forProto(nlProto); + connectToKernel(fd); + sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT); + final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT); // recvMessage() guaranteed to not return null if it did not throw. final NetlinkMessage response = NetlinkMessage.parse(bytes); if (response != null && response instanceof NetlinkErrorMessage && @@ -81,61 +88,30 @@ public class NetlinkSocket implements Closeable { errmsg = response.toString(); } Log.e(TAG, errPrefix + ", errmsg=" + errmsg); - throw new ErrnoException(errmsg, OsConstants.EPROTO); + throw new ErrnoException(errmsg, EPROTO); } } catch (InterruptedIOException e) { Log.e(TAG, errPrefix, e); - throw new ErrnoException(errPrefix, OsConstants.ETIMEDOUT, e); + throw new ErrnoException(errPrefix, ETIMEDOUT, e); } catch (SocketException e) { Log.e(TAG, errPrefix, e); - throw new ErrnoException(errPrefix, OsConstants.EIO, e); + throw new ErrnoException(errPrefix, EIO, e); } - } - - public NetlinkSocket(int nlProto) throws ErrnoException { - mDescriptor = Os.socket( - OsConstants.AF_NETLINK, OsConstants.SOCK_DGRAM, nlProto); - - Os.setsockoptInt( - mDescriptor, OsConstants.SOL_SOCKET, - OsConstants.SO_RCVBUF, SOCKET_RECV_BUFSIZE); - } - - public NetlinkSocketAddress getLocalAddress() throws ErrnoException { - return (NetlinkSocketAddress) Os.getsockname(mDescriptor); - } - public void bind(NetlinkSocketAddress localAddr) throws ErrnoException, SocketException { - Os.bind(mDescriptor, (SocketAddress)localAddr); + IoUtils.closeQuietly(fd); } - public void connectTo(NetlinkSocketAddress peerAddr) - throws ErrnoException, SocketException { - Os.connect(mDescriptor, (SocketAddress) peerAddr); + public static FileDescriptor forProto(int nlProto) throws ErrnoException { + final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto); + Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE); + return fd; } - public void connectToKernel() throws ErrnoException, SocketException { - connectTo(new NetlinkSocketAddress(0, 0)); - } - - /** - * Wait indefinitely (or until underlying socket error) for a - * netlink message of at most DEFAULT_RECV_BUFSIZE size. - */ - public ByteBuffer recvMessage() - throws ErrnoException, InterruptedIOException { - return recvMessage(DEFAULT_RECV_BUFSIZE, 0); - } - - /** - * Wait up to |timeoutMs| (or until underlying socket error) for a - * netlink message of at most DEFAULT_RECV_BUFSIZE size. - */ - public ByteBuffer recvMessage(long timeoutMs) throws ErrnoException, InterruptedIOException { - return recvMessage(DEFAULT_RECV_BUFSIZE, timeoutMs); + public static void connectToKernel(FileDescriptor fd) throws ErrnoException, SocketException { + Os.connect(fd, (SocketAddress) (new NetlinkSocketAddress(0, 0))); } - private void checkTimeout(long timeoutMs) { + private static void checkTimeout(long timeoutMs) { if (timeoutMs < 0) { throw new IllegalArgumentException("Negative timeouts not permitted"); } @@ -147,21 +123,14 @@ public class NetlinkSocket implements Closeable { * * Multi-threaded calls with different timeouts will cause unexpected results. */ - public ByteBuffer recvMessage(int bufsize, long timeoutMs) + public static ByteBuffer recvMessage(FileDescriptor fd, int bufsize, long timeoutMs) throws ErrnoException, IllegalArgumentException, InterruptedIOException { checkTimeout(timeoutMs); - synchronized (mDescriptor) { - if (mLastRecvTimeoutMs != timeoutMs) { - Os.setsockoptTimeval(mDescriptor, - OsConstants.SOL_SOCKET, OsConstants.SO_RCVTIMEO, - StructTimeval.fromMillis(timeoutMs)); - mLastRecvTimeoutMs = timeoutMs; - } - } + Os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(timeoutMs)); ByteBuffer byteBuffer = ByteBuffer.allocate(bufsize); - int length = Os.read(mDescriptor, byteBuffer); + int length = Os.read(fd, byteBuffer); if (length == bufsize) { Log.w(TAG, "maximum read"); } @@ -171,40 +140,17 @@ public class NetlinkSocket implements Closeable { return byteBuffer; } - /** - * Send a message to a peer to which this socket has previously connected. - * - * This blocks until completion or an error occurs. - */ - public boolean sendMessage(byte[] bytes, int offset, int count) - throws ErrnoException, InterruptedIOException { - return sendMessage(bytes, offset, count, 0); - } - /** * Send a message to a peer to which this socket has previously connected, * waiting at most |timeoutMs| milliseconds for the send to complete. * * Multi-threaded calls with different timeouts will cause unexpected results. */ - public boolean sendMessage(byte[] bytes, int offset, int count, long timeoutMs) + public static int sendMessage( + FileDescriptor fd, byte[] bytes, int offset, int count, long timeoutMs) throws ErrnoException, IllegalArgumentException, InterruptedIOException { checkTimeout(timeoutMs); - - synchronized (mDescriptor) { - if (mLastSendTimeoutMs != timeoutMs) { - Os.setsockoptTimeval(mDescriptor, - OsConstants.SOL_SOCKET, OsConstants.SO_SNDTIMEO, - StructTimeval.fromMillis(timeoutMs)); - mLastSendTimeoutMs = timeoutMs; - } - } - - return (count == Os.write(mDescriptor, bytes, offset, count)); - } - - @Override - public void close() { - IoUtils.closeQuietly(mDescriptor); + Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(timeoutMs)); + return Os.write(fd, bytes, offset, count); } } diff --git a/android/net/netlink/StructNdMsg.java b/android/net/netlink/StructNdMsg.java index b68ec0bc..e34ec39a 100644 --- a/android/net/netlink/StructNdMsg.java +++ b/android/net/netlink/StructNdMsg.java @@ -63,6 +63,11 @@ public class StructNdMsg { return ((nudState & (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE)) != 0); } + public static boolean isNudStateValid(short nudState) { + return (isNudStateConnected(nudState) || + ((nudState & (NUD_PROBE|NUD_STALE|NUD_DELAY)) != 0)); + } + // Neighbor Cache Entry Flags public static byte NTF_USE = (byte) 0x01; public static byte NTF_SELF = (byte) 0x02; @@ -143,7 +148,7 @@ public class StructNdMsg { } public boolean nudValid() { - return (nudConnected() || ((ndm_state & (NUD_PROBE|NUD_STALE|NUD_DELAY)) != 0)); + return isNudStateValid(ndm_state); } @Override diff --git a/android/net/util/BlockingSocketReader.java b/android/net/util/BlockingSocketReader.java deleted file mode 100644 index 99bf4695..00000000 --- a/android/net/util/BlockingSocketReader.java +++ /dev/null @@ -1,249 +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.net.util; - -import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; -import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; - -import android.annotation.Nullable; -import android.os.Handler; -import android.os.Looper; -import android.os.MessageQueue; -import android.os.MessageQueue.OnFileDescriptorEventListener; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; - -import libcore.io.IoUtils; - -import java.io.FileDescriptor; -import java.io.IOException; - - -/** - * This class encapsulates the mechanics of registering a file descriptor - * with a thread's Looper and handling read events (and errors). - * - * Subclasses MUST implement createFd() and SHOULD override handlePacket(). - - * Subclasses can expect a call life-cycle like the following: - * - * [1] start() calls createFd() and (if all goes well) onStart() - * - * [2] yield, waiting for read event or error notification: - * - * [a] readPacket() && handlePacket() - * - * [b] if (no error): - * goto 2 - * else: - * goto 3 - * - * [3] stop() calls onStop() if not previously stopped - * - * The packet receive buffer is recycled on every read call, so subclasses - * should make any copies they would like inside their handlePacket() - * implementation. - * - * All public methods MUST only be called from the same thread with which - * the Handler constructor argument is associated. - * - * TODO: rename this class to something more correctly descriptive (something - * like [or less horrible than] FdReadEventsHandler?). - * - * @hide - */ -public abstract class BlockingSocketReader { - private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; - private static final int UNREGISTER_THIS_FD = 0; - - public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024; - - private final Handler mHandler; - private final MessageQueue mQueue; - private final byte[] mPacket; - private FileDescriptor mFd; - private long mPacketsReceived; - - protected static void closeFd(FileDescriptor fd) { - IoUtils.closeQuietly(fd); - } - - protected BlockingSocketReader(Handler h) { - this(h, DEFAULT_RECV_BUF_SIZE); - } - - protected BlockingSocketReader(Handler h, int recvbufsize) { - mHandler = h; - mQueue = mHandler.getLooper().getQueue(); - mPacket = new byte[Math.max(recvbufsize, DEFAULT_RECV_BUF_SIZE)]; - } - - public final void start() { - if (onCorrectThread()) { - createAndRegisterFd(); - } else { - mHandler.post(() -> { - logError("start() called from off-thread", null); - createAndRegisterFd(); - }); - } - } - - public final void stop() { - if (onCorrectThread()) { - unregisterAndDestroyFd(); - } else { - mHandler.post(() -> { - logError("stop() called from off-thread", null); - unregisterAndDestroyFd(); - }); - } - } - - public final int recvBufSize() { return mPacket.length; } - - public final long numPacketsReceived() { return mPacketsReceived; } - - /** - * Subclasses MUST create the listening socket here, including setting - * all desired socket options, interface or address/port binding, etc. - */ - protected abstract FileDescriptor createFd(); - - /** - * Subclasses MAY override this to change the default read() implementation - * in favour of, say, recvfrom(). - * - * Implementations MUST return the bytes read or throw an Exception. - */ - protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { - return Os.read(fd, packetBuffer, 0, packetBuffer.length); - } - - /** - * Called by the main loop for every packet. Any desired copies of - * |recvbuf| should be made in here, as the underlying byte array is - * reused across all reads. - */ - protected void handlePacket(byte[] recvbuf, int length) {} - - /** - * Called by the main loop to log errors. In some cases |e| may be null. - */ - protected void logError(String msg, Exception e) {} - - /** - * Called by start(), if successful, just prior to returning. - */ - protected void onStart() {} - - /** - * Called by stop() just prior to returning. - */ - protected void onStop() {} - - private void createAndRegisterFd() { - if (mFd != null) return; - - try { - mFd = createFd(); - if (mFd != null) { - // Force the socket to be non-blocking. - IoUtils.setBlocking(mFd, false); - } - } catch (Exception e) { - logError("Failed to create socket: ", e); - closeFd(mFd); - mFd = null; - return; - } - - if (mFd == null) return; - - mQueue.addOnFileDescriptorEventListener( - mFd, - FD_EVENTS, - new OnFileDescriptorEventListener() { - @Override - public int onFileDescriptorEvents(FileDescriptor fd, int events) { - // Always call handleInput() so read/recvfrom are given - // a proper chance to encounter a meaningful errno and - // perhaps log a useful error message. - if (!isRunning() || !handleInput()) { - unregisterAndDestroyFd(); - return UNREGISTER_THIS_FD; - } - return FD_EVENTS; - } - }); - onStart(); - } - - private boolean isRunning() { return (mFd != null) && mFd.valid(); } - - // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. - private boolean handleInput() { - while (isRunning()) { - final int bytesRead; - - try { - bytesRead = readPacket(mFd, mPacket); - if (bytesRead < 1) { - if (isRunning()) logError("Socket closed, exiting", null); - break; - } - mPacketsReceived++; - } catch (ErrnoException e) { - if (e.errno == OsConstants.EAGAIN) { - // We've read everything there is to read this time around. - return true; - } else if (e.errno == OsConstants.EINTR) { - continue; - } else { - if (isRunning()) logError("readPacket error: ", e); - break; - } - } catch (Exception e) { - if (isRunning()) logError("readPacket error: ", e); - break; - } - - try { - handlePacket(mPacket, bytesRead); - } catch (Exception e) { - logError("handlePacket error: ", e); - break; - } - } - - return false; - } - - private void unregisterAndDestroyFd() { - if (mFd == null) return; - - mQueue.removeOnFileDescriptorEventListener(mFd); - closeFd(mFd); - mFd = null; - onStop(); - } - - private boolean onCorrectThread() { - return (mHandler.getLooper() == Looper.myLooper()); - } -} diff --git a/android/net/util/PacketReader.java b/android/net/util/PacketReader.java new file mode 100644 index 00000000..10da2a55 --- /dev/null +++ b/android/net/util/PacketReader.java @@ -0,0 +1,251 @@ +/* + * 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.net.util; + +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; + +import android.annotation.Nullable; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.os.MessageQueue.OnFileDescriptorEventListener; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import libcore.io.IoUtils; + +import java.io.FileDescriptor; +import java.io.IOException; + + +/** + * This class encapsulates the mechanics of registering a file descriptor + * with a thread's Looper and handling read events (and errors). + * + * Subclasses MUST implement createFd() and SHOULD override handlePacket(). + + * Subclasses can expect a call life-cycle like the following: + * + * [1] start() calls createFd() and (if all goes well) onStart() + * + * [2] yield, waiting for read event or error notification: + * + * [a] readPacket() && handlePacket() + * + * [b] if (no error): + * goto 2 + * else: + * goto 3 + * + * [3] stop() calls onStop() if not previously stopped + * + * The packet receive buffer is recycled on every read call, so subclasses + * should make any copies they would like inside their handlePacket() + * implementation. + * + * All public methods MUST only be called from the same thread with which + * the Handler constructor argument is associated. + * + * TODO: rename this class to something more correctly descriptive (something + * like [or less horrible than] FdReadEventsHandler?). + * + * @hide + */ +public abstract class PacketReader { + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int UNREGISTER_THIS_FD = 0; + + public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024; + + private final Handler mHandler; + private final MessageQueue mQueue; + private final byte[] mPacket; + private FileDescriptor mFd; + private long mPacketsReceived; + + protected static void closeFd(FileDescriptor fd) { + IoUtils.closeQuietly(fd); + } + + protected PacketReader(Handler h) { + this(h, DEFAULT_RECV_BUF_SIZE); + } + + protected PacketReader(Handler h, int recvbufsize) { + mHandler = h; + mQueue = mHandler.getLooper().getQueue(); + mPacket = new byte[Math.max(recvbufsize, DEFAULT_RECV_BUF_SIZE)]; + } + + public final void start() { + if (onCorrectThread()) { + createAndRegisterFd(); + } else { + mHandler.post(() -> { + logError("start() called from off-thread", null); + createAndRegisterFd(); + }); + } + } + + public final void stop() { + if (onCorrectThread()) { + unregisterAndDestroyFd(); + } else { + mHandler.post(() -> { + logError("stop() called from off-thread", null); + unregisterAndDestroyFd(); + }); + } + } + + public Handler getHandler() { return mHandler; } + + public final int recvBufSize() { return mPacket.length; } + + public final long numPacketsReceived() { return mPacketsReceived; } + + /** + * Subclasses MUST create the listening socket here, including setting + * all desired socket options, interface or address/port binding, etc. + */ + protected abstract FileDescriptor createFd(); + + /** + * Subclasses MAY override this to change the default read() implementation + * in favour of, say, recvfrom(). + * + * Implementations MUST return the bytes read or throw an Exception. + */ + protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { + return Os.read(fd, packetBuffer, 0, packetBuffer.length); + } + + /** + * Called by the main loop for every packet. Any desired copies of + * |recvbuf| should be made in here, as the underlying byte array is + * reused across all reads. + */ + protected void handlePacket(byte[] recvbuf, int length) {} + + /** + * Called by the main loop to log errors. In some cases |e| may be null. + */ + protected void logError(String msg, Exception e) {} + + /** + * Called by start(), if successful, just prior to returning. + */ + protected void onStart() {} + + /** + * Called by stop() just prior to returning. + */ + protected void onStop() {} + + private void createAndRegisterFd() { + if (mFd != null) return; + + try { + mFd = createFd(); + if (mFd != null) { + // Force the socket to be non-blocking. + IoUtils.setBlocking(mFd, false); + } + } catch (Exception e) { + logError("Failed to create socket: ", e); + closeFd(mFd); + mFd = null; + return; + } + + if (mFd == null) return; + + mQueue.addOnFileDescriptorEventListener( + mFd, + FD_EVENTS, + new OnFileDescriptorEventListener() { + @Override + public int onFileDescriptorEvents(FileDescriptor fd, int events) { + // Always call handleInput() so read/recvfrom are given + // a proper chance to encounter a meaningful errno and + // perhaps log a useful error message. + if (!isRunning() || !handleInput()) { + unregisterAndDestroyFd(); + return UNREGISTER_THIS_FD; + } + return FD_EVENTS; + } + }); + onStart(); + } + + private boolean isRunning() { return (mFd != null) && mFd.valid(); } + + // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. + private boolean handleInput() { + while (isRunning()) { + final int bytesRead; + + try { + bytesRead = readPacket(mFd, mPacket); + if (bytesRead < 1) { + if (isRunning()) logError("Socket closed, exiting", null); + break; + } + mPacketsReceived++; + } catch (ErrnoException e) { + if (e.errno == OsConstants.EAGAIN) { + // We've read everything there is to read this time around. + return true; + } else if (e.errno == OsConstants.EINTR) { + continue; + } else { + if (isRunning()) logError("readPacket error: ", e); + break; + } + } catch (Exception e) { + if (isRunning()) logError("readPacket error: ", e); + break; + } + + try { + handlePacket(mPacket, bytesRead); + } catch (Exception e) { + logError("handlePacket error: ", e); + break; + } + } + + return false; + } + + private void unregisterAndDestroyFd() { + if (mFd == null) return; + + mQueue.removeOnFileDescriptorEventListener(mFd); + closeFd(mFd); + mFd = null; + onStop(); + } + + private boolean onCorrectThread() { + return (mHandler.getLooper() == Looper.myLooper()); + } +} diff --git a/android/net/wifi/WifiInfo.java b/android/net/wifi/WifiInfo.java index bf8fed1c..3eb13ce6 100644 --- a/android/net/wifi/WifiInfo.java +++ b/android/net/wifi/WifiInfo.java @@ -22,7 +22,6 @@ import android.net.NetworkInfo.DetailedState; import android.net.NetworkUtils; import android.text.TextUtils; -import java.lang.Math; import java.net.InetAddress; import java.net.Inet4Address; import java.net.UnknownHostException; @@ -126,143 +125,35 @@ public class WifiInfo implements Parcelable { public long rxSuccess; /** - * Average rate of lost transmitted packets, in units of packets per 5 seconds. + * Average rate of lost transmitted packets, in units of packets per second. * @hide */ public double txBadRate; /** - * Average rate of transmitted retry packets, in units of packets per 5 seconds. + * Average rate of transmitted retry packets, in units of packets per second. * @hide */ public double txRetriesRate; /** - * Average rate of successfully transmitted unicast packets, in units of packets per 5 seconds. + * Average rate of successfully transmitted unicast packets, in units of packets per second. * @hide */ public double txSuccessRate; /** - * Average rate of received unicast data packets, in units of packets per 5 seconds. + * Average rate of received unicast data packets, in units of packets per second. * @hide */ public double rxSuccessRate; - private static final long RESET_TIME_STAMP = Long.MIN_VALUE; - private static final long FILTER_TIME_CONSTANT = 3000; - /** - * This factor is used to adjust the rate output under the new algorithm - * such that the result is comparable to the previous algorithm. - * This actually converts from unit 'packets per second' to 'packets per 5 seconds'. - */ - private static final long OUTPUT_SCALE_FACTOR = 5; - private long mLastPacketCountUpdateTimeStamp; - - /** - * @hide - */ - public int badRssiCount; - - /** - * @hide - */ - public int linkStuckCount; - - /** - * @hide - */ - public int lowRssiCount; - /** * @hide */ public int score; /** - * @hide - */ - public void updatePacketRates(WifiLinkLayerStats stats, long timeStamp) { - if (stats != null) { - long txgood = stats.txmpdu_be + stats.txmpdu_bk + stats.txmpdu_vi + stats.txmpdu_vo; - long txretries = stats.retries_be + stats.retries_bk - + stats.retries_vi + stats.retries_vo; - long rxgood = stats.rxmpdu_be + stats.rxmpdu_bk + stats.rxmpdu_vi + stats.rxmpdu_vo; - long txbad = stats.lostmpdu_be + stats.lostmpdu_bk - + stats.lostmpdu_vi + stats.lostmpdu_vo; - - if (mLastPacketCountUpdateTimeStamp != RESET_TIME_STAMP - && mLastPacketCountUpdateTimeStamp < timeStamp - && txBad <= txbad - && txSuccess <= txgood - && rxSuccess <= rxgood - && txRetries <= txretries) { - long timeDelta = timeStamp - mLastPacketCountUpdateTimeStamp; - double lastSampleWeight = Math.exp(-1.0 * timeDelta / FILTER_TIME_CONSTANT); - double currentSampleWeight = 1.0 - lastSampleWeight; - - txBadRate = txBadRate * lastSampleWeight - + (txbad - txBad) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta - * currentSampleWeight; - txSuccessRate = txSuccessRate * lastSampleWeight - + (txgood - txSuccess) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta - * currentSampleWeight; - rxSuccessRate = rxSuccessRate * lastSampleWeight - + (rxgood - rxSuccess) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta - * currentSampleWeight; - txRetriesRate = txRetriesRate * lastSampleWeight - + (txretries - txRetries) * OUTPUT_SCALE_FACTOR * 1000/ timeDelta - * currentSampleWeight; - } else { - txBadRate = 0; - txSuccessRate = 0; - rxSuccessRate = 0; - txRetriesRate = 0; - } - txBad = txbad; - txSuccess = txgood; - rxSuccess = rxgood; - txRetries = txretries; - mLastPacketCountUpdateTimeStamp = timeStamp; - } else { - txBad = 0; - txSuccess = 0; - rxSuccess = 0; - txRetries = 0; - txBadRate = 0; - txSuccessRate = 0; - rxSuccessRate = 0; - txRetriesRate = 0; - mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP; - } - } - - - /** - * This function is less powerful and used if the WifiLinkLayerStats API is not implemented - * at the Wifi HAL - * @hide + * Flag indicating that AP has hinted that upstream connection is metered, + * and sensitive to heavy data transfers. */ - public void updatePacketRates(long txPackets, long rxPackets) { - //paranoia - txBad = 0; - txRetries = 0; - txBadRate = 0; - txRetriesRate = 0; - if (txSuccess <= txPackets && rxSuccess <= rxPackets) { - txSuccessRate = (txSuccessRate * 0.5) - + ((double) (txPackets - txSuccess) * 0.5); - rxSuccessRate = (rxSuccessRate * 0.5) - + ((double) (rxPackets - rxSuccess) * 0.5); - } else { - txBadRate = 0; - txRetriesRate = 0; - } - txSuccess = txPackets; - rxSuccess = rxPackets; - } - - /** - * Flag indicating that AP has hinted that upstream connection is metered, - * and sensitive to heavy data transfers. - */ private boolean mMeteredHint; /** @hide */ @@ -274,7 +165,6 @@ public class WifiInfo implements Parcelable { mRssi = INVALID_RSSI; mLinkSpeed = -1; mFrequency = -1; - mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP; } /** @hide */ @@ -296,11 +186,7 @@ public class WifiInfo implements Parcelable { txSuccessRate = 0; rxSuccessRate = 0; txRetriesRate = 0; - lowRssiCount = 0; - badRssiCount = 0; - linkStuckCount = 0; score = 0; - mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP; } /** @@ -328,12 +214,7 @@ public class WifiInfo implements Parcelable { txRetriesRate = source.txRetriesRate; txSuccessRate = source.txSuccessRate; rxSuccessRate = source.rxSuccessRate; - mLastPacketCountUpdateTimeStamp = - source.mLastPacketCountUpdateTimeStamp; score = source.score; - badRssiCount = source.badRssiCount; - lowRssiCount = source.lowRssiCount; - linkStuckCount = source.linkStuckCount; } } @@ -451,22 +332,6 @@ public class WifiInfo implements Parcelable { return ScanResult.is5GHz(mFrequency); } - /** - * @hide - * This returns txSuccessRate in packets per second. - */ - public double getTxSuccessRatePps() { - return txSuccessRate / OUTPUT_SCALE_FACTOR; - } - - /** - * @hide - * This returns rxSuccessRate in packets per second. - */ - public double getRxSuccessRatePps() { - return rxSuccessRate / OUTPUT_SCALE_FACTOR; - } - /** * Record the MAC address of the WLAN interface * @param macAddress the MAC address in {@code XX:XX:XX:XX:XX:XX} form @@ -658,8 +523,6 @@ public class WifiInfo implements Parcelable { dest.writeDouble(txRetriesRate); dest.writeDouble(txBadRate); dest.writeDouble(rxSuccessRate); - dest.writeInt(badRssiCount); - dest.writeInt(lowRssiCount); mSupplicantState.writeToParcel(dest, flags); } @@ -689,8 +552,6 @@ public class WifiInfo implements Parcelable { info.txRetriesRate = in.readDouble(); info.txBadRate = in.readDouble(); info.rxSuccessRate = in.readDouble(); - info.badRssiCount = in.readInt(); - info.lowRssiCount = in.readInt(); info.mSupplicantState = SupplicantState.CREATOR.createFromParcel(in); return info; } diff --git a/android/net/wifi/WifiLinkLayerStats.java b/android/net/wifi/WifiLinkLayerStats.java deleted file mode 100644 index edd400b5..00000000 --- a/android/net/wifi/WifiLinkLayerStats.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.net.wifi; - -import android.os.Parcelable; -import android.os.Parcel; - -import java.util.Arrays; - -/** - * A class representing link layer statistics collected over a Wifi Interface. - */ -/** {@hide} */ -public class WifiLinkLayerStats implements Parcelable { - private static final String TAG = "WifiLinkLayerStats"; - - /** - * The current status of this network configuration entry. - * @see Status - */ - /** {@hide} */ - public int status; - - /** - * The network's SSID. Can either be an ASCII string, - * which must be enclosed in double quotation marks - * (e.g., {@code "MyNetwork"}, or a string of - * hex digits,which are not enclosed in quotes - * (e.g., {@code 01a243f405}). - */ - /** {@hide} */ - public String SSID; - /** - * When set. this is the BSSID the radio is currently associated with. - * The value is a string in the format of an Ethernet MAC address, e.g., - * XX:XX:XX:XX:XX:XX where each X is a hex digit. - */ - /** {@hide} */ - public String BSSID; - - /* number beacons received from our own AP */ - /** {@hide} */ - public int beacon_rx; - - /* RSSI taken on management frames */ - /** {@hide} */ - public int rssi_mgmt; - - /* packets counters */ - /** {@hide} */ - /* WME Best Effort Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries)*/ - public long rxmpdu_be; - /** {@hide} */ - public long txmpdu_be; - /** {@hide} */ - public long lostmpdu_be; - /** {@hide} */ - public long retries_be; - /** {@hide} */ - /* WME Background Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */ - public long rxmpdu_bk; - /** {@hide} */ - public long txmpdu_bk; - /** {@hide} */ - public long lostmpdu_bk; - /** {@hide} */ - public long retries_bk; - /** {@hide} */ - /* WME Video Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */ - public long rxmpdu_vi; - /** {@hide} */ - public long txmpdu_vi; - /** {@hide} */ - public long lostmpdu_vi; - /** {@hide} */ - public long retries_vi; - /** {@hide} */ - /* WME Voice Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */ - public long rxmpdu_vo; - /** {@hide} */ - public long txmpdu_vo; - /** {@hide} */ - public long lostmpdu_vo; - /** {@hide} */ - public long retries_vo; - - /** {@hide} */ - public int on_time; - /** {@hide} */ - public int tx_time; - /** {@hide} */ - public int[] tx_time_per_level; - /** {@hide} */ - public int rx_time; - /** {@hide} */ - public int on_time_scan; - - /** {@hide} */ - public WifiLinkLayerStats() { - } - - @Override - /** {@hide} */ - public String toString() { - StringBuilder sbuf = new StringBuilder(); - sbuf.append(" WifiLinkLayerStats: ").append('\n'); - - if (this.SSID != null) { - sbuf.append(" SSID: ").append(this.SSID).append('\n'); - } - if (this.BSSID != null) { - sbuf.append(" BSSID: ").append(this.BSSID).append('\n'); - } - - sbuf.append(" my bss beacon rx: ").append(Integer.toString(this.beacon_rx)).append('\n'); - sbuf.append(" RSSI mgmt: ").append(Integer.toString(this.rssi_mgmt)).append('\n'); - sbuf.append(" BE : ").append(" rx=").append(Long.toString(this.rxmpdu_be)) - .append(" tx=").append(Long.toString(this.txmpdu_be)) - .append(" lost=").append(Long.toString(this.lostmpdu_be)) - .append(" retries=").append(Long.toString(this.retries_be)).append('\n'); - sbuf.append(" BK : ").append(" rx=").append(Long.toString(this.rxmpdu_bk)) - .append(" tx=").append(Long.toString(this.txmpdu_bk)) - .append(" lost=").append(Long.toString(this.lostmpdu_bk)) - .append(" retries=").append(Long.toString(this.retries_bk)).append('\n'); - sbuf.append(" VI : ").append(" rx=").append(Long.toString(this.rxmpdu_vi)) - .append(" tx=").append(Long.toString(this.txmpdu_vi)) - .append(" lost=").append(Long.toString(this.lostmpdu_vi)) - .append(" retries=").append(Long.toString(this.retries_vi)).append('\n'); - sbuf.append(" VO : ").append(" rx=").append(Long.toString(this.rxmpdu_vo)) - .append(" tx=").append(Long.toString(this.txmpdu_vo)) - .append(" lost=").append(Long.toString(this.lostmpdu_vo)) - .append(" retries=").append(Long.toString(this.retries_vo)).append('\n'); - sbuf.append(" on_time : ").append(Integer.toString(this.on_time)) - .append(" rx_time=").append(Integer.toString(this.rx_time)) - .append(" scan_time=").append(Integer.toString(this.on_time_scan)).append('\n') - .append(" tx_time=").append(Integer.toString(this.tx_time)) - .append(" tx_time_per_level=" + Arrays.toString(tx_time_per_level)); - return sbuf.toString(); - } - - /** Implement the Parcelable interface {@hide} */ - public int describeContents() { - return 0; - } - - /** {@hide} */ - public String getPrintableSsid() { - if (SSID == null) return ""; - final int length = SSID.length(); - if (length > 2 && (SSID.charAt(0) == '"') && SSID.charAt(length - 1) == '"') { - return SSID.substring(1, length - 1); - } - - /** The ascii-encoded string format is P"" - * The decoding is implemented in the supplicant for a newly configured - * network. - */ - if (length > 3 && (SSID.charAt(0) == 'P') && (SSID.charAt(1) == '"') && - (SSID.charAt(length-1) == '"')) { - WifiSsid wifiSsid = WifiSsid.createFromAsciiEncoded( - SSID.substring(2, length - 1)); - return wifiSsid.toString(); - } - return SSID; - } - - /** Implement the Parcelable interface {@hide} */ - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(SSID); - dest.writeString(BSSID); - dest.writeInt(on_time); - dest.writeInt(tx_time); - dest.writeIntArray(tx_time_per_level); - dest.writeInt(rx_time); - dest.writeInt(on_time_scan); - } - - /** Implement the Parcelable interface {@hide} */ - public static final Creator CREATOR = - new Creator() { - public WifiLinkLayerStats createFromParcel(Parcel in) { - WifiLinkLayerStats stats = new WifiLinkLayerStats(); - stats.SSID = in.readString(); - stats.BSSID = in.readString(); - stats.on_time = in.readInt(); - stats.tx_time = in.readInt(); - stats.tx_time_per_level = in.createIntArray(); - stats.rx_time = in.readInt(); - stats.on_time_scan = in.readInt(); - return stats; - }; - public WifiLinkLayerStats[] newArray(int size) { - return new WifiLinkLayerStats[size]; - } - - }; -} diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java index 558004ce..ea9be290 100644 --- a/android/net/wifi/WifiManager.java +++ b/android/net/wifi/WifiManager.java @@ -2846,8 +2846,7 @@ public class WifiManager { * gets added to the list of configured networks for the foreground user. * * For a new network, this function is used instead of a - * sequence of addNetwork(), enableNetwork(), saveConfiguration() and - * reconnect() + * sequence of addNetwork(), enableNetwork(), and reconnect() * * @param config the set of variables that describe the configuration, * contained in a {@link WifiConfiguration} object. @@ -2869,8 +2868,7 @@ public class WifiManager { /** * Connect to a network with the given networkId. * - * This function is used instead of a enableNetwork(), saveConfiguration() and - * reconnect() + * This function is used instead of a enableNetwork() and reconnect() * * @param networkId the ID of the network as returned by {@link #addNetwork} or {@link * getConfiguredNetworks}. @@ -2890,10 +2888,12 @@ public class WifiManager { * is updated. Any new network is enabled by default. * * For a new network, this function is used instead of a - * sequence of addNetwork(), enableNetwork() and saveConfiguration(). + * sequence of addNetwork() and enableNetwork(). * * For an existing network, it accomplishes the task of updateNetwork() - * and saveConfiguration() + * + * This API will cause reconnect if the crecdentials of the current active + * connection has been changed. * * @param config the set of variables that describe the configuration, * contained in a {@link WifiConfiguration} object. @@ -2912,7 +2912,6 @@ public class WifiManager { * foreground user. * * This function is used instead of a sequence of removeNetwork() - * and saveConfiguration(). * * @param config the set of variables that describe the configuration, * contained in a {@link WifiConfiguration} object. @@ -3488,27 +3487,23 @@ public class WifiManager { } /** - * Set setting for allowing Scans when traffic is ongoing. + * Deprecated + * Does nothing * @hide + * @deprecated */ public void setAllowScansWithTraffic(int enabled) { - try { - mService.setAllowScansWithTraffic(enabled); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return; } /** - * Get setting for allowing Scans when traffic is ongoing. + * Deprecated + * returns value for 'disabled' * @hide + * @deprecated */ public int getAllowScansWithTraffic() { - try { - return mService.getAllowScansWithTraffic(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return 0; } /** @@ -3538,29 +3533,23 @@ public class WifiManager { } /** - * Framework layer autojoin enable/disable when device is associated - * this will enable/disable autojoin scan and switch network when connected - * @return true -- if set successful false -- if set failed + * Deprecated + * returns false * @hide + * @deprecated */ public boolean setEnableAutoJoinWhenAssociated(boolean enabled) { - try { - return mService.setEnableAutoJoinWhenAssociated(enabled); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return false; } /** - * Get setting for Framework layer autojoin enable status + * Deprecated + * returns false * @hide + * @deprecated */ public boolean getEnableAutoJoinWhenAssociated() { - try { - return mService.getEnableAutoJoinWhenAssociated(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return false; } /** diff --git a/android/net/wifi/WifiScanner.java b/android/net/wifi/WifiScanner.java index e3752ac7..928a1da8 100644 --- a/android/net/wifi/WifiScanner.java +++ b/android/net/wifi/WifiScanner.java @@ -160,6 +160,24 @@ public class WifiScanner { */ public static final int REPORT_EVENT_NO_BATCH = (1 << 2); + /** + * This is used to indicate the purpose of the scan to the wifi chip in + * {@link ScanSettings#type}. + * On devices with multiple hardware radio chains (and hence different modes of scan), + * this type serves as an indication to the hardware on what mode of scan to perform. + * Only apps holding android.Manifest.permission.NETWORK_STACK permission can set this value. + * + * Note: This serves as an intent and not as a stipulation, the wifi chip + * might honor or ignore the indication based on the current radio conditions. Always + * use the {@link ScanResult#radioChainInfos} to figure out the radio chain configuration used + * to receive the corresponding scan result. + */ + /** {@hide} */ + public static final int TYPE_LOW_LATENCY = 0; + /** {@hide} */ + public static final int TYPE_LOW_POWER = 1; + /** {@hide} */ + public static final int TYPE_HIGH_ACCURACY = 2; /** {@hide} */ public static final String SCAN_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings"; @@ -193,7 +211,8 @@ public class WifiScanner { * list of hidden networks to scan for. Explicit probe requests are sent out for such * networks during scan. Only valid for single scan requests. * {@hide} - * */ + */ + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public HiddenNetwork[] hiddenNetworks; /** period of background scan; in millisecond, 0 => single shot scan */ public int periodInMs; @@ -223,6 +242,13 @@ public class WifiScanner { * {@hide} */ public boolean isPnoScan; + /** + * Indicate the type of scan to be performed by the wifi chip. + * Default value: {@link #TYPE_LOW_LATENCY}. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + public int type = TYPE_LOW_LATENCY; /** Implement the Parcelable interface {@hide} */ public int describeContents() { @@ -239,6 +265,7 @@ public class WifiScanner { dest.writeInt(maxPeriodInMs); dest.writeInt(stepCount); dest.writeInt(isPnoScan ? 1 : 0); + dest.writeInt(type); if (channels != null) { dest.writeInt(channels.length); for (int i = 0; i < channels.length; i++) { @@ -272,6 +299,7 @@ public class WifiScanner { settings.maxPeriodInMs = in.readInt(); settings.stepCount = in.readInt(); settings.isPnoScan = in.readInt() == 1; + settings.type = in.readInt(); int num_channels = in.readInt(); settings.channels = new ChannelSpec[num_channels]; for (int i = 0; i < num_channels; i++) { diff --git a/android/net/wifi/aware/PeerHandle.java b/android/net/wifi/aware/PeerHandle.java index 1b0aba15..b525212e 100644 --- a/android/net/wifi/aware/PeerHandle.java +++ b/android/net/wifi/aware/PeerHandle.java @@ -33,7 +33,6 @@ public class PeerHandle { /** @hide */ public int peerId; - /** @hide RTT_API */ @Override public boolean equals(Object o) { if (this == o) { @@ -47,7 +46,6 @@ public class PeerHandle { return peerId == ((PeerHandle) o).peerId; } - /** @hide RTT_API */ @Override public int hashCode() { return peerId; diff --git a/android/net/wifi/aware/PublishConfig.java b/android/net/wifi/aware/PublishConfig.java index e60f52f8..7a5049d7 100644 --- a/android/net/wifi/aware/PublishConfig.java +++ b/android/net/wifi/aware/PublishConfig.java @@ -182,7 +182,7 @@ public final class PublishConfig implements Parcelable { * * @hide */ - public void assertValid(Characteristics characteristics) + public void assertValid(Characteristics characteristics, boolean rttSupported) throws IllegalArgumentException { WifiAwareUtils.validateServiceName(mServiceName); @@ -216,6 +216,10 @@ public final class PublishConfig implements Parcelable { "Match filter longer than supported by device characteristics"); } } + + if (!rttSupported && mEnableRanging) { + throw new IllegalArgumentException("Ranging is not supported"); + } } /** @@ -364,6 +368,9 @@ public final class PublishConfig implements Parcelable { * Optional. Disabled by default - i.e. any peer which attempts to measure distance to this * device will be refused. If the peer has ranging enabled (using the * {@link SubscribeConfig} APIs listed above, it will never discover this device. + *

      + * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked + * as described in {@link android.net.wifi.rtt}. * * @param enable If true, ranging is supported on request of the peer. * diff --git a/android/net/wifi/aware/SubscribeConfig.java b/android/net/wifi/aware/SubscribeConfig.java index f6552a76..91f8e520 100644 --- a/android/net/wifi/aware/SubscribeConfig.java +++ b/android/net/wifi/aware/SubscribeConfig.java @@ -224,7 +224,7 @@ public final class SubscribeConfig implements Parcelable { * * @hide */ - public void assertValid(Characteristics characteristics) + public void assertValid(Characteristics characteristics, boolean rttSupported) throws IllegalArgumentException { WifiAwareUtils.validateServiceName(mServiceName); @@ -269,6 +269,10 @@ public final class SubscribeConfig implements Parcelable { throw new IllegalArgumentException( "Maximum distance must be greater than minimum distance"); } + + if (!rttSupported && (mMinDistanceMmSet || mMaxDistanceMmSet)) { + throw new IllegalArgumentException("Ranging is not supported"); + } } /** @@ -422,6 +426,9 @@ public final class SubscribeConfig implements Parcelable { * peer must enable ranging using * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. Otherwise discovery will * never be triggered. + *

      + * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked + * as described in {@link android.net.wifi.rtt}. * * @param minDistanceMm Minimum distance, in mm, to the publisher above which to trigger * discovery. @@ -450,6 +457,9 @@ public final class SubscribeConfig implements Parcelable { * peer must enable ranging using * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. Otherwise discovery will * never be triggered. + *

      + * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked + * as described in {@link android.net.wifi.rtt}. * * @param maxDistanceMm Maximum distance, in mm, to the publisher below which to trigger * discovery. diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java index 166da48e..d57d1524 100644 --- a/android/net/wifi/aware/WifiAwareManager.java +++ b/android/net/wifi/aware/WifiAwareManager.java @@ -301,7 +301,7 @@ public class WifiAwareManager { if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig); try { - mService.publish(clientId, publishConfig, + mService.publish(mContext.getOpPackageName(), clientId, publishConfig, new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback, clientId)); } catch (RemoteException e) { @@ -334,7 +334,7 @@ public class WifiAwareManager { } try { - mService.subscribe(clientId, subscribeConfig, + mService.subscribe(mContext.getOpPackageName(), clientId, subscribeConfig, new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback, clientId)); } catch (RemoteException e) { diff --git a/android/net/wifi/hotspot2/ProvisioningCallback.java b/android/net/wifi/hotspot2/ProvisioningCallback.java index 8b86cdde..2ea6e797 100644 --- a/android/net/wifi/hotspot2/ProvisioningCallback.java +++ b/android/net/wifi/hotspot2/ProvisioningCallback.java @@ -26,12 +26,48 @@ import android.os.Handler; */ public abstract class ProvisioningCallback { - /** + /** * The reason code for Provisioning Failure due to connection failure to OSU AP. * @hide */ public static final int OSU_FAILURE_AP_CONNECTION = 1; + /** + * The reason code for Provisioning Failure due to connection failure to OSU AP. + * @hide + */ + public static final int OSU_FAILURE_SERVER_URL_INVALID = 2; + + /** + * The reason code for Provisioning Failure due to connection failure to OSU AP. + * @hide + */ + public static final int OSU_FAILURE_SERVER_CONNECTION = 3; + + /** + * The reason code for Provisioning Failure due to connection failure to OSU AP. + * @hide + */ + public static final int OSU_FAILURE_SERVER_VALIDATION = 4; + + /** + * The reason code for Provisioning Failure due to connection failure to OSU AP. + * @hide + */ + public static final int OSU_FAILURE_PROVIDER_VERIFICATION = 5; + + /** + * The reason code for Provisioning Failure when a provisioning flow is aborted. + * @hide + */ + public static final int OSU_FAILURE_PROVISIONING_ABORTED = 6; + + /** + * The reason code for Provisioning Failure when a provisioning flow is aborted. + * @hide + */ + public static final int OSU_FAILURE_PROVISIONING_NOT_AVAILABLE = 7; + /** * The status code for Provisioning flow to indicate connecting to OSU AP * @hide @@ -44,6 +80,24 @@ public abstract class ProvisioningCallback { */ public static final int OSU_STATUS_AP_CONNECTED = 2; + /** + * The status code for Provisioning flow to indicate connecting to OSU AP + * @hide + */ + public static final int OSU_STATUS_SERVER_CONNECTED = 3; + + /** + * The status code for Provisioning flow to indicate connecting to OSU AP + * @hide + */ + public static final int OSU_STATUS_SERVER_VALIDATED = 4; + + /** + * The status code for Provisioning flow to indicate connecting to OSU AP + * @hide + */ + public static final int OSU_STATUS_PROVIDER_VERIFIED = 5; + /** * Provisioning status for OSU failure * @param status indicates error condition diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java index a396281f..b4e3097a 100644 --- a/android/net/wifi/rtt/RangingRequest.java +++ b/android/net/wifi/rtt/RangingRequest.java @@ -16,6 +16,8 @@ package android.net.wifi.rtt; +import android.annotation.NonNull; +import android.net.MacAddress; import android.net.wifi.ScanResult; import android.net.wifi.aware.AttachCallback; import android.net.wifi.aware.DiscoverySessionCallback; @@ -25,14 +27,9 @@ import android.net.wifi.aware.WifiAwareManager; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; - -import libcore.util.HexEncoding; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.StringJoiner; /** @@ -63,10 +60,10 @@ public final class RangingRequest implements Parcelable { } /** @hide */ - public final List mRttPeers; + public final List mRttPeers; /** @hide */ - private RangingRequest(List rttPeers) { + private RangingRequest(List rttPeers) { mRttPeers = rttPeers; } @@ -95,9 +92,9 @@ public final class RangingRequest implements Parcelable { /** @hide */ @Override public String toString() { - StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", ","); - for (RttPeer rp : mRttPeers) { - sj.add(rp.toString()); + StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", "]"); + for (ResponderConfig rc : mRttPeers) { + sj.add(rc.toString()); } return sj.toString(); } @@ -109,26 +106,9 @@ public final class RangingRequest implements Parcelable { "Ranging to too many peers requested. Use getMaxPeers() API to get limit."); } - for (RttPeer peer: mRttPeers) { - if (peer instanceof RttPeerAp) { - RttPeerAp apPeer = (RttPeerAp) peer; - if (apPeer.scanResult == null || apPeer.scanResult.BSSID == null) { - throw new IllegalArgumentException("Invalid AP peer specification"); - } - } else if (peer instanceof RttPeerAware) { - if (!awareSupported) { - throw new IllegalArgumentException( - "Request contains Aware peers - but Aware isn't supported on this " - + "device"); - } - - RttPeerAware awarePeer = (RttPeerAware) peer; - if (awarePeer.peerMacAddress == null && awarePeer.peerHandle == null) { - throw new IllegalArgumentException("Invalid Aware peer specification"); - } - } else { - throw new IllegalArgumentException( - "Request contains unknown peer specification types"); + for (ResponderConfig peer: mRttPeers) { + if (!peer.isValid(awareSupported)) { + throw new IllegalArgumentException("Invalid Responder specification"); } } } @@ -137,35 +117,34 @@ public final class RangingRequest implements Parcelable { * Builder class used to construct {@link RangingRequest} objects. */ public static final class Builder { - private List mRttPeers = new ArrayList<>(); + private List mRttPeers = new ArrayList<>(); /** * Add the device specified by the {@link ScanResult} to the list of devices with - * which to measure range. The total number of results added to a request cannot exceed the + * which to measure range. The total number of peers added to a request cannot exceed the * limit specified by {@link #getMaxPeers()}. * * @param apInfo Information of an Access Point (AP) obtained in a Scan Result. * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addAccessPoint(ScanResult apInfo) { + public Builder addAccessPoint(@NonNull ScanResult apInfo) { if (apInfo == null) { throw new IllegalArgumentException("Null ScanResult!"); } - mRttPeers.add(new RttPeerAp(apInfo)); - return this; + return addResponder(ResponderConfig.fromScanResult(apInfo)); } /** * Add the devices specified by the {@link ScanResult}s to the list of devices with - * which to measure range. The total number of results added to a request cannot exceed the + * which to measure range. The total number of peers added to a request cannot exceed the * limit specified by {@link #getMaxPeers()}. * * @param apInfos Information of an Access Points (APs) obtained in a Scan Result. * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addAccessPoints(List apInfos) { + public Builder addAccessPoints(@NonNull List apInfos) { if (apInfos == null) { throw new IllegalArgumentException("Null list of ScanResults!"); } @@ -190,9 +169,12 @@ public final class RangingRequest implements Parcelable { * @param peerMacAddress The MAC address of the Wi-Fi Aware peer. * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addWifiAwarePeer(byte[] peerMacAddress) { - mRttPeers.add(new RttPeerAware(peerMacAddress)); - return this; + public Builder addWifiAwarePeer(@NonNull MacAddress peerMacAddress) { + if (peerMacAddress == null) { + throw new IllegalArgumentException("Null peer MAC address"); + } + return addResponder( + ResponderConfig.fromWifiAwarePeerMacAddressWithDefaults(peerMacAddress)); } /** @@ -208,8 +190,30 @@ public final class RangingRequest implements Parcelable { * @param peerHandle The peer handler of the peer Wi-Fi Aware device. * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addWifiAwarePeer(PeerHandle peerHandle) { - mRttPeers.add(new RttPeerAware(peerHandle)); + public Builder addWifiAwarePeer(@NonNull PeerHandle peerHandle) { + if (peerHandle == null) { + throw new IllegalArgumentException("Null peer handler (identifier)"); + } + + return addResponder(ResponderConfig.fromWifiAwarePeerHandleWithDefaults(peerHandle)); + } + + /* + * Add the Responder device specified by the {@link ResponderConfig} to the list of devices + * with which to measure range. The total number of peers added to the request cannot exceed + * the limit specified by {@link #getMaxPeers()}. + * + * @param responder Information on the RTT Responder. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + * + * @hide (SystemApi) + */ + public Builder addResponder(@NonNull ResponderConfig responder) { + if (responder == null) { + throw new IllegalArgumentException("Null Responder!"); + } + + mRttPeers.add(responder); return this; } @@ -241,152 +245,4 @@ public final class RangingRequest implements Parcelable { public int hashCode() { return mRttPeers.hashCode(); } - - /** @hide */ - public interface RttPeer { - // empty (marker interface) - } - - /** @hide */ - public static class RttPeerAp implements RttPeer, Parcelable { - public final ScanResult scanResult; - - public RttPeerAp(ScanResult scanResult) { - this.scanResult = scanResult; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - scanResult.writeToParcel(dest, flags); - } - - public static final Creator CREATOR = new Creator() { - @Override - public RttPeerAp[] newArray(int size) { - return new RttPeerAp[size]; - } - - @Override - public RttPeerAp createFromParcel(Parcel in) { - return new RttPeerAp(ScanResult.CREATOR.createFromParcel(in)); - } - }; - - @Override - public String toString() { - return new StringBuilder("RttPeerAp: scanResult=").append( - scanResult.toString()).toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (!(o instanceof RttPeerAp)) { - return false; - } - - RttPeerAp lhs = (RttPeerAp) o; - - // Note: the only thing which matters for the request identity is the BSSID of the AP - return TextUtils.equals(scanResult.BSSID, lhs.scanResult.BSSID); - } - - @Override - public int hashCode() { - return scanResult.hashCode(); - } - } - - /** @hide */ - public static class RttPeerAware implements RttPeer, Parcelable { - public PeerHandle peerHandle; - public byte[] peerMacAddress; - - public RttPeerAware(PeerHandle peerHandle) { - if (peerHandle == null) { - throw new IllegalArgumentException("Null peerHandle"); - } - this.peerHandle = peerHandle; - peerMacAddress = null; - } - - public RttPeerAware(byte[] peerMacAddress) { - if (peerMacAddress == null) { - throw new IllegalArgumentException("Null peerMacAddress"); - } - - this.peerMacAddress = peerMacAddress; - peerHandle = null; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - if (peerHandle == null) { - dest.writeBoolean(false); - dest.writeByteArray(peerMacAddress); - } else { - dest.writeBoolean(true); - dest.writeInt(peerHandle.peerId); - } - } - - public static final Creator CREATOR = new Creator() { - @Override - public RttPeerAware[] newArray(int size) { - return new RttPeerAware[size]; - } - - @Override - public RttPeerAware createFromParcel(Parcel in) { - boolean peerHandleAvail = in.readBoolean(); - if (peerHandleAvail) { - return new RttPeerAware(new PeerHandle(in.readInt())); - } else { - return new RttPeerAware(in.createByteArray()); - } - } - }; - - @Override - public String toString() { - return new StringBuilder("RttPeerAware: peerHandle=").append( - peerHandle == null ? "" : Integer.toString(peerHandle.peerId)).append( - ", peerMacAddress=").append(peerMacAddress == null ? "" - : new String(HexEncoding.encode(peerMacAddress))).toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (!(o instanceof RttPeerAware)) { - return false; - } - - RttPeerAware lhs = (RttPeerAware) o; - - return Objects.equals(peerHandle, lhs.peerHandle) && Arrays.equals(peerMacAddress, - lhs.peerMacAddress); - } - - @Override - public int hashCode() { - return Objects.hash(peerHandle.peerId, peerMacAddress); - } - } } diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java index 93e52aeb..a380fae7 100644 --- a/android/net/wifi/rtt/RangingResult.java +++ b/android/net/wifi/rtt/RangingResult.java @@ -17,16 +17,15 @@ package android.net.wifi.rtt; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.net.MacAddress; import android.net.wifi.aware.PeerHandle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import libcore.util.HexEncoding; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -62,7 +61,7 @@ public final class RangingResult implements Parcelable { public static final int STATUS_FAIL = 1; private final int mStatus; - private final byte[] mMac; + private final MacAddress mMac; private final PeerHandle mPeerHandle; private final int mDistanceMm; private final int mDistanceStdDevMm; @@ -70,7 +69,7 @@ public final class RangingResult implements Parcelable { private final long mTimestamp; /** @hide */ - public RangingResult(@RangeResultStatus int status, byte[] mac, int distanceMm, + public RangingResult(@RangeResultStatus int status, @NonNull MacAddress mac, int distanceMm, int distanceStdDevMm, int rssi, long timestamp) { mStatus = status; mMac = mac; @@ -109,7 +108,7 @@ public final class RangingResult implements Parcelable { * Will return a {@code null} for results corresponding to requests issued using a {@code * PeerHandle}, i.e. using the {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)} API. */ - public byte[] getMacAddress() { + public MacAddress getMacAddress() { return mMac; } @@ -193,7 +192,12 @@ public final class RangingResult implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mStatus); - dest.writeByteArray(mMac); + if (mMac == null) { + dest.writeBoolean(false); + } else { + dest.writeBoolean(true); + mMac.writeToParcel(dest, flags); + } if (mPeerHandle == null) { dest.writeBoolean(false); } else { @@ -216,7 +220,11 @@ public final class RangingResult implements Parcelable { @Override public RangingResult createFromParcel(Parcel in) { int status = in.readInt(); - byte[] mac = in.createByteArray(); + boolean macAddressPresent = in.readBoolean(); + MacAddress mac = null; + if (macAddressPresent) { + mac = MacAddress.CREATOR.createFromParcel(in); + } boolean peerHandlePresent = in.readBoolean(); PeerHandle peerHandle = null; if (peerHandlePresent) { @@ -240,11 +248,11 @@ public final class RangingResult implements Parcelable { @Override public String toString() { return new StringBuilder("RangingResult: [status=").append(mStatus).append(", mac=").append( - mMac == null ? "" : new String(HexEncoding.encodeToString(mMac))).append( - ", peerHandle=").append(mPeerHandle == null ? "" : mPeerHandle.peerId).append( - ", distanceMm=").append(mDistanceMm).append(", distanceStdDevMm=").append( - mDistanceStdDevMm).append(", rssi=").append(mRssi).append(", timestamp=").append( - mTimestamp).append("]").toString(); + mMac).append(", peerHandle=").append( + mPeerHandle == null ? "" : mPeerHandle.peerId).append(", distanceMm=").append( + mDistanceMm).append(", distanceStdDevMm=").append(mDistanceStdDevMm).append( + ", rssi=").append(mRssi).append(", timestamp=").append(mTimestamp).append( + "]").toString(); } @Override @@ -259,7 +267,7 @@ public final class RangingResult implements Parcelable { RangingResult lhs = (RangingResult) o; - return mStatus == lhs.mStatus && Arrays.equals(mMac, lhs.mMac) && Objects.equals( + return mStatus == lhs.mStatus && Objects.equals(mMac, lhs.mMac) && Objects.equals( mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm && mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi && mTimestamp == lhs.mTimestamp; diff --git a/android/net/wifi/rtt/ResponderConfig.java b/android/net/wifi/rtt/ResponderConfig.java new file mode 100644 index 00000000..c3e10074 --- /dev/null +++ b/android/net/wifi/rtt/ResponderConfig.java @@ -0,0 +1,474 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.wifi.rtt; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.net.MacAddress; +import android.net.wifi.ScanResult; +import android.net.wifi.aware.PeerHandle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Defines the configuration of an IEEE 802.11mc Responder. The Responder may be an Access Point + * (AP), a Wi-Fi Aware device, or a manually configured Responder. + *

      + * A Responder configuration may be constructed from a {@link ScanResult} or manually (with the + * data obtained out-of-band from a peer). + * + * @hide (@SystemApi) + */ +public final class ResponderConfig implements Parcelable { + private static final int AWARE_BAND_2_DISCOVERY_CHANNEL = 2437; + + /** @hide */ + @IntDef({RESPONDER_AP, RESPONDER_STA, RESPONDER_P2P_GO, RESPONDER_P2P_CLIENT, RESPONDER_AWARE}) + @Retention(RetentionPolicy.SOURCE) + public @interface ResponderType { + } + + /** + * Responder is an AP. + */ + public static final int RESPONDER_AP = 0; + /** + * Responder is a STA. + */ + public static final int RESPONDER_STA = 1; + /** + * Responder is a Wi-Fi Direct Group Owner (GO). + */ + public static final int RESPONDER_P2P_GO = 2; + /** + * Responder is a Wi-Fi Direct Group Client. + */ + public static final int RESPONDER_P2P_CLIENT = 3; + /** + * Responder is a Wi-Fi Aware device. + */ + public static final int RESPONDER_AWARE = 4; + + + /** @hide */ + @IntDef({ + CHANNEL_WIDTH_20MHZ, CHANNEL_WIDTH_40MHZ, CHANNEL_WIDTH_80MHZ, CHANNEL_WIDTH_160MHZ, + CHANNEL_WIDTH_80MHZ_PLUS_MHZ}) + @Retention(RetentionPolicy.SOURCE) + public @interface ChannelWidth { + } + + /** + * Channel bandwidth is 20 MHZ + */ + public static final int CHANNEL_WIDTH_20MHZ = 0; + /** + * Channel bandwidth is 40 MHZ + */ + public static final int CHANNEL_WIDTH_40MHZ = 1; + /** + * Channel bandwidth is 80 MHZ + */ + public static final int CHANNEL_WIDTH_80MHZ = 2; + /** + * Channel bandwidth is 160 MHZ + */ + public static final int CHANNEL_WIDTH_160MHZ = 3; + /** + * Channel bandwidth is 160 MHZ, but 80MHZ + 80MHZ + */ + public static final int CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4; + + /** @hide */ + @IntDef({PREAMBLE_LEGACY, PREAMBLE_HT, PREAMBLE_VHT}) + @Retention(RetentionPolicy.SOURCE) + public @interface PreambleType { + } + + /** + * Preamble type: Legacy. + */ + public static final int PREAMBLE_LEGACY = 0; + + /** + * Preamble type: HT. + */ + public static final int PREAMBLE_HT = 1; + + /** + * Preamble type: VHT. + */ + public static final int PREAMBLE_VHT = 2; + + + /** + * The MAC address of the Responder. Will be null if a Wi-Fi Aware peer identifier (the + * peerHandle field) ise used to identify the Responder. + */ + public final MacAddress macAddress; + + /** + * The peer identifier of a Wi-Fi Aware Responder. Will be null if a MAC Address (the macAddress + * field) is used to identify the Responder. + */ + public final PeerHandle peerHandle; + + /** + * The device type of the Responder. + */ + public final int responderType; + + /** + * Indicates whether the Responder device supports IEEE 802.11mc. + */ + public final boolean supports80211mc; + + /** + * Responder channel bandwidth, specified using {@link ChannelWidth}. + */ + public final int channelWidth; + + /** + * The primary 20 MHz frequency (in MHz) of the channel of the Responder. + */ + public final int frequency; + + /** + * Not used if the {@link #channelWidth} is 20 MHz. If the Responder uses 40, 80 or 160 MHz, + * this is the center frequency (in MHz), if the Responder uses 80 + 80 MHz, this is the + * center frequency of the first segment (in MHz). + */ + public final int centerFreq0; + + /** + * Only used if the {@link #channelWidth} is 80 + 80 MHz. If the Responder uses 80 + 80 MHz, + * this is the center frequency of the second segment (in MHz). + */ + public final int centerFreq1; + + /** + * The preamble used by the Responder, specified using {@link PreambleType}. + */ + public final int preamble; + + /** + * Constructs Responder configuration, using a MAC address to identify the Responder. + * + * @param macAddress The MAC address of the Responder. + * @param responderType The type of the responder device, specified using + * {@link ResponderType}. + * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc. + * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}. + * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder. + * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses + * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the + * Responder uses 80 + 80 MHz, this is the center frequency of the first + * segment (in MHz). + * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the + * Responder + * uses 80 + 80 MHz, this is the center frequency of the second segment + * (in + * MHz). + * @param preamble The preamble used by the Responder, specified using + * {@link PreambleType}. + */ + public ResponderConfig(@NonNull MacAddress macAddress, @ResponderType int responderType, + boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0, + int centerFreq1, @PreambleType int preamble) { + if (macAddress == null) { + throw new IllegalArgumentException( + "Invalid ResponderConfig - must specify a MAC address"); + } + this.macAddress = macAddress; + this.peerHandle = null; + this.responderType = responderType; + this.supports80211mc = supports80211mc; + this.channelWidth = channelWidth; + this.frequency = frequency; + this.centerFreq0 = centerFreq0; + this.centerFreq1 = centerFreq1; + this.preamble = preamble; + } + + /** + * Constructs Responder configuration, using a Wi-Fi Aware PeerHandle to identify the Responder. + * + * @param peerHandle The Wi-Fi Aware peer identifier of the Responder. + * @param responderType The type of the responder device, specified using + * {@link ResponderType}. + * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc. + * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}. + * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder. + * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses + * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the + * Responder uses 80 + 80 MHz, this is the center frequency of the first + * segment (in MHz). + * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the + * Responder + * uses 80 + 80 MHz, this is the center frequency of the second segment + * (in + * MHz). + * @param preamble The preamble used by the Responder, specified using + * {@link PreambleType}. + */ + public ResponderConfig(@NonNull PeerHandle peerHandle, @ResponderType int responderType, + boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0, + int centerFreq1, @PreambleType int preamble) { + this.macAddress = null; + this.peerHandle = peerHandle; + this.responderType = responderType; + this.supports80211mc = supports80211mc; + this.channelWidth = channelWidth; + this.frequency = frequency; + this.centerFreq0 = centerFreq0; + this.centerFreq1 = centerFreq1; + this.preamble = preamble; + } + + /** + * Constructs Responder configuration. This is an internal-only constructor which specifies both + * a MAC address and a Wi-Fi PeerHandle to identify the Responder. + * + * @param macAddress The MAC address of the Responder. + * @param peerHandle The Wi-Fi Aware peer identifier of the Responder. + * @param responderType The type of the responder device, specified using + * {@link ResponderType}. + * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc. + * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}. + * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder. + * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses + * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the + * Responder uses 80 + 80 MHz, this is the center frequency of the first + * segment (in MHz). + * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the + * Responder + * uses 80 + 80 MHz, this is the center frequency of the second segment + * (in + * MHz). + * @param preamble The preamble used by the Responder, specified using + * {@link PreambleType}. + * @hide + */ + public ResponderConfig(@NonNull MacAddress macAddress, @NonNull PeerHandle peerHandle, + @ResponderType int responderType, boolean supports80211mc, + @ChannelWidth int channelWidth, int frequency, int centerFreq0, int centerFreq1, + @PreambleType int preamble) { + this.macAddress = macAddress; + this.peerHandle = peerHandle; + this.responderType = responderType; + this.supports80211mc = supports80211mc; + this.channelWidth = channelWidth; + this.frequency = frequency; + this.centerFreq0 = centerFreq0; + this.centerFreq1 = centerFreq1; + this.preamble = preamble; + } + + /** + * Creates a Responder configuration from a {@link ScanResult} corresponding to an Access + * Point (AP), which can be obtained from {@link android.net.wifi.WifiManager#getScanResults()}. + */ + public static ResponderConfig fromScanResult(ScanResult scanResult) { + MacAddress macAddress = MacAddress.fromString(scanResult.BSSID); + int responderType = RESPONDER_AP; + boolean supports80211mc = scanResult.is80211mcResponder(); + int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth); + int frequency = scanResult.frequency; + int centerFreq0 = scanResult.centerFreq0; + int centerFreq1 = scanResult.centerFreq1; + + // TODO: b/68936111 - extract preamble info from IE + int preamble; + if (channelWidth == CHANNEL_WIDTH_80MHZ || channelWidth == CHANNEL_WIDTH_160MHZ) { + preamble = PREAMBLE_VHT; + } else { + preamble = PREAMBLE_HT; + } + + return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth, + frequency, centerFreq0, centerFreq1, preamble); + } + + /** + * Creates a Responder configuration from a MAC address corresponding to a Wi-Fi Aware + * Responder. The Responder parameters are set to defaults. + */ + public static ResponderConfig fromWifiAwarePeerMacAddressWithDefaults(MacAddress macAddress) { + /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder + * is expected to be brought up and available to negotiate a maximum accuracy channel + * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware + * Unsolicited Publisher with Ranging enabled. + */ + return new ResponderConfig(macAddress, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ, + AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT); + } + + /** + * Creates a Responder configuration from a {@link PeerHandle} corresponding to a Wi-Fi Aware + * Responder. The Responder parameters are set to defaults. + */ + public static ResponderConfig fromWifiAwarePeerHandleWithDefaults(PeerHandle peerHandle) { + /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder + * is expected to be brought up and available to negotiate a maximum accuracy channel + * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware + * Unsolicited Publisher with Ranging enabled. + */ + return new ResponderConfig(peerHandle, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ, + AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT); + } + + /** + * Check whether the Responder configuration is valid. + * + * @return true if valid, false otherwise. + * @hide + */ + public boolean isValid(boolean awareSupported) { + if (macAddress == null && peerHandle == null || macAddress != null && peerHandle != null) { + return false; + } + if (!awareSupported && responderType == RESPONDER_AWARE) { + return false; + } + + return true; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (macAddress == null) { + dest.writeBoolean(false); + } else { + dest.writeBoolean(true); + macAddress.writeToParcel(dest, flags); + } + if (peerHandle == null) { + dest.writeBoolean(false); + } else { + dest.writeBoolean(true); + dest.writeInt(peerHandle.peerId); + } + dest.writeInt(responderType); + dest.writeInt(supports80211mc ? 1 : 0); + dest.writeInt(channelWidth); + dest.writeInt(frequency); + dest.writeInt(centerFreq0); + dest.writeInt(centerFreq1); + dest.writeInt(preamble); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ResponderConfig[] newArray(int size) { + return new ResponderConfig[size]; + } + + @Override + public ResponderConfig createFromParcel(Parcel in) { + boolean macAddressPresent = in.readBoolean(); + MacAddress macAddress = null; + if (macAddressPresent) { + macAddress = MacAddress.CREATOR.createFromParcel(in); + } + boolean peerHandlePresent = in.readBoolean(); + PeerHandle peerHandle = null; + if (peerHandlePresent) { + peerHandle = new PeerHandle(in.readInt()); + } + int responderType = in.readInt(); + boolean supports80211mc = in.readInt() == 1; + int channelWidth = in.readInt(); + int frequency = in.readInt(); + int centerFreq0 = in.readInt(); + int centerFreq1 = in.readInt(); + int preamble = in.readInt(); + + if (peerHandle == null) { + return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth, + frequency, centerFreq0, centerFreq1, preamble); + } else { + return new ResponderConfig(peerHandle, responderType, supports80211mc, channelWidth, + frequency, centerFreq0, centerFreq1, preamble); + } + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ResponderConfig)) { + return false; + } + + ResponderConfig lhs = (ResponderConfig) o; + + return Objects.equals(macAddress, lhs.macAddress) && Objects.equals(peerHandle, + lhs.peerHandle) && responderType == lhs.responderType + && supports80211mc == lhs.supports80211mc && channelWidth == lhs.channelWidth + && frequency == lhs.frequency && centerFreq0 == lhs.centerFreq0 + && centerFreq1 == lhs.centerFreq1 && preamble == lhs.preamble; + } + + @Override + public int hashCode() { + return Objects.hash(macAddress, peerHandle, responderType, supports80211mc, channelWidth, + frequency, centerFreq0, centerFreq1, preamble); + } + + /** @hide */ + @Override + public String toString() { + return new StringBuffer("ResponderConfig: macAddress=").append(macAddress).append( + ", peerHandle=").append(peerHandle == null ? "" : peerHandle.peerId).append( + ", responderType=").append(responderType).append(", supports80211mc=").append( + supports80211mc).append(", channelWidth=").append(channelWidth).append( + ", frequency=").append(frequency).append(", centerFreq0=").append( + centerFreq0).append(", centerFreq1=").append(centerFreq1).append( + ", preamble=").append(preamble).toString(); + } + + /** @hide */ + static int translcateScanResultChannelWidth(int scanResultChannelWidth) { + switch (scanResultChannelWidth) { + case ScanResult.CHANNEL_WIDTH_20MHZ: + return CHANNEL_WIDTH_20MHZ; + case ScanResult.CHANNEL_WIDTH_40MHZ: + return CHANNEL_WIDTH_40MHZ; + case ScanResult.CHANNEL_WIDTH_80MHZ: + return CHANNEL_WIDTH_80MHZ; + case ScanResult.CHANNEL_WIDTH_160MHZ: + return CHANNEL_WIDTH_160MHZ; + case ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ: + return CHANNEL_WIDTH_80MHZ_PLUS_MHZ; + default: + throw new IllegalArgumentException( + "translcateScanResultChannelWidth: bad " + scanResultChannelWidth); + } + } +} diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java index 735e872e..b4c690f4 100644 --- a/android/net/wifi/rtt/WifiRttManager.java +++ b/android/net/wifi/rtt/WifiRttManager.java @@ -41,7 +41,7 @@ import java.util.List; * * @hide RTT_API */ -@SystemService(Context.WIFI_RTT2_SERVICE) +@SystemService(Context.WIFI_RTT_RANGING_SERVICE) public class WifiRttManager { private static final String TAG = "WifiRttManager"; private static final boolean VDBG = false; diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java index 811091e3..1e847c59 100644 --- a/android/os/BatteryStats.java +++ b/android/os/BatteryStats.java @@ -16,6 +16,7 @@ package android.os; +import android.app.ActivityManager; import android.app.job.JobParameters; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -33,6 +34,7 @@ import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.Display; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; @@ -225,8 +227,11 @@ public abstract class BatteryStats implements Parcelable { * New in version 28: * - Light/Deep Doze power * - WiFi Multicast Wakelock statistics (count & duration) + * New in version 29: + * - Process states re-ordered. TOP_SLEEPING now below BACKGROUND. HEAVY_WEIGHT introduced. + * - CPU times per UID process state */ - static final int CHECKIN_VERSION = 28; + static final int CHECKIN_VERSION = 29; /** * Old version, we hit 9 and ran out of room, need to remove. @@ -327,7 +332,8 @@ public abstract class BatteryStats implements Parcelable { * * Other types might include times spent in foreground, background etc. */ - private final String UID_TIMES_TYPE_ALL = "A"; + @VisibleForTesting + public static final String UID_TIMES_TYPE_ALL = "A"; /** * State for keeping track of counting information. @@ -506,6 +512,31 @@ public abstract class BatteryStats implements Parcelable { public abstract void logState(Printer pw, String prefix); } + /** + * Maps the ActivityManager procstate into corresponding BatteryStats procstate. + */ + public static int mapToInternalProcessState(int procState) { + if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) { + return ActivityManager.PROCESS_STATE_NONEXISTENT; + } else if (procState == ActivityManager.PROCESS_STATE_TOP) { + return Uid.PROCESS_STATE_TOP; + } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + // Persistent and other foreground states go here. + return Uid.PROCESS_STATE_FOREGROUND_SERVICE; + } else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) { + // Persistent and other foreground states go here. + return Uid.PROCESS_STATE_FOREGROUND; + } else if (procState <= ActivityManager.PROCESS_STATE_RECEIVER) { + return Uid.PROCESS_STATE_BACKGROUND; + } else if (procState <= ActivityManager.PROCESS_STATE_TOP_SLEEPING) { + return Uid.PROCESS_STATE_TOP_SLEEPING; + } else if (procState <= ActivityManager.PROCESS_STATE_HEAVY_WEIGHT) { + return Uid.PROCESS_STATE_HEAVY_WEIGHT; + } else { + return Uid.PROCESS_STATE_CACHED; + } + } + /** * The statistics associated with a particular uid. */ @@ -645,6 +676,15 @@ public abstract class BatteryStats implements Parcelable { public abstract long[] getCpuFreqTimes(int which); public abstract long[] getScreenOffCpuFreqTimes(int which); + /** + * Returns cpu times of an uid at a particular process state. + */ + public abstract long[] getCpuFreqTimes(int which, int procState); + /** + * Returns cpu times of an uid while the screen if off at a particular process state. + */ + public abstract long[] getScreenOffCpuFreqTimes(int which, int procState); + // Note: the following times are disjoint. They can be added together to find the // total time a uid has had any processes running at all. @@ -657,33 +697,62 @@ public abstract class BatteryStats implements Parcelable { * none in the "top" state. */ public static final int PROCESS_STATE_FOREGROUND_SERVICE = 1; - /** - * Time this uid has any process that is top while the device is sleeping, but none - * in the "foreground service" or better state. - */ - public static final int PROCESS_STATE_TOP_SLEEPING = 2; /** * Time this uid has any process in an active foreground state, but none in the * "top sleeping" or better state. */ - public static final int PROCESS_STATE_FOREGROUND = 3; + public static final int PROCESS_STATE_FOREGROUND = 2; /** * Time this uid has any process in an active background state, but none in the * "foreground" or better state. */ - public static final int PROCESS_STATE_BACKGROUND = 4; + public static final int PROCESS_STATE_BACKGROUND = 3; + /** + * Time this uid has any process that is top while the device is sleeping, but not + * active for any other reason. We kind-of consider it a kind of cached process + * for execution restrictions. + */ + public static final int PROCESS_STATE_TOP_SLEEPING = 4; + /** + * Time this uid has any process that is in the background but it has an activity + * marked as "can't save state". This is essentially a cached process, though the + * system will try much harder than normal to avoid killing it. + */ + public static final int PROCESS_STATE_HEAVY_WEIGHT = 5; /** * Time this uid has any processes that are sitting around cached, not in one of the * other active states. */ - public static final int PROCESS_STATE_CACHED = 5; + public static final int PROCESS_STATE_CACHED = 6; /** * Total number of process states we track. */ - public static final int NUM_PROCESS_STATE = 6; + public static final int NUM_PROCESS_STATE = 7; + // Used in dump static final String[] PROCESS_STATE_NAMES = { - "Top", "Fg Service", "Top Sleeping", "Foreground", "Background", "Cached" + "Top", "Fg Service", "Foreground", "Background", "Top Sleeping", "Heavy Weight", + "Cached" + }; + + // Used in checkin dump + @VisibleForTesting + public static final String[] UID_PROCESS_TYPES = { + "T", // TOP + "FS", // FOREGROUND_SERVICE + "F", // FOREGROUND + "B", // BACKGROUND + "TS", // TOP_SLEEPING + "HW", // HEAVY_WEIGHT + "C" // CACHED + }; + + /** + * When the process exits one of these states, we need to make sure cpu time in this state + * is not attributed to any non-critical process states. + */ + public static final int[] CRITICAL_PROC_STATES = { + PROCESS_STATE_TOP, PROCESS_STATE_FOREGROUND_SERVICE, PROCESS_STATE_FOREGROUND }; public abstract long getProcessStateTime(int state, long elapsedRealtimeUs, int which); @@ -1180,7 +1249,7 @@ public abstract class BatteryStats implements Parcelable { public static final class PackageChange { public String mPackageName; public boolean mUpdate; - public int mVersionCode; + public long mVersionCode; } public static final class DailyItem { @@ -3994,6 +4063,29 @@ public abstract class BatteryStats implements Parcelable { dumpLine(pw, uid, category, CPU_TIMES_AT_FREQ_DATA, UID_TIMES_TYPE_ALL, cpuFreqTimeMs.length, sb.toString()); } + + for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) { + final long[] timesMs = u.getCpuFreqTimes(which, procState); + if (timesMs != null && timesMs.length == cpuFreqs.length) { + sb.setLength(0); + for (int i = 0; i < timesMs.length; ++i) { + sb.append((i == 0 ? "" : ",") + timesMs[i]); + } + final long[] screenOffTimesMs = u.getScreenOffCpuFreqTimes( + which, procState); + if (screenOffTimesMs != null) { + for (int i = 0; i < screenOffTimesMs.length; ++i) { + sb.append("," + screenOffTimesMs[i]); + } + } else { + for (int i = 0; i < timesMs.length; ++i) { + sb.append(",0"); + } + } + dumpLine(pw, uid, category, CPU_TIMES_AT_FREQ_DATA, + Uid.UID_PROCESS_TYPES[procState], timesMs.length, sb.toString()); + } + } } final ArrayMap processStats @@ -5604,6 +5696,30 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } + for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) { + final long[] cpuTimes = u.getCpuFreqTimes(which, procState); + if (cpuTimes != null) { + sb.setLength(0); + sb.append(" Cpu times per freq at state " + + Uid.PROCESS_STATE_NAMES[procState] + ":"); + for (int i = 0; i < cpuTimes.length; ++i) { + sb.append(" " + cpuTimes[i]); + } + pw.println(sb.toString()); + } + + final long[] screenOffCpuTimes = u.getScreenOffCpuFreqTimes(which, procState); + if (screenOffCpuTimes != null) { + sb.setLength(0); + sb.append(" Screen-off cpu times per freq at state " + + Uid.PROCESS_STATE_NAMES[procState] + ":"); + for (int i = 0; i < screenOffCpuTimes.length; ++i) { + sb.append(" " + screenOffCpuTimes[i]); + } + pw.println(sb.toString()); + } + } + final ArrayMap processStats = u.getProcessStats(); for (int ipr=processStats.size()-1; ipr>=0; ipr--) { @@ -6742,7 +6858,7 @@ public abstract class BatteryStats implements Parcelable { /** Dump #STATS_SINCE_CHARGED batterystats data to a proto. @hide */ public void dumpProtoLocked(Context context, FileDescriptor fd, List apps, - int flags, long historyStart) { + int flags) { final ProtoOutputStream proto = new ProtoOutputStream(fd); final long bToken = proto.start(BatteryStatsServiceDumpProto.BATTERYSTATS); prepareForDumpLocked(); @@ -6752,13 +6868,7 @@ public abstract class BatteryStats implements Parcelable { proto.write(BatteryStatsProto.START_PLATFORM_VERSION, getStartPlatformVersion()); proto.write(BatteryStatsProto.END_PLATFORM_VERSION, getEndPlatformVersion()); - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - - if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) { - if (startIteratingHistoryLocked()) { - // TODO: implement dumpProtoHistoryLocked(proto); - } - } + // History intentionally not included in proto dump. if ((flags & (DUMP_HISTORY_ONLY | DUMP_DAILY_ONLY)) == 0) { final BatteryStatsHelper helper = new BatteryStatsHelper(context, false, diff --git a/android/os/Binder.java b/android/os/Binder.java index b5bcd02c..33470f36 100644 --- a/android/os/Binder.java +++ b/android/os/Binder.java @@ -35,6 +35,9 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; /** * Base class for a remotable object, the core part of a lightweight @@ -298,7 +301,7 @@ public class Binder implements IBinder { long callingIdentity = clearCallingIdentity(); Throwable throwableToPropagate = null; try { - action.run(); + action.runOrThrow(); } catch (Throwable throwable) { throwableToPropagate = throwable; } finally { @@ -322,7 +325,7 @@ public class Binder implements IBinder { long callingIdentity = clearCallingIdentity(); Throwable throwableToPropagate = null; try { - return action.get(); + return action.getOrThrow(); } catch (Throwable throwable) { throwableToPropagate = throwable; return null; // overridden by throwing in finally block @@ -778,6 +781,8 @@ final class BinderProxy implements IBinder { private static final int LOG_MAIN_INDEX_SIZE = 8; private static final int MAIN_INDEX_SIZE = 1 << LOG_MAIN_INDEX_SIZE; private static final int MAIN_INDEX_MASK = MAIN_INDEX_SIZE - 1; + // Debuggable builds will throw an AssertionError if the number of map entries exceeds: + private static final int CRASH_AT_SIZE = 5_000; /** * We next warn when we exceed this bucket size. @@ -899,10 +904,60 @@ final class BinderProxy implements IBinder { keyArray[size] = key; } if (size >= mWarnBucketSize) { + final int totalSize = size(); Log.v(Binder.TAG, "BinderProxy map growth! bucket size = " + size - + " total = " + size()); + + " total = " + totalSize); mWarnBucketSize += WARN_INCREMENT; + if (Build.IS_DEBUGGABLE && totalSize > CRASH_AT_SIZE) { + diagnosticCrash(); + } + } + } + + /** + * Dump a histogram to the logcat, then throw an assertion error. Used to diagnose + * abnormally large proxy maps. + */ + private void diagnosticCrash() { + Map counts = new HashMap<>(); + for (ArrayList> a : mMainIndexValues) { + if (a != null) { + for (WeakReference weakRef : a) { + BinderProxy bp = weakRef.get(); + String key; + if (bp == null) { + key = ""; + } else { + try { + key = bp.getInterfaceDescriptor(); + } catch (Throwable t) { + key = ""; + } + } + Integer i = counts.get(key); + if (i == null) { + counts.put(key, 1); + } else { + counts.put(key, i + 1); + } + } + } + } + Map.Entry[] sorted = counts.entrySet().toArray( + new Map.Entry[counts.size()]); + Arrays.sort(sorted, (Map.Entry a, Map.Entry b) + -> b.getValue().compareTo(a.getValue())); + Log.v(Binder.TAG, "BinderProxy descriptor histogram (top ten):"); + int printLength = Math.min(10, sorted.length); + for (int i = 0; i < printLength; i++) { + Log.v(Binder.TAG, " #" + (i + 1) + ": " + sorted[i].getKey() + " x" + + sorted[i].getValue()); } + + // Now throw an assertion. + final int totalSize = size(); + throw new AssertionError("Binder ProxyMap has too many entries: " + totalSize + + ". BinderProxy leak?"); } // Corresponding ArrayLists in the following two arrays always have the same size. diff --git a/android/os/Build.java b/android/os/Build.java index 02c7bd64..48f56847 100644 --- a/android/os/Build.java +++ b/android/os/Build.java @@ -117,8 +117,14 @@ public class Build { public static final String SERIAL = getString("no.such.thing"); /** - * Gets the hardware serial, if available. - * @return The serial if specified. + * Gets the hardware serial number, if available. + * + *

      Note: Root access may allow you to modify device identifiers, such as + * the hardware serial number. If you change these identifiers, you can use + * key attestation to obtain + * proof of the device's original identifiers. + * + * @return The serial number if specified. */ @RequiresPermission(Manifest.permission.READ_PHONE_STATE) public static String getSerial() { @@ -215,12 +221,30 @@ public class Build { public static final String SDK = getString("ro.build.version.sdk"); /** - * The user-visible SDK version of the framework; its possible - * values are defined in {@link Build.VERSION_CODES}. + * The SDK version of the software currently running on this hardware + * device. This value never changes while a device is booted, but it may + * increase when the hardware manufacturer provides an OTA update. + *

      + * Possible values are defined in {@link Build.VERSION_CODES}. + * + * @see #FIRST_SDK_INT */ public static final int SDK_INT = SystemProperties.getInt( "ro.build.version.sdk", 0); + /** + * The SDK version of the software that initially shipped on + * this hardware device. It never changes during the lifetime + * of the device, even when {@link #SDK_INT} increases due to an OTA + * update. + *

      + * Possible values are defined in {@link Build.VERSION_CODES}. + * + * @see #SDK_INT + */ + public static final int FIRST_SDK_INT = SystemProperties + .getInt("ro.product.first_api_level", 0); + /** * The developer preview revision of a prerelease SDK. This value will always * be 0 on production platform builds/devices. @@ -264,6 +288,14 @@ public class Build { * @hide */ public static final int RESOURCES_SDK_INT = SDK_INT + ACTIVE_CODENAMES.length; + + /** + * The current lowest supported value of app target SDK. Applications targeting + * lower values will fail to install and run. Its possible values are defined + * in {@link Build.VERSION_CODES}. + */ + public static final int MIN_SUPPORTED_TARGET_SDK_INT = SystemProperties.getInt( + "ro.build.version.min_supported_target_sdk", 0); } /** @@ -937,7 +969,9 @@ public class Build { if (IS_ENG) return true; if (IS_TREBLE_ENABLED) { - int result = VintfObject.verify(new String[0]); + // If we can run this code, the device should already pass AVB. + // So, we don't need to check AVB here. + int result = VintfObject.verifyWithoutAvb(); if (result != 0) { Slog.e(TAG, "Vendor interface is incompatible, error=" diff --git a/android/os/Debug.java b/android/os/Debug.java index 2acf36fe..848ab88d 100644 --- a/android/os/Debug.java +++ b/android/os/Debug.java @@ -1136,7 +1136,7 @@ public final class Debug int intervalUs) { VMDebug.startMethodTracing(fixTracePath(tracePath), bufferSize, 0, true, intervalUs); } - + /** * Formats name of trace log file for method tracing. */ @@ -1706,11 +1706,11 @@ public final class Debug * Retrieves information about this processes memory usages. This information is broken down by * how much is in use by dalvik, the native heap, and everything else. * - *

      Note: this method directly retrieves memory information for the give process + *

      Note: this method directly retrieves memory information for the given process * from low-level data available to it. It may not be able to retrieve information about * some protected allocations, such as graphics. If you want to be sure you can see - * all information about allocations by the process, use instead - * {@link android.app.ActivityManager#getProcessMemoryInfo(int[])}.

      + * all information about allocations by the process, use + * {@link android.app.ActivityManager#getProcessMemoryInfo(int[])} instead.

      */ public static native void getMemoryInfo(MemoryInfo memoryInfo); diff --git a/android/os/Environment.java b/android/os/Environment.java index f977c1de..b1794a6d 100644 --- a/android/os/Environment.java +++ b/android/os/Environment.java @@ -24,6 +24,7 @@ import android.text.TextUtils; import android.util.Log; import java.io.File; +import java.util.LinkedList; /** * Provides access to environment variables. @@ -291,8 +292,9 @@ public class Environment { } /** {@hide} */ - public static File getReferenceProfile(String packageName) { - return buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName); + public static File getProfileSnapshotPath(String packageName, String codePath) { + return buildPath(buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName, + "primary.prof.snapshot")); } /** {@hide} */ @@ -608,6 +610,79 @@ public class Environment { return false; } + /** {@hide} */ public static final int HAS_MUSIC = 1 << 0; + /** {@hide} */ public static final int HAS_PODCASTS = 1 << 1; + /** {@hide} */ public static final int HAS_RINGTONES = 1 << 2; + /** {@hide} */ public static final int HAS_ALARMS = 1 << 3; + /** {@hide} */ public static final int HAS_NOTIFICATIONS = 1 << 4; + /** {@hide} */ public static final int HAS_PICTURES = 1 << 5; + /** {@hide} */ public static final int HAS_MOVIES = 1 << 6; + /** {@hide} */ public static final int HAS_DOWNLOADS = 1 << 7; + /** {@hide} */ public static final int HAS_DCIM = 1 << 8; + /** {@hide} */ public static final int HAS_DOCUMENTS = 1 << 9; + + /** {@hide} */ public static final int HAS_ANDROID = 1 << 16; + /** {@hide} */ public static final int HAS_OTHER = 1 << 17; + + /** + * Classify the content types present on the given external storage device. + *

      + * This is typically useful for deciding if an inserted SD card is empty, or + * if it contains content like photos that should be preserved. + * + * @hide + */ + public static int classifyExternalStorageDirectory(File dir) { + int res = 0; + for (File f : FileUtils.listFilesOrEmpty(dir)) { + if (f.isFile() && isInterestingFile(f)) { + res |= HAS_OTHER; + } else if (f.isDirectory() && hasInterestingFiles(f)) { + final String name = f.getName(); + if (DIRECTORY_MUSIC.equals(name)) res |= HAS_MUSIC; + else if (DIRECTORY_PODCASTS.equals(name)) res |= HAS_PODCASTS; + else if (DIRECTORY_RINGTONES.equals(name)) res |= HAS_RINGTONES; + else if (DIRECTORY_ALARMS.equals(name)) res |= HAS_ALARMS; + else if (DIRECTORY_NOTIFICATIONS.equals(name)) res |= HAS_NOTIFICATIONS; + else if (DIRECTORY_PICTURES.equals(name)) res |= HAS_PICTURES; + else if (DIRECTORY_MOVIES.equals(name)) res |= HAS_MOVIES; + else if (DIRECTORY_DOWNLOADS.equals(name)) res |= HAS_DOWNLOADS; + else if (DIRECTORY_DCIM.equals(name)) res |= HAS_DCIM; + else if (DIRECTORY_DOCUMENTS.equals(name)) res |= HAS_DOCUMENTS; + else if (DIRECTORY_ANDROID.equals(name)) res |= HAS_ANDROID; + else res |= HAS_OTHER; + } + } + return res; + } + + private static boolean hasInterestingFiles(File dir) { + final LinkedList explore = new LinkedList<>(); + explore.add(dir); + while (!explore.isEmpty()) { + dir = explore.pop(); + for (File f : FileUtils.listFilesOrEmpty(dir)) { + if (isInterestingFile(f)) return true; + if (f.isDirectory()) explore.add(f); + } + } + return false; + } + + private static boolean isInterestingFile(File file) { + if (file.isFile()) { + final String name = file.getName().toLowerCase(); + if (name.endsWith(".exe") || name.equals("autorun.inf") + || name.equals("launchpad.zip") || name.equals(".nomedia")) { + return false; + } else { + return true; + } + } else { + return false; + } + } + /** * Get a top-level shared/external storage directory for placing files of a * particular type. This is where the user will typically place and manage diff --git a/android/os/HandlerExecutor.java b/android/os/HandlerExecutor.java new file mode 100644 index 00000000..416b24b5 --- /dev/null +++ b/android/os/HandlerExecutor.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +/** + * An adapter {@link Executor} that posts all executed tasks onto the given + * {@link Handler}. + * + * @hide + */ +public class HandlerExecutor implements Executor { + private final Handler mHandler; + + public HandlerExecutor(@NonNull Handler handler) { + mHandler = Preconditions.checkNotNull(handler); + } + + @Override + public void execute(Runnable command) { + if (!mHandler.post(command)) { + throw new RejectedExecutionException(mHandler + " is shutting down"); + } + } +} diff --git a/android/os/HardwarePropertiesManager.java b/android/os/HardwarePropertiesManager.java index aad202e7..eae7d701 100644 --- a/android/os/HardwarePropertiesManager.java +++ b/android/os/HardwarePropertiesManager.java @@ -40,9 +40,11 @@ public class HardwarePropertiesManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ - DEVICE_TEMPERATURE_CPU, DEVICE_TEMPERATURE_GPU, DEVICE_TEMPERATURE_BATTERY, - DEVICE_TEMPERATURE_SKIN + @IntDef(prefix = { "DEVICE_TEMPERATURE_" }, value = { + DEVICE_TEMPERATURE_CPU, + DEVICE_TEMPERATURE_GPU, + DEVICE_TEMPERATURE_BATTERY, + DEVICE_TEMPERATURE_SKIN }) public @interface DeviceTemperatureType {} @@ -50,9 +52,11 @@ public class HardwarePropertiesManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ - TEMPERATURE_CURRENT, TEMPERATURE_THROTTLING, TEMPERATURE_SHUTDOWN, - TEMPERATURE_THROTTLING_BELOW_VR_MIN + @IntDef(prefix = { "TEMPERATURE_" }, value = { + TEMPERATURE_CURRENT, + TEMPERATURE_THROTTLING, + TEMPERATURE_SHUTDOWN, + TEMPERATURE_THROTTLING_BELOW_VR_MIN }) public @interface TemperatureSource {} diff --git a/android/os/Message.java b/android/os/Message.java index d066db1f..b303e10f 100644 --- a/android/os/Message.java +++ b/android/os/Message.java @@ -109,7 +109,9 @@ public final class Message implements Parcelable { // sometimes we store linked lists of these things /*package*/ Message next; - private static final Object sPoolSync = new Object(); + + /** @hide */ + public static final Object sPoolSync = new Object(); private static Message sPool; private static int sPoolSize = 0; @@ -370,6 +372,12 @@ public final class Message implements Parcelable { return callback; } + /** @hide */ + public Message setCallback(Runnable r) { + callback = r; + return this; + } + /** * Obtains a Bundle of arbitrary data associated with this * event, lazily creating it if necessary. Set this value by calling @@ -410,6 +418,16 @@ public final class Message implements Parcelable { this.data = data; } + /** + * Chainable setter for {@link #what} + * + * @hide + */ + public Message setWhat(int what) { + this.what = what; + return this; + } + /** * Sends this Message to the Handler specified by {@link #getTarget}. * Throws a null pointer exception if this field has not been set. diff --git a/android/os/MessageQueue.java b/android/os/MessageQueue.java index 624e28a6..96e7a598 100644 --- a/android/os/MessageQueue.java +++ b/android/os/MessageQueue.java @@ -871,7 +871,11 @@ public final class MessageQueue { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag=true, value={EVENT_INPUT, EVENT_OUTPUT, EVENT_ERROR}) + @IntDef(flag = true, prefix = { "EVENT_" }, value = { + EVENT_INPUT, + EVENT_OUTPUT, + EVENT_ERROR + }) public @interface Events {} /** diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java index 068f5f7f..cd6d41b3 100644 --- a/android/os/PowerManager.java +++ b/android/os/PowerManager.java @@ -466,7 +466,7 @@ public final class PowerManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "SHUTDOWN_REASON_" }, value = { SHUTDOWN_REASON_UNKNOWN, SHUTDOWN_REASON_SHUTDOWN, SHUTDOWN_REASON_REBOOT, @@ -680,6 +680,26 @@ public final class PowerManager { * as the user moves between applications and doesn't require a special permission. *

      * + *

      + * Recommended naming conventions for tags to make debugging easier: + *

        + *
      • use a unique prefix delimited by a colon for your app/library (e.g. + * gmail:mytag) to make it easier to understand where the wake locks comes + * from. This namespace will also avoid collision for tags inside your app + * coming from different libraries which will make debugging easier. + *
      • use constants (e.g. do not include timestamps in the tag) to make it + * easier for tools to aggregate similar wake locks. When collecting + * debugging data, the platform only monitors a finite number of tags, + * using constants will help tools to provide better debugging data. + *
      • avoid using Class#getName() or similar method since this class name + * can be transformed by java optimizer and obfuscator tools. + *
      • avoid wrapping the tag or a prefix to avoid collision with wake lock + * tags from the platform (e.g. *alarm*). + *
      • never include personnally identifiable information for privacy + * reasons. + *
      + *

      + * * @param levelAndFlags Combination of wake lock level and flag values defining * the requested behavior of the WakeLock. * @param tag Your class name (or other tag) for debugging purposes. @@ -1509,11 +1529,18 @@ public final class PowerManager { * cost of that work can be accounted to the application. *

      * + *

      + * Make sure to follow the tag naming convention when using WorkSource + * to make it easier for app developers to understand wake locks + * attributed to them. See {@link PowerManager#newWakeLock(int, String)} + * documentation. + *

      + * * @param ws The work source, or null if none. */ public void setWorkSource(WorkSource ws) { synchronized (mToken) { - if (ws != null && ws.size() == 0) { + if (ws != null && ws.isEmpty()) { ws = null; } @@ -1525,7 +1552,7 @@ public final class PowerManager { changed = true; mWorkSource = new WorkSource(ws); } else { - changed = mWorkSource.diff(ws); + changed = !mWorkSource.equals(ws); if (changed) { mWorkSource.set(ws); } diff --git a/android/os/PowerManagerInternal.java b/android/os/PowerManagerInternal.java index 77ac2651..3ef0961f 100644 --- a/android/os/PowerManagerInternal.java +++ b/android/os/PowerManagerInternal.java @@ -110,7 +110,7 @@ public abstract class PowerManagerInternal { * * This method must only be called by the device administration policy manager. */ - public abstract void setMaximumScreenOffTimeoutFromDeviceAdmin(int timeMs); + public abstract void setMaximumScreenOffTimeoutFromDeviceAdmin(int userId, long timeMs); /** * Used by the dream manager to override certain properties while dozing. diff --git a/android/os/RemoteCallbackList.java b/android/os/RemoteCallbackList.java index b9b9a18e..bbb8a7b5 100644 --- a/android/os/RemoteCallbackList.java +++ b/android/os/RemoteCallbackList.java @@ -333,6 +333,23 @@ public class RemoteCallbackList { } } + /** + * Performs {@code action} for each cookie associated with a callback, calling + * {@link #beginBroadcast()}/{@link #finishBroadcast()} before/after looping + * + * @hide + */ + public void broadcastForEachCookie(Consumer action) { + int itemCount = beginBroadcast(); + try { + for (int i = 0; i < itemCount; i++) { + action.accept((C) getBroadcastCookie(i)); + } + } finally { + finishBroadcast(); + } + } + /** * Returns the number of registered callbacks. Note that the number of registered * callbacks may differ from the value returned by {@link #beginBroadcast()} since diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java index 42ec315c..34c78455 100644 --- a/android/os/ServiceManager.java +++ b/android/os/ServiceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,29 +16,9 @@ package android.os; -import android.util.Log; - -import com.android.internal.os.BinderInternal; - -import java.util.HashMap; import java.util.Map; -/** @hide */ public final class ServiceManager { - private static final String TAG = "ServiceManager"; - private static IServiceManager sServiceManager; - private static HashMap sCache = new HashMap(); - - private static IServiceManager getIServiceManager() { - if (sServiceManager != null) { - return sServiceManager; - } - - // Find the service manager - sServiceManager = ServiceManagerNative - .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); - return sServiceManager; - } /** * Returns a reference to a service with the given name. @@ -47,32 +27,14 @@ public final class ServiceManager { * @return a reference to the service, or null if the service doesn't exist */ public static IBinder getService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().getService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in getService", e); - } return null; } /** - * Returns a reference to a service with the given name, or throws - * {@link NullPointerException} if none is found. - * - * @hide + * Is not supposed to return null, but that is fine for layoutlib. */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { - final IBinder binder = getService(name); - if (binder != null) { - return binder; - } else { - throw new ServiceNotFoundException(name); - } + throw new ServiceNotFoundException(name); } /** @@ -83,39 +45,7 @@ public final class ServiceManager { * @param service the service object */ public static void addService(String name, IBinder service) { - addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL); - } - - /** - * Place a new @a service called @a name into the service - * manager. - * - * @param name the name of the new service - * @param service the service object - * @param allowIsolated set to true to allow isolated sandboxed processes - * to access this service - */ - public static void addService(String name, IBinder service, boolean allowIsolated) { - addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL); - } - - /** - * Place a new @a service called @a name into the service - * manager. - * - * @param name the name of the new service - * @param service the service object - * @param allowIsolated set to true to allow isolated sandboxed processes - * @param dumpPriority supported dump priority levels as a bitmask - * to access this service - */ - public static void addService(String name, IBinder service, boolean allowIsolated, - int dumpPriority) { - try { - getIServiceManager().addService(name, service, allowIsolated, dumpPriority); - } catch (RemoteException e) { - Log.e(TAG, "error in addService", e); - } + // pass } /** @@ -123,17 +53,7 @@ public final class ServiceManager { * service manager. Non-blocking. */ public static IBinder checkService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().checkService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in checkService", e); - return null; - } + return null; } /** @@ -142,12 +62,9 @@ public final class ServiceManager { * case of an exception */ public static String[] listServices() { - try { - return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL); - } catch (RemoteException e) { - Log.e(TAG, "error in listServices", e); - return null; - } + // actual implementation returns null sometimes, so it's ok + // to return null instead of an empty list. + return null; } /** @@ -159,10 +76,7 @@ public final class ServiceManager { * @hide */ public static void initServiceCache(Map cache) { - if (sCache.size() != 0) { - throw new IllegalStateException("setServiceCache may only be called once"); - } - sCache.putAll(cache); + // pass } /** @@ -173,6 +87,7 @@ public final class ServiceManager { * @hide */ public static class ServiceNotFoundException extends Exception { + // identical to the original implementation public ServiceNotFoundException(String name) { super("No service published for: " + name); } diff --git a/android/os/StatsLogEventWrapper.java b/android/os/StatsLogEventWrapper.java index 9491bec6..3e8161f2 100644 --- a/android/os/StatsLogEventWrapper.java +++ b/android/os/StatsLogEventWrapper.java @@ -33,6 +33,8 @@ public final class StatsLogEventWrapper implements Parcelable { private static final int EVENT_TYPE_LIST = 3; private static final int EVENT_TYPE_FLOAT = 4; + // Keep this in sync with system/core/logcat/event.logtags + private static final int STATS_BUFFER_TAG_ID = 1937006964; /** * Creates a log_event that is binary-encoded as implemented in * system/core/liblog/log_event_list.c; this allows us to use the same parsing logic in statsd @@ -46,9 +48,14 @@ public final class StatsLogEventWrapper implements Parcelable { */ public StatsLogEventWrapper(int tag, int fields) { // Write four bytes from tag, starting with least-significant bit. - write4Bytes(tag); + // For pulled data, this tag number is not really used. We use the same tag number as + // pushed ones to be consistent. + write4Bytes(STATS_BUFFER_TAG_ID); mStorage.write(EVENT_TYPE_LIST); // This is required to start the log entry. - mStorage.write(fields); // Indicate number of elements in this list. + mStorage.write(fields + 1); // Indicate number of elements in this list. +1 for the tag + mStorage.write(EVENT_TYPE_INT); + // The first element is the real atom tag number + write4Bytes(tag); } /** diff --git a/android/os/UserManager.java b/android/os/UserManager.java index 22967af7..dd9fd93e 100644 --- a/android/os/UserManager.java +++ b/android/os/UserManager.java @@ -103,8 +103,12 @@ public class UserManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag=true, value={RESTRICTION_NOT_SET, RESTRICTION_SOURCE_SYSTEM, - RESTRICTION_SOURCE_DEVICE_OWNER, RESTRICTION_SOURCE_PROFILE_OWNER}) + @IntDef(flag = true, prefix = { "RESTRICTION_" }, value = { + RESTRICTION_NOT_SET, + RESTRICTION_SOURCE_SYSTEM, + RESTRICTION_SOURCE_DEVICE_OWNER, + RESTRICTION_SOURCE_PROFILE_OWNER + }) @SystemApi public @interface UserRestrictionSource {} @@ -189,6 +193,21 @@ public class UserManager { */ public static final String DISALLOW_SHARE_LOCATION = "no_share_location"; + /** + * Specifies if airplane mode is disallowed on the device. + * + *

      This restriction can only be set by the device owner and the profile owner on the + * primary user and it applies globally - i.e. it disables airplane mode on the entire device. + *

      The default value is false. + * + *

      Key for user restrictions. + *

      Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode"; + /** * Specifies if a user is disallowed from enabling the * "Unknown Sources" setting, that allows installation of apps from unknown sources. @@ -330,6 +349,28 @@ public class UserManager { */ public static final String DISALLOW_CONFIG_VPN = "no_config_vpn"; + /** + * Specifies if a user is disallowed from configuring location mode. Device owner and profile + * owners can set this restriction and it only applies on the managed user. + * + *

      In a managed profile, location sharing is forced off when it's off on primary user, so + * user can still turn off location sharing on managed profile when the restriction is set by + * profile owner on managed profile. + * + *

      This user restriction is different from {@link #DISALLOW_SHARE_LOCATION}, + * as the device owner or profile owner can still enable or disable location mode via + * {@link DevicePolicyManager#setSecureSetting} when this restriction is on. + * + *

      The default value is false. + * + *

      Key for user restrictions. + *

      Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_LOCATION_MODE = "no_config_location_mode"; + /** * Specifies if date, time and timezone configuring is disallowed. * @@ -769,6 +810,25 @@ public class UserManager { @SystemApi public static final String DISALLOW_OEM_UNLOCK = "no_oem_unlock"; + /** + * Specifies that the managed profile is not allowed to have unified lock screen challenge with + * the primary user. + * + *

      Note: Setting this restriction alone doesn't automatically set a + * separate challenge. Profile owner can ask the user to set a new password using + * {@link DevicePolicyManager#ACTION_SET_NEW_PASSWORD} and verify it using + * {@link DevicePolicyManager#isUsingUnifiedPassword(ComponentName)}. + * + *

      Can be set by profile owners. It only has effect on managed profiles when set by managed + * profile owner. Has no effect on non-managed profiles or users. + *

      Key for user restrictions. + *

      Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_UNIFIED_PASSWORD = "no_unified_password"; + /** * Allows apps in the parent profile to handle web links from the managed profile. * @@ -2107,15 +2167,46 @@ public class UserManager { } /** - * Set quiet mode of a managed profile. + * Enables or disables quiet mode for a managed profile. If quiet mode is enabled, apps in a + * managed profile don't run, generate notifications, or consume data or battery. + *

      + * If a user's credential is needed to turn off quiet mode, a confirm credential screen will be + * shown to the user. + *

      + * The change may not happen instantly, however apps can listen for + * {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and + * {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE} broadcasts in order to be notified of + * the change of the quiet mode. Apps can also check the current state of quiet mode by + * calling {@link #isQuietModeEnabled(UserHandle)}. + *

      + * The caller must either be the foreground default launcher or have one of these permissions: + * {@code MANAGE_USERS} or {@code MODIFY_QUIET_MODE}. * - * @param userHandle The user handle of the profile. - * @param enableQuietMode Whether quiet mode should be enabled or disabled. + * @param enableQuietMode whether quiet mode should be enabled or disabled + * @param userHandle user handle of the profile + * @return {@code false} if user's credential is needed in order to turn off quiet mode, + * {@code true} otherwise + * @throws SecurityException if the caller is invalid + * @throws IllegalArgumentException if {@code userHandle} is not a managed profile + * + * @see #isQuietModeEnabled(UserHandle) + */ + public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) { + return trySetQuietModeEnabled(enableQuietMode, userHandle, null); + } + + /** + * Similar to {@link #trySetQuietModeEnabled(boolean, UserHandle)}, except you can specify + * a target to start when user is unlocked. + * + * @see {@link #trySetQuietModeEnabled(boolean, UserHandle)} * @hide */ - public void setQuietModeEnabled(@UserIdInt int userHandle, boolean enableQuietMode) { + public boolean trySetQuietModeEnabled( + boolean enableQuietMode, @NonNull UserHandle userHandle, IntentSender target) { try { - mService.setQuietModeEnabled(userHandle, enableQuietMode); + return mService.trySetQuietModeEnabled( + mContext.getPackageName(), enableQuietMode, userHandle.getIdentifier(), target); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -2136,23 +2227,6 @@ public class UserManager { } } - /** - * Tries disabling quiet mode for a given user. If the user is still locked, we unlock the user - * first by showing the confirm credentials screen and disable quiet mode upon successful - * unlocking. If the user is already unlocked, we call through to {@link #setQuietModeEnabled} - * directly. - * - * @return true if the quiet mode was disabled immediately - * @hide - */ - public boolean trySetQuietModeDisabled(@UserIdInt int userHandle, IntentSender target) { - try { - return mService.trySetQuietModeDisabled(userHandle, target); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - /** * If the target user is a managed profile of the calling user or the caller * is itself a managed profile, then this returns a badged copy of the given diff --git a/android/os/UserManagerInternal.java b/android/os/UserManagerInternal.java index 9369eebf..6c9f1b25 100644 --- a/android/os/UserManagerInternal.java +++ b/android/os/UserManagerInternal.java @@ -130,7 +130,8 @@ public abstract class UserManagerInternal { *

      Called by the {@link com.android.server.devicepolicy.DevicePolicyManagerService} when * createAndManageUser is called by the device owner. */ - public abstract UserInfo createUserEvenWhenDisallowed(String name, int flags); + public abstract UserInfo createUserEvenWhenDisallowed(String name, int flags, + String[] disallowedPackages); /** * Same as {@link UserManager#removeUser(int userHandle)}, but bypasses the check for diff --git a/android/os/VintfObject.java b/android/os/VintfObject.java index 65b33e59..340f3fb8 100644 --- a/android/os/VintfObject.java +++ b/android/os/VintfObject.java @@ -18,7 +18,6 @@ package android.os; import java.util.Map; -import android.util.Log; /** * Java API for libvintf. @@ -40,7 +39,7 @@ public class VintfObject { * Verify that the given metadata for an OTA package is compatible with * this device. * - * @param packageInfo a list of serialized form of HalMaanifest's / + * @param packageInfo a list of serialized form of HalManifest's / * CompatibilityMatri'ces (XML). * @return = 0 if success (compatible) * > 0 if incompatible @@ -48,6 +47,17 @@ public class VintfObject { */ public static native int verify(String[] packageInfo); + /** + * Verify Vintf compatibility on the device without checking AVB + * (Android Verified Boot). It is useful to verify a running system + * image where AVB check is irrelevant. + * + * @return = 0 if success (compatible) + * > 0 if incompatible + * < 0 if any error (mount partition fails, illformed XML, etc.) + */ + public static native int verifyWithoutAvb(); + /// ---------- CTS Device Info /** diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java index ecec448a..401b4a36 100644 --- a/android/os/WorkSource.java +++ b/android/os/WorkSource.java @@ -1,10 +1,13 @@ package android.os; +import android.annotation.Nullable; import android.os.WorkSourceProto; import android.util.Log; import android.util.proto.ProtoOutputStream; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Objects; /** * Describes the source of some work that may be done by someone else. @@ -19,6 +22,8 @@ public class WorkSource implements Parcelable { int[] mUids; String[] mNames; + private ArrayList mChains; + /** * Internal statics to avoid object allocations in some operations. * The WorkSource object itself is not thread safe, but we need to @@ -39,6 +44,7 @@ public class WorkSource implements Parcelable { */ public WorkSource() { mNum = 0; + mChains = null; } /** @@ -48,6 +54,7 @@ public class WorkSource implements Parcelable { public WorkSource(WorkSource orig) { if (orig == null) { mNum = 0; + mChains = null; return; } mNum = orig.mNum; @@ -58,6 +65,16 @@ public class WorkSource implements Parcelable { mUids = null; mNames = null; } + + if (orig.mChains != null) { + // Make a copy of all WorkChains that exist on |orig| since they are mutable. + mChains = new ArrayList<>(orig.mChains.size()); + for (WorkChain chain : orig.mChains) { + mChains.add(new WorkChain(chain)); + } + } else { + mChains = null; + } } /** @hide */ @@ -65,6 +82,7 @@ public class WorkSource implements Parcelable { mNum = 1; mUids = new int[] { uid, 0 }; mNames = null; + mChains = null; } /** @hide */ @@ -75,12 +93,21 @@ public class WorkSource implements Parcelable { mNum = 1; mUids = new int[] { uid, 0 }; mNames = new String[] { name, null }; + mChains = null; } WorkSource(Parcel in) { mNum = in.readInt(); mUids = in.createIntArray(); mNames = in.createStringArray(); + + int numChains = in.readInt(); + if (numChains > 0) { + mChains = new ArrayList<>(numChains); + in.readParcelableList(mChains, WorkChain.class.getClassLoader()); + } else { + mChains = null; + } } /** @hide */ @@ -99,7 +126,8 @@ public class WorkSource implements Parcelable { } /** - * Clear names from this WorkSource. Uids are left intact. + * Clear names from this WorkSource. Uids are left intact. WorkChains if any, are left + * intact. * *

      Useful when combining with another WorkSource that doesn't have names. * @hide @@ -127,11 +155,16 @@ public class WorkSource implements Parcelable { */ public void clear() { mNum = 0; + if (mChains != null) { + mChains.clear(); + } } @Override public boolean equals(Object o) { - return o instanceof WorkSource && !diff((WorkSource)o); + return o instanceof WorkSource + && !diff((WorkSource) o) + && Objects.equals(mChains, ((WorkSource) o).mChains); } @Override @@ -145,6 +178,11 @@ public class WorkSource implements Parcelable { result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode(); } } + + if (mChains != null) { + result = ((result << 4) | (result >>> 28)) ^ mChains.hashCode(); + } + return result; } @@ -153,6 +191,8 @@ public class WorkSource implements Parcelable { * @param other The WorkSource to compare against. * @return If there is a difference, true is returned. */ + // TODO: This is a public API so it cannot be renamed. Because it is used in several places, + // we keep its semantics the same and ignore any differences in WorkChains (if any). public boolean diff(WorkSource other) { int N = mNum; if (N != other.mNum) { @@ -175,12 +215,15 @@ public class WorkSource implements Parcelable { /** * Replace the current contents of this work source with the given - * work source. If other is null, the current work source + * work source. If {@code other} is null, the current work source * will be made empty. */ public void set(WorkSource other) { if (other == null) { mNum = 0; + if (mChains != null) { + mChains.clear(); + } return; } mNum = other.mNum; @@ -203,6 +246,18 @@ public class WorkSource implements Parcelable { mUids = null; mNames = null; } + + if (other.mChains != null) { + if (mChains != null) { + mChains.clear(); + } else { + mChains = new ArrayList<>(other.mChains.size()); + } + + for (WorkChain chain : other.mChains) { + mChains.add(new WorkChain(chain)); + } + } } /** @hide */ @@ -211,6 +266,9 @@ public class WorkSource implements Parcelable { if (mUids == null) mUids = new int[2]; mUids[0] = uid; mNames = null; + if (mChains != null) { + mChains.clear(); + } } /** @hide */ @@ -225,9 +283,23 @@ public class WorkSource implements Parcelable { } mUids[0] = uid; mNames[0] = name; + if (mChains != null) { + mChains.clear(); + } } - /** @hide */ + /** + * Legacy API, DO NOT USE: Only deals with flat UIDs and tags. No chains are transferred, and no + * differences in chains are returned. This will be removed once its callers have been + * rewritten. + * + * NOTE: This is currently only used in GnssLocationProvider. + * + * @hide + * @deprecated for internal use only. WorkSources are opaque and no external callers should need + * to be aware of internal differences. + */ + @Deprecated public WorkSource[] setReturningDiffs(WorkSource other) { synchronized (sTmpWorkSource) { sNewbWork = null; @@ -251,11 +323,34 @@ public class WorkSource implements Parcelable { */ public boolean add(WorkSource other) { synchronized (sTmpWorkSource) { - return updateLocked(other, false, false); + boolean uidAdded = updateLocked(other, false, false); + + boolean chainAdded = false; + if (other.mChains != null) { + // NOTE: This is quite an expensive operation, especially if the number of chains + // is large. We could look into optimizing it if it proves problematic. + if (mChains == null) { + mChains = new ArrayList<>(other.mChains.size()); + } + + for (WorkChain wc : other.mChains) { + if (!mChains.contains(wc)) { + mChains.add(new WorkChain(wc)); + } + } + } + + return uidAdded || chainAdded; } } - /** @hide */ + /** + * Legacy API: DO NOT USE. Only in use from unit tests. + * + * @hide + * @deprecated meant for unit testing use only. Will be removed in a future API revision. + */ + @Deprecated public WorkSource addReturningNewbs(WorkSource other) { synchronized (sTmpWorkSource) { sNewbWork = null; @@ -311,22 +406,14 @@ public class WorkSource implements Parcelable { return true; } - /** @hide */ - public WorkSource addReturningNewbs(int uid) { - synchronized (sTmpWorkSource) { - sNewbWork = null; - sTmpWorkSource.mUids[0] = uid; - updateLocked(sTmpWorkSource, false, true); - return sNewbWork; - } - } - public boolean remove(WorkSource other) { if (mNum <= 0 || other.mNum <= 0) { return false; } + + boolean uidRemoved = false; if (mNames == null && other.mNames == null) { - return removeUids(other); + uidRemoved = removeUids(other); } else { if (mNames == null) { throw new IllegalArgumentException("Other " + other + " has names, but target " @@ -336,24 +423,54 @@ public class WorkSource implements Parcelable { throw new IllegalArgumentException("Target " + this + " has names, but other " + other + " does not"); } - return removeUidsAndNames(other); + uidRemoved = removeUidsAndNames(other); } - } - /** @hide */ - public WorkSource stripNames() { - if (mNum <= 0) { - return new WorkSource(); - } - WorkSource result = new WorkSource(); - int lastUid = -1; - for (int i=0; i(4); + } + + final WorkChain wc = new WorkChain(); + mChains.add(wc); + + return wc; + } + + /** + * Returns {@code true} iff. this work source contains zero UIDs and zero WorkChains to + * attribute usage to. + * + * @hide for internal use only. + */ + public boolean isEmpty() { + return mNum == 0 && (mChains == null || mChains.isEmpty()); + } + + /** + * @return the list of {@code WorkChains} associated with this {@code WorkSource}. + * @hide + */ + public ArrayList getWorkChains() { + return mChains; } private boolean removeUids(WorkSource other) { @@ -664,6 +781,224 @@ public class WorkSource implements Parcelable { } } + /** + * Represents an attribution chain for an item of work being performed. An attribution chain is + * an indexed list of {code (uid, tag)} nodes. The node at {@code index == 0} is the initiator + * of the work, and the node at the highest index performs the work. Nodes at other indices + * are intermediaries that facilitate the work. Examples : + * + * (1) Work being performed by uid=2456 (no chaining): + *

      +     * WorkChain {
      +     *   mUids = { 2456 }
      +     *   mTags = { null }
      +     *   mSize = 1;
      +     * }
      +     * 
      + * + * (2) Work being performed by uid=2456 (from component "c1") on behalf of uid=5678: + * + *
      +     * WorkChain {
      +     *   mUids = { 5678, 2456 }
      +     *   mTags = { null, "c1" }
      +     *   mSize = 1
      +     * }
      +     * 
      + * + * Attribution chains are mutable, though the only operation that can be performed on them + * is the addition of a new node at the end of the attribution chain to represent + * + * @hide + */ + public static class WorkChain implements Parcelable { + private int mSize; + private int[] mUids; + private String[] mTags; + + // @VisibleForTesting + public WorkChain() { + mSize = 0; + mUids = new int[4]; + mTags = new String[4]; + } + + // @VisibleForTesting + public WorkChain(WorkChain other) { + mSize = other.mSize; + mUids = other.mUids.clone(); + mTags = other.mTags.clone(); + } + + private WorkChain(Parcel in) { + mSize = in.readInt(); + mUids = in.createIntArray(); + mTags = in.createStringArray(); + } + + /** + * Append a node whose uid is {@code uid} and whose optional tag is {@code tag} to this + * {@code WorkChain}. + */ + public WorkChain addNode(int uid, @Nullable String tag) { + if (mSize == mUids.length) { + resizeArrays(); + } + + mUids[mSize] = uid; + mTags[mSize] = tag; + mSize++; + + return this; + } + + /** + * Return the UID to which this WorkChain should be attributed to, i.e, the UID that + * initiated the work and not the UID performing it. + */ + public int getAttributionUid() { + return mUids[0]; + } + + // TODO: The following three trivial getters are purely for testing and will be removed + // once we have higher level logic in place, e.g for serializing this WorkChain to a proto, + // diffing it etc. + // + // @VisibleForTesting + public int[] getUids() { + return mUids; + } + // @VisibleForTesting + public String[] getTags() { + return mTags; + } + // @VisibleForTesting + public int getSize() { + return mSize; + } + + private void resizeArrays() { + final int newSize = mSize * 2; + int[] uids = new int[newSize]; + String[] tags = new String[newSize]; + + System.arraycopy(mUids, 0, uids, 0, mSize); + System.arraycopy(mTags, 0, tags, 0, mSize); + + mUids = uids; + mTags = tags; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("WorkChain{"); + for (int i = 0; i < mSize; ++i) { + if (i != 0) { + result.append(", "); + } + result.append("("); + result.append(mUids[i]); + if (mTags[i] != null) { + result.append(", "); + result.append(mTags[i]); + } + result.append(")"); + } + + result.append("}"); + return result.toString(); + } + + @Override + public int hashCode() { + return (mSize + 31 * Arrays.hashCode(mUids)) * 31 + Arrays.hashCode(mTags); + } + + @Override + public boolean equals(Object o) { + if (o instanceof WorkChain) { + WorkChain other = (WorkChain) o; + + return mSize == other.mSize + && Arrays.equals(mUids, other.mUids) + && Arrays.equals(mTags, other.mTags); + } + + return false; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mSize); + dest.writeIntArray(mUids); + dest.writeStringArray(mTags); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public WorkChain createFromParcel(Parcel in) { + return new WorkChain(in); + } + public WorkChain[] newArray(int size) { + return new WorkChain[size]; + } + }; + } + + /** + * Computes the differences in WorkChains contained between {@code oldWs} and {@code newWs}. + * + * Returns {@code null} if no differences exist, otherwise returns a two element array. The + * first element is a list of "new" chains, i.e WorkChains present in {@code newWs} but not in + * {@code oldWs}. The second element is a list of "gone" chains, i.e WorkChains present in + * {@code oldWs} but not in {@code newWs}. + * + * @hide + */ + public static ArrayList[] diffChains(WorkSource oldWs, WorkSource newWs) { + ArrayList newChains = null; + ArrayList goneChains = null; + + // TODO(narayan): This is a dumb O(M*N) algorithm that determines what has changed across + // WorkSource objects. We can replace this with something smarter, for e.g by defining + // a Comparator between WorkChains. It's unclear whether that will be more efficient if + // the number of chains associated with a WorkSource is expected to be small + if (oldWs.mChains != null) { + for (int i = 0; i < oldWs.mChains.size(); ++i) { + final WorkChain wc = oldWs.mChains.get(i); + if (newWs.mChains == null || !newWs.mChains.contains(wc)) { + if (goneChains == null) { + goneChains = new ArrayList<>(oldWs.mChains.size()); + } + goneChains.add(wc); + } + } + } + + if (newWs.mChains != null) { + for (int i = 0; i < newWs.mChains.size(); ++i) { + final WorkChain wc = newWs.mChains.get(i); + if (oldWs.mChains == null || !oldWs.mChains.contains(wc)) { + if (newChains == null) { + newChains = new ArrayList<>(newWs.mChains.size()); + } + newChains.add(wc); + } + } + } + + if (newChains != null || goneChains != null) { + return new ArrayList[] { newChains, goneChains }; + } + + return null; + } + @Override public int describeContents() { return 0; @@ -674,6 +1009,13 @@ public class WorkSource implements Parcelable { dest.writeInt(mNum); dest.writeIntArray(mUids); dest.writeStringArray(mNames); + + if (mChains == null) { + dest.writeInt(-1); + } else { + dest.writeInt(mChains.size()); + dest.writeParcelableList(mChains, flags); + } } @Override @@ -690,6 +1032,17 @@ public class WorkSource implements Parcelable { result.append(mNames[i]); } } + + if (mChains != null) { + result.append(" chains="); + for (int i = 0; i < mChains.size(); ++i) { + if (i != 0) { + result.append(", "); + } + result.append(mChains.get(i)); + } + } + result.append("}"); return result.toString(); } @@ -705,6 +1058,25 @@ public class WorkSource implements Parcelable { } proto.end(contentProto); } + + if (mChains != null) { + for (int i = 0; i < mChains.size(); i++) { + final WorkChain wc = mChains.get(i); + final long workChain = proto.start(WorkSourceProto.WORK_CHAINS); + + final String[] tags = wc.getTags(); + final int[] uids = wc.getUids(); + for (int j = 0; j < tags.length; j++) { + final long contentProto = proto.start(WorkSourceProto.WORK_SOURCE_CONTENTS); + proto.write(WorkSourceProto.WorkSourceContentProto.UID, uids[j]); + proto.write(WorkSourceProto.WorkSourceContentProto.NAME, tags[j]); + proto.end(contentProto); + } + + proto.end(workChain); + } + } + proto.end(workSourceToken); } diff --git a/android/os/connectivity/CellularBatteryStats.java b/android/os/connectivity/CellularBatteryStats.java new file mode 100644 index 00000000..2593c85c --- /dev/null +++ b/android/os/connectivity/CellularBatteryStats.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os.connectivity; + +import android.os.BatteryStats; +import android.os.Parcel; +import android.os.Parcelable; + +import android.telephony.ModemActivityInfo; +import android.telephony.SignalStrength; + +import java.util.Arrays; + +/** + * API for Cellular power stats + * + * @hide + */ +public final class CellularBatteryStats implements Parcelable { + + private long mLoggingDurationMs; + private long mKernelActiveTimeMs; + private long mNumPacketsTx; + private long mNumBytesTx; + private long mNumPacketsRx; + private long mNumBytesRx; + private long mSleepTimeMs; + private long mIdleTimeMs; + private long mRxTimeMs; + private long mEnergyConsumedMaMs; + private long[] mTimeInRatMs; + private long[] mTimeInRxSignalStrengthLevelMs; + private long[] mTxTimeMs; + + public static final Parcelable.Creator CREATOR = new + Parcelable.Creator() { + public CellularBatteryStats createFromParcel(Parcel in) { + return new CellularBatteryStats(in); + } + + public CellularBatteryStats[] newArray(int size) { + return new CellularBatteryStats[size]; + } + }; + + public CellularBatteryStats() { + initialize(); + } + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mLoggingDurationMs); + out.writeLong(mKernelActiveTimeMs); + out.writeLong(mNumPacketsTx); + out.writeLong(mNumBytesTx); + out.writeLong(mNumPacketsRx); + out.writeLong(mNumBytesRx); + out.writeLong(mSleepTimeMs); + out.writeLong(mIdleTimeMs); + out.writeLong(mRxTimeMs); + out.writeLong(mEnergyConsumedMaMs); + out.writeLongArray(mTimeInRatMs); + out.writeLongArray(mTimeInRxSignalStrengthLevelMs); + out.writeLongArray(mTxTimeMs); + } + + public void readFromParcel(Parcel in) { + mLoggingDurationMs = in.readLong(); + mKernelActiveTimeMs = in.readLong(); + mNumPacketsTx = in.readLong(); + mNumBytesTx = in.readLong(); + mNumPacketsRx = in.readLong(); + mNumBytesRx = in.readLong(); + mSleepTimeMs = in.readLong(); + mIdleTimeMs = in.readLong(); + mRxTimeMs = in.readLong(); + mEnergyConsumedMaMs = in.readLong(); + in.readLongArray(mTimeInRatMs); + in.readLongArray(mTimeInRxSignalStrengthLevelMs); + in.readLongArray(mTxTimeMs); + } + + public long getLoggingDurationMs() { + return mLoggingDurationMs; + } + + public long getKernelActiveTimeMs() { + return mKernelActiveTimeMs; + } + + public long getNumPacketsTx() { + return mNumPacketsTx; + } + + public long getNumBytesTx() { + return mNumBytesTx; + } + + public long getNumPacketsRx() { + return mNumPacketsRx; + } + + public long getNumBytesRx() { + return mNumBytesRx; + } + + public long getSleepTimeMs() { + return mSleepTimeMs; + } + + public long getIdleTimeMs() { + return mIdleTimeMs; + } + + public long getRxTimeMs() { + return mRxTimeMs; + } + + public long getEnergyConsumedMaMs() { + return mEnergyConsumedMaMs; + } + + public long[] getTimeInRatMs() { + return mTimeInRatMs; + } + + public long[] getTimeInRxSignalStrengthLevelMs() { + return mTimeInRxSignalStrengthLevelMs; + } + + public long[] getTxTimeMs() { + return mTxTimeMs; + } + + public void setLoggingDurationMs(long t) { + mLoggingDurationMs = t; + return; + } + + public void setKernelActiveTimeMs(long t) { + mKernelActiveTimeMs = t; + return; + } + + public void setNumPacketsTx(long n) { + mNumPacketsTx = n; + return; + } + + public void setNumBytesTx(long b) { + mNumBytesTx = b; + return; + } + + public void setNumPacketsRx(long n) { + mNumPacketsRx = n; + return; + } + + public void setNumBytesRx(long b) { + mNumBytesRx = b; + return; + } + + public void setSleepTimeMs(long t) { + mSleepTimeMs = t; + return; + } + + public void setIdleTimeMs(long t) { + mIdleTimeMs = t; + return; + } + + public void setRxTimeMs(long t) { + mRxTimeMs = t; + return; + } + + public void setEnergyConsumedMaMs(long e) { + mEnergyConsumedMaMs = e; + return; + } + + public void setTimeInRatMs(long[] t) { + mTimeInRatMs = Arrays.copyOfRange(t, 0, + Math.min(t.length, BatteryStats.NUM_DATA_CONNECTION_TYPES)); + return; + } + + public void setTimeInRxSignalStrengthLevelMs(long[] t) { + mTimeInRxSignalStrengthLevelMs = Arrays.copyOfRange(t, 0, + Math.min(t.length, SignalStrength.NUM_SIGNAL_STRENGTH_BINS)); + return; + } + + public void setTxTimeMs(long[] t) { + mTxTimeMs = Arrays.copyOfRange(t, 0, Math.min(t.length, ModemActivityInfo.TX_POWER_LEVELS)); + return; + } + + public int describeContents() { + return 0; + } + + private CellularBatteryStats(Parcel in) { + initialize(); + readFromParcel(in); + } + + private void initialize() { + mLoggingDurationMs = 0; + mKernelActiveTimeMs = 0; + mNumPacketsTx = 0; + mNumBytesTx = 0; + mNumPacketsRx = 0; + mNumBytesRx = 0; + mSleepTimeMs = 0; + mIdleTimeMs = 0; + mRxTimeMs = 0; + mEnergyConsumedMaMs = 0; + mTimeInRatMs = new long[BatteryStats.NUM_DATA_CONNECTION_TYPES]; + Arrays.fill(mTimeInRatMs, 0); + mTimeInRxSignalStrengthLevelMs = new long[SignalStrength.NUM_SIGNAL_STRENGTH_BINS]; + Arrays.fill(mTimeInRxSignalStrengthLevelMs, 0); + mTxTimeMs = new long[ModemActivityInfo.TX_POWER_LEVELS]; + Arrays.fill(mTxTimeMs, 0); + return; + } +} \ No newline at end of file diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java index 4796712f..4c587a83 100644 --- a/android/os/storage/StorageManager.java +++ b/android/os/storage/StorageManager.java @@ -1115,12 +1115,14 @@ public class StorageManager { /** {@hide} */ public static Pair getPrimaryStoragePathAndSize() { return Pair.create(null, - FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace())); + FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace() + + Environment.getRootDirectory().getTotalSpace())); } /** {@hide} */ public long getPrimaryStorageSize() { - return FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()); + return FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace() + + Environment.getRootDirectory().getTotalSpace()); } /** {@hide} */ @@ -1690,7 +1692,7 @@ public class StorageManager { public static final int FLAG_ALLOCATE_DEFY_HALF_RESERVED = 1 << 2; /** @hide */ - @IntDef(flag = true, value = { + @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = { FLAG_ALLOCATE_AGGRESSIVE, FLAG_ALLOCATE_DEFY_ALL_RESERVED, FLAG_ALLOCATE_DEFY_HALF_RESERVED, diff --git a/android/os/storage/StorageVolume.java b/android/os/storage/StorageVolume.java index 1fc0b820..070b8c1b 100644 --- a/android/os/storage/StorageVolume.java +++ b/android/os/storage/StorageVolume.java @@ -19,7 +19,6 @@ package android.os.storage; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; -import android.net.TrafficStats; import android.net.Uri; import android.os.Environment; import android.os.Parcel; @@ -78,13 +77,11 @@ import java.io.File; public final class StorageVolume implements Parcelable { private final String mId; - private final int mStorageId; private final File mPath; private final String mDescription; private final boolean mPrimary; private final boolean mRemovable; private final boolean mEmulated; - private final long mMtpReserveSize; private final boolean mAllowMassStorage; private final long mMaxFileSize; private final UserHandle mOwner; @@ -121,17 +118,15 @@ public final class StorageVolume implements Parcelable { public static final int STORAGE_ID_PRIMARY = 0x00010001; /** {@hide} */ - public StorageVolume(String id, int storageId, File path, String description, boolean primary, - boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage, + public StorageVolume(String id, File path, String description, boolean primary, + boolean removable, boolean emulated, boolean allowMassStorage, long maxFileSize, UserHandle owner, String fsUuid, String state) { mId = Preconditions.checkNotNull(id); - mStorageId = storageId; mPath = Preconditions.checkNotNull(path); mDescription = Preconditions.checkNotNull(description); mPrimary = primary; mRemovable = removable; mEmulated = emulated; - mMtpReserveSize = mtpReserveSize; mAllowMassStorage = allowMassStorage; mMaxFileSize = maxFileSize; mOwner = Preconditions.checkNotNull(owner); @@ -141,13 +136,11 @@ public final class StorageVolume implements Parcelable { private StorageVolume(Parcel in) { mId = in.readString(); - mStorageId = in.readInt(); mPath = new File(in.readString()); mDescription = in.readString(); mPrimary = in.readInt() != 0; mRemovable = in.readInt() != 0; mEmulated = in.readInt() != 0; - mMtpReserveSize = in.readLong(); mAllowMassStorage = in.readInt() != 0; mMaxFileSize = in.readLong(); mOwner = in.readParcelable(null); @@ -210,34 +203,6 @@ public final class StorageVolume implements Parcelable { return mEmulated; } - /** - * Returns the MTP storage ID for the volume. - * this is also used for the storage_id column in the media provider. - * - * @return MTP storage ID - * @hide - */ - public int getStorageId() { - return mStorageId; - } - - /** - * Number of megabytes of space to leave unallocated by MTP. - * MTP will subtract this value from the free space it reports back - * to the host via GetStorageInfo, and will not allow new files to - * be added via MTP if there is less than this amount left free in the storage. - * If MTP has dedicated storage this value should be zero, but if MTP is - * sharing storage with the rest of the system, set this to a positive value - * to ensure that MTP activity does not result in the storage being - * too close to full. - * - * @return MTP reserve space - * @hide - */ - public int getMtpReserveSpace() { - return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES); - } - /** * Returns true if this volume can be shared via USB mass storage. * @@ -385,13 +350,11 @@ public final class StorageVolume implements Parcelable { pw.println("StorageVolume:"); pw.increaseIndent(); pw.printPair("mId", mId); - pw.printPair("mStorageId", mStorageId); pw.printPair("mPath", mPath); pw.printPair("mDescription", mDescription); pw.printPair("mPrimary", mPrimary); pw.printPair("mRemovable", mRemovable); pw.printPair("mEmulated", mEmulated); - pw.printPair("mMtpReserveSize", mMtpReserveSize); pw.printPair("mAllowMassStorage", mAllowMassStorage); pw.printPair("mMaxFileSize", mMaxFileSize); pw.printPair("mOwner", mOwner); @@ -420,13 +383,11 @@ public final class StorageVolume implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mId); - parcel.writeInt(mStorageId); parcel.writeString(mPath.toString()); parcel.writeString(mDescription); parcel.writeInt(mPrimary ? 1 : 0); parcel.writeInt(mRemovable ? 1 : 0); parcel.writeInt(mEmulated ? 1 : 0); - parcel.writeLong(mMtpReserveSize); parcel.writeInt(mAllowMassStorage ? 1 : 0); parcel.writeLong(mMaxFileSize); parcel.writeParcelable(mOwner, flags); diff --git a/android/os/storage/VolumeInfo.java b/android/os/storage/VolumeInfo.java index 76f79f13..d3877cac 100644 --- a/android/os/storage/VolumeInfo.java +++ b/android/os/storage/VolumeInfo.java @@ -343,9 +343,7 @@ public class VolumeInfo implements Parcelable { String description = null; String derivedFsUuid = fsUuid; - long mtpReserveSize = 0; long maxFileSize = 0; - int mtpStorageId = StorageVolume.STORAGE_ID_INVALID; if (type == TYPE_EMULATED) { emulated = true; @@ -356,12 +354,6 @@ public class VolumeInfo implements Parcelable { derivedFsUuid = privateVol.fsUuid; } - if (isPrimary()) { - mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY; - } - - mtpReserveSize = storage.getStorageLowBytes(userPath); - if (ID_EMULATED_INTERNAL.equals(id)) { removable = false; } else { @@ -374,14 +366,6 @@ public class VolumeInfo implements Parcelable { description = storage.getBestVolumeDescription(this); - if (isPrimary()) { - mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY; - } else { - // Since MediaProvider currently persists this value, we need a - // value that is stable over time. - mtpStorageId = buildStableMtpStorageId(fsUuid); - } - if ("vfat".equals(fsType)) { maxFileSize = 4294967295L; } @@ -394,8 +378,8 @@ public class VolumeInfo implements Parcelable { description = context.getString(android.R.string.unknownName); } - return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable, - emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId), + return new StorageVolume(id, userPath, description, isPrimary(), removable, + emulated, allowMassStorage, maxFileSize, new UserHandle(userId), derivedFsUuid, envState); } diff --git a/android/print/PrintAttributes.java b/android/print/PrintAttributes.java index ce5b11ee..f0d9a0ce 100644 --- a/android/print/PrintAttributes.java +++ b/android/print/PrintAttributes.java @@ -49,8 +49,9 @@ import java.util.Map; public final class PrintAttributes implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { - COLOR_MODE_MONOCHROME, COLOR_MODE_COLOR + @IntDef(flag = true, prefix = { "COLOR_MODE_" }, value = { + COLOR_MODE_MONOCHROME, + COLOR_MODE_COLOR }) @interface ColorMode { } @@ -64,8 +65,10 @@ public final class PrintAttributes implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { - DUPLEX_MODE_NONE, DUPLEX_MODE_LONG_EDGE, DUPLEX_MODE_SHORT_EDGE + @IntDef(flag = true, prefix = { "DUPLEX_MODE_" }, value = { + DUPLEX_MODE_NONE, + DUPLEX_MODE_LONG_EDGE, + DUPLEX_MODE_SHORT_EDGE }) @interface DuplexMode { } diff --git a/android/print/PrintDocumentInfo.java b/android/print/PrintDocumentInfo.java index 61434041..55c902e1 100644 --- a/android/print/PrintDocumentInfo.java +++ b/android/print/PrintDocumentInfo.java @@ -83,11 +83,14 @@ public final class PrintDocumentInfo implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ - CONTENT_TYPE_UNKNOWN, CONTENT_TYPE_DOCUMENT, CONTENT_TYPE_PHOTO + @IntDef(prefix = { "CONTENT_TYPE_" }, value = { + CONTENT_TYPE_UNKNOWN, + CONTENT_TYPE_DOCUMENT, + CONTENT_TYPE_PHOTO }) public @interface ContentType { } + /** * Content type: unknown. */ diff --git a/android/print/PrintJobInfo.java b/android/print/PrintJobInfo.java index 94686a8e..85fdd642 100644 --- a/android/print/PrintJobInfo.java +++ b/android/print/PrintJobInfo.java @@ -45,9 +45,14 @@ import java.util.Arrays; public final class PrintJobInfo implements Parcelable { /** @hide */ - @IntDef({ - STATE_CREATED, STATE_QUEUED, STATE_STARTED, STATE_BLOCKED, STATE_COMPLETED, - STATE_FAILED, STATE_CANCELED + @IntDef(prefix = { "STATE_" }, value = { + STATE_CREATED, + STATE_QUEUED, + STATE_STARTED, + STATE_BLOCKED, + STATE_COMPLETED, + STATE_FAILED, + STATE_CANCELED }) @Retention(RetentionPolicy.SOURCE) public @interface State { diff --git a/android/print/PrinterInfo.java b/android/print/PrinterInfo.java index 88feab7f..e79cc651 100644 --- a/android/print/PrinterInfo.java +++ b/android/print/PrinterInfo.java @@ -53,12 +53,15 @@ import java.lang.annotation.RetentionPolicy; public final class PrinterInfo implements Parcelable { /** @hide */ - @IntDef({ - STATUS_IDLE, STATUS_BUSY, STATUS_UNAVAILABLE + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_IDLE, + STATUS_BUSY, + STATUS_UNAVAILABLE }) @Retention(RetentionPolicy.SOURCE) public @interface Status { } + /** Printer status: the printer is idle and ready to print. */ public static final int STATUS_IDLE = PrinterInfoProto.STATUS_IDLE; diff --git a/android/privacy/DifferentialPrivacyConfig.java b/android/privacy/DifferentialPrivacyConfig.java new file mode 100644 index 00000000..e14893e7 --- /dev/null +++ b/android/privacy/DifferentialPrivacyConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy; + +/** + * An interface for differential privacy configuration. + * {@link DifferentialPrivacyEncoder} will apply this configuration to do differential privacy + * encoding. + * + * @hide + */ +public interface DifferentialPrivacyConfig { + + /** + * Returns the name of the algorithm used in differential privacy config. + * + * @return The name of the algorithm + */ + String getAlgorithm(); +} diff --git a/android/privacy/DifferentialPrivacyEncoder.java b/android/privacy/DifferentialPrivacyEncoder.java new file mode 100644 index 00000000..9355d6a5 --- /dev/null +++ b/android/privacy/DifferentialPrivacyEncoder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy; + +/** + * An interface for differential privacy encoder. + * Applications can use it to convert privacy sensitive data to privacy protected report. + * There is no decoder implemented in Android as it is not possible decode a single report by + * design. + * + *

      Each type of log should have its own encoder, otherwise it may leak + * some information about Permanent Randomized Response(PRR, is used to create a “noisy” + * answer which is memoized by the client and permanently reused in place of the real answer). + * + *

      Some encoders may not support all encoding methods, and it will throw {@link + * UnsupportedOperationException} if you call unsupported encoding method. + * + *

      WARNING: Privacy protection works only when encoder uses a suitable DP configuration, + * and the configuration and algorithm that is suitable is highly dependent on the use case. + * If the configuration is not suitable for the use case, it may hurt privacy or utility or both. + * + * @hide + */ +public interface DifferentialPrivacyEncoder { + + /** + * Apply differential privacy to encode a string. + * + * @param original An arbitrary string + * @return Differential privacy encoded bytes derived from the string + */ + byte[] encodeString(String original); + + /** + * Apply differential privacy to encode a boolean. + * + * @param original An arbitrary boolean. + * @return Differential privacy encoded bytes derived from the boolean + */ + byte[] encodeBoolean(boolean original); + + /** + * Apply differential privacy to encode sequence of bytes. + * + * @param original An arbitrary byte array. + * @return Differential privacy encoded bytes derived from the bytes + */ + byte[] encodeBits(byte[] original); + + /** + * Returns the configuration that this encoder is using. + */ + DifferentialPrivacyConfig getConfig(); + + /** + * Return True if the output from encoder is NOT securely randomized, otherwise encoder should + * be secure to randomize input. + * + * A non-secure encoder is intended only for testing only and must not be used to process + * real data. + * + */ + boolean isInsecureEncoderForTest(); +} diff --git a/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java b/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java new file mode 100644 index 00000000..ee910fc1 --- /dev/null +++ b/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy.internal.longitudinalreporting; + +import android.privacy.DifferentialPrivacyConfig; +import android.privacy.internal.rappor.RapporConfig; +import android.text.TextUtils; + +import com.android.internal.util.Preconditions; + +/** + * A class to store {@link LongitudinalReportingEncoder} configuration. + * + *

        + *
      • f is probability to flip input value, used in IRR. + *
      • p is probability to override input value, used in PRR1. + *
      • q is probability to set input value as 1 when result of PRR(p) is true, used in PRR2. + *
      + * + * @hide + */ +public class LongitudinalReportingConfig implements DifferentialPrivacyConfig { + + private static final String ALGORITHM_NAME = "LongitudinalReporting"; + + // Probability to flip input value. + private final double mProbabilityF; + + // Probability to override original value. + private final double mProbabilityP; + // Probability to override value with 1. + private final double mProbabilityQ; + + // IRR config to randomize original value + private final RapporConfig mIRRConfig; + + private final String mEncoderId; + + /** + * Constructor to create {@link LongitudinalReportingConfig} used for {@link + * LongitudinalReportingEncoder} + * + * @param encoderId Unique encoder id. + * @param probabilityF Probability F used in Longitudinal Reporting algorithm. + * @param probabilityP Probability P used in Longitudinal Reporting algorithm. This will be + * quantized to the nearest 1/256. + * @param probabilityQ Probability Q used in Longitudinal Reporting algorithm. This will be + * quantized to the nearest 1/256. + */ + public LongitudinalReportingConfig(String encoderId, double probabilityF, + double probabilityP, double probabilityQ) { + Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1, + "probabilityF must be in range [0.0, 1.0]"); + this.mProbabilityF = probabilityF; + Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1, + "probabilityP must be in range [0.0, 1.0]"); + this.mProbabilityP = probabilityP; + Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1, + "probabilityQ must be in range [0.0, 1.0]"); + this.mProbabilityQ = probabilityQ; + Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty"); + mEncoderId = encoderId; + mIRRConfig = new RapporConfig(encoderId, 1, 0.0, probabilityF, 1 - probabilityF, 1, 1); + } + + @Override + public String getAlgorithm() { + return ALGORITHM_NAME; + } + + RapporConfig getIRRConfig() { + return mIRRConfig; + } + + double getProbabilityP() { + return mProbabilityP; + } + + double getProbabilityQ() { + return mProbabilityQ; + } + + String getEncoderId() { + return mEncoderId; + } + + @Override + public String toString() { + return String.format("EncoderId: %s, ProbabilityF: %.3f, ProbabilityP: %.3f" + + ", ProbabilityQ: %.3f", + mEncoderId, mProbabilityF, mProbabilityP, mProbabilityQ); + } +} diff --git a/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java b/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java new file mode 100644 index 00000000..219868d6 --- /dev/null +++ b/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java @@ -0,0 +1,170 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy.internal.longitudinalreporting; + +import android.privacy.DifferentialPrivacyEncoder; +import android.privacy.internal.rappor.RapporConfig; +import android.privacy.internal.rappor.RapporEncoder; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Differential privacy encoder by using Longitudinal Reporting algorithm. + * + * + * Notes: It supports encodeBoolean() only for now. + * + * + *

      + * Definition: + * PRR = Permanent Randomized Response + * IRR = Instantaneous Randomized response + * + * Algorithm: + * Step 1: Create long-term secrets x(ignoreOriginalInput)=Ber(P), y=Ber(Q), where Ber denotes + * Bernoulli distribution on {0, 1}, and we use it as a long-term secret, we implement Ber(x) by + * using PRR(2x, 0) when x < 1/2, PRR(2(1-x), 1) when x >= 1/2. + * Step 2: If x is 0, report IRR(F, original), otherwise report IRR(F, y) + *

      + * + * Reference: go/bit-reporting-with-longitudinal-privacy + * TODO: Add a public blog / site to explain how it works. + * + * @hide + */ +public class LongitudinalReportingEncoder implements DifferentialPrivacyEncoder { + + // Suffix that will be added to Rappor's encoder id. There's a (relatively) small risk some + // other Rappor encoder may re-use the same encoder id. + private static final String PRR1_ENCODER_ID = "prr1_encoder_id"; + private static final String PRR2_ENCODER_ID = "prr2_encoder_id"; + + private final LongitudinalReportingConfig mConfig; + + // IRR encoder to encode input value. + private final RapporEncoder mIRREncoder; + + // A value that used to replace original value as input, so there's always a chance we are + // doing IRR on a fake value not actual original value. + // Null if original value does not need to be replaced. + private final Boolean mFakeValue; + + // True if encoder is securely randomized. + private final boolean mIsSecure; + + /** + * Create {@link LongitudinalReportingEncoder} with + * {@link LongitudinalReportingConfig} provided. + * + * @param config Longitudinal Reporting parameters to encode input + * @param userSecret User generated secret that used to generate PRR + * @return {@link LongitudinalReportingEncoder} instance + */ + public static LongitudinalReportingEncoder createEncoder(LongitudinalReportingConfig config, + byte[] userSecret) { + return new LongitudinalReportingEncoder(config, true, userSecret); + } + + /** + * Create insecure {@link LongitudinalReportingEncoder} with + * {@link LongitudinalReportingConfig} provided. + * Should not use it to process sensitive data. + * + * @param config Rappor parameters to encode input. + * @return {@link LongitudinalReportingEncoder} instance. + */ + @VisibleForTesting + public static LongitudinalReportingEncoder createInsecureEncoderForTest( + LongitudinalReportingConfig config) { + return new LongitudinalReportingEncoder(config, false, null); + } + + private LongitudinalReportingEncoder(LongitudinalReportingConfig config, + boolean secureEncoder, byte[] userSecret) { + mConfig = config; + mIsSecure = secureEncoder; + final boolean ignoreOriginalInput = getLongTermRandomizedResult(config.getProbabilityP(), + secureEncoder, userSecret, config.getEncoderId() + PRR1_ENCODER_ID); + + if (ignoreOriginalInput) { + mFakeValue = getLongTermRandomizedResult(config.getProbabilityQ(), + secureEncoder, userSecret, config.getEncoderId() + PRR2_ENCODER_ID); + } else { + // Not using fake value, so IRR will be processed on real input value. + mFakeValue = null; + } + + final RapporConfig irrConfig = config.getIRRConfig(); + mIRREncoder = secureEncoder + ? RapporEncoder.createEncoder(irrConfig, userSecret) + : RapporEncoder.createInsecureEncoderForTest(irrConfig); + } + + @Override + public byte[] encodeString(String original) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] encodeBoolean(boolean original) { + if (mFakeValue != null) { + // Use the fake value generated in PRR. + original = mFakeValue.booleanValue(); + } + return mIRREncoder.encodeBoolean(original); + } + + @Override + public byte[] encodeBits(byte[] bits) { + throw new UnsupportedOperationException(); + } + + @Override + public LongitudinalReportingConfig getConfig() { + return mConfig; + } + + @Override + public boolean isInsecureEncoderForTest() { + return !mIsSecure; + } + + /** + * Get PRR result that with probability p is 1, probability 1-p is 0. + */ + @VisibleForTesting + public static boolean getLongTermRandomizedResult(double p, boolean secureEncoder, + byte[] userSecret, String encoderId) { + // Use Rappor to get PRR result. Rappor's P and Q are set to 0 and 1 so IRR will not be + // effective. + // As Rappor has rapporF/2 chance returns 0, rapporF/2 chance returns 1, and 1-rapporF + // chance returns original input. + // If p < 0.5, setting rapporF=2p and input=0 will make Rappor has p chance to return 1 + // P(output=1 | input=0) = rapporF/2 = 2p/2 = p. + // If p >= 0.5, setting rapporF=2(1-p) and input=1 will make Rappor has p chance + // to return 1. + // P(output=1 | input=1) = rapporF/2 + (1 - rapporF) = 2(1-p)/2 + (1 - 2(1-p)) = p. + final double effectiveF = p < 0.5f ? p * 2 : (1 - p) * 2; + final boolean prrInput = p < 0.5f ? false : true; + final RapporConfig prrConfig = new RapporConfig(encoderId, 1, effectiveF, + 0, 1, 1, 1); + final RapporEncoder encoder = secureEncoder + ? RapporEncoder.createEncoder(prrConfig, userSecret) + : RapporEncoder.createInsecureEncoderForTest(prrConfig); + return encoder.encodeBoolean(prrInput)[0] > 0; + } +} diff --git a/android/privacy/internal/rappor/RapporConfig.java b/android/privacy/internal/rappor/RapporConfig.java new file mode 100644 index 00000000..221999bf --- /dev/null +++ b/android/privacy/internal/rappor/RapporConfig.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy.internal.rappor; + +import android.privacy.DifferentialPrivacyConfig; +import android.text.TextUtils; + +import com.android.internal.util.Preconditions; + +/** + * A class to store {@link RapporEncoder} config. + * + * @hide + */ +public class RapporConfig implements DifferentialPrivacyConfig { + + private static final String ALGORITHM_NAME = "Rappor"; + + final String mEncoderId; + final int mNumBits; + final double mProbabilityF; + final double mProbabilityP; + final double mProbabilityQ; + final int mNumCohorts; + final int mNumBloomHashes; + + /** + * Constructor for {@link RapporConfig}. + * + * @param encoderId Unique id for encoder. + * @param numBits Number of bits to be encoded in Rappor algorithm. + * @param probabilityF Probability F that used in Rappor algorithm. This will be + * quantized to the nearest 1/128. + * @param probabilityP Probability P that used in Rappor algorithm. + * @param probabilityQ Probability Q that used in Rappor algorithm. + * @param numCohorts Number of cohorts that used in Rappor algorithm. + * @param numBloomHashes Number of bloom hashes that used in Rappor algorithm. + */ + public RapporConfig(String encoderId, int numBits, double probabilityF, + double probabilityP, double probabilityQ, int numCohorts, int numBloomHashes) { + Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty"); + this.mEncoderId = encoderId; + Preconditions.checkArgument(numBits > 0, "numBits needs to be > 0"); + this.mNumBits = numBits; + Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1, + "probabilityF must be in range [0.0, 1.0]"); + this.mProbabilityF = probabilityF; + Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1, + "probabilityP must be in range [0.0, 1.0]"); + this.mProbabilityP = probabilityP; + Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1, + "probabilityQ must be in range [0.0, 1.0]"); + this.mProbabilityQ = probabilityQ; + Preconditions.checkArgument(numCohorts > 0, "numCohorts needs to be > 0"); + this.mNumCohorts = numCohorts; + Preconditions.checkArgument(numBloomHashes > 0, "numBloomHashes needs to be > 0"); + this.mNumBloomHashes = numBloomHashes; + } + + @Override + public String getAlgorithm() { + return ALGORITHM_NAME; + } + + @Override + public String toString() { + return String.format( + "EncoderId: %s, NumBits: %d, ProbabilityF: %.3f, ProbabilityP: %.3f" + + ", ProbabilityQ: %.3f, NumCohorts: %d, NumBloomHashes: %d", + mEncoderId, mNumBits, mProbabilityF, mProbabilityP, mProbabilityQ, + mNumCohorts, mNumBloomHashes); + } +} diff --git a/android/privacy/internal/rappor/RapporEncoder.java b/android/privacy/internal/rappor/RapporEncoder.java new file mode 100644 index 00000000..2eca4c98 --- /dev/null +++ b/android/privacy/internal/rappor/RapporEncoder.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.privacy.internal.rappor; + +import android.privacy.DifferentialPrivacyEncoder; + +import com.google.android.rappor.Encoder; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * Differential privacy encoder by using + * RAPPOR + * algorithm. + * + * @hide + */ +public class RapporEncoder implements DifferentialPrivacyEncoder { + + // Hard-coded seed and secret for insecure encoder + private static final long INSECURE_RANDOM_SEED = 0x12345678L; + private static final byte[] INSECURE_SECRET = new byte[]{ + (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93, + (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54, + (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93, + (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54, + (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93, + (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54, + (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54 + }; + private static final SecureRandom sSecureRandom = new SecureRandom(); + + private final RapporConfig mConfig; + + // Rappor encoder + private final Encoder mEncoder; + // True if encoder is secure (seed is securely randomized) + private final boolean mIsSecure; + + + private RapporEncoder(RapporConfig config, boolean secureEncoder, byte[] userSecret) { + mConfig = config; + mIsSecure = secureEncoder; + final Random random; + if (secureEncoder) { + // Use SecureRandom as random generator. + random = sSecureRandom; + } else { + // Hard-coded random generator, to have deterministic result. + random = new Random(INSECURE_RANDOM_SEED); + userSecret = INSECURE_SECRET; + } + mEncoder = new Encoder(random, null, null, + userSecret, config.mEncoderId, config.mNumBits, + config.mProbabilityF, config.mProbabilityP, config.mProbabilityQ, + config.mNumCohorts, config.mNumBloomHashes); + } + + /** + * Create {@link RapporEncoder} with {@link RapporConfig} and user secret provided. + * + * @param config Rappor parameters to encode input. + * @param userSecret Per device unique secret key. + * @return {@link RapporEncoder} instance. + */ + public static RapporEncoder createEncoder(RapporConfig config, byte[] userSecret) { + return new RapporEncoder(config, true, userSecret); + } + + /** + * Create insecure {@link RapporEncoder} with {@link RapporConfig} provided. + * Should not use it to process sensitive data. + * + * @param config Rappor parameters to encode input. + * @return {@link RapporEncoder} instance. + */ + public static RapporEncoder createInsecureEncoderForTest(RapporConfig config) { + return new RapporEncoder(config, false, null); + } + + @Override + public byte[] encodeString(String original) { + return mEncoder.encodeString(original); + } + + @Override + public byte[] encodeBoolean(boolean original) { + return mEncoder.encodeBoolean(original); + } + + @Override + public byte[] encodeBits(byte[] bits) { + return mEncoder.encodeBits(bits); + } + + @Override + public RapporConfig getConfig() { + return mConfig; + } + + @Override + public boolean isInsecureEncoderForTest() { + return !mIsSecure; + } +} diff --git a/android/provider/AlarmClock.java b/android/provider/AlarmClock.java index f9030124..21694575 100644 --- a/android/provider/AlarmClock.java +++ b/android/provider/AlarmClock.java @@ -158,7 +158,6 @@ public final class AlarmClock { *

      * Dismiss all currently expired timers. If there are no expired timers, then this is a no-op. *

      - * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_DISMISS_TIMER = "android.intent.action.DISMISS_TIMER"; diff --git a/android/provider/CallLog.java b/android/provider/CallLog.java index a8acb976..60df467b 100644 --- a/android/provider/CallLog.java +++ b/android/provider/CallLog.java @@ -212,16 +212,25 @@ public class CallLog { public static final String FEATURES = "features"; /** Call had video. */ - public static final int FEATURES_VIDEO = 0x1; + public static final int FEATURES_VIDEO = 1 << 0; /** Call was pulled externally. */ - public static final int FEATURES_PULLED_EXTERNALLY = 0x2; + public static final int FEATURES_PULLED_EXTERNALLY = 1 << 1; /** Call was HD. */ - public static final int FEATURES_HD_CALL = 0x4; + public static final int FEATURES_HD_CALL = 1 << 2; /** Call was WIFI call. */ - public static final int FEATURES_WIFI = 0x8; + public static final int FEATURES_WIFI = 1 << 3; + + /** Call was on RTT at some point */ + public static final int FEATURES_RTT = 1 << 4; + + /** + * Indicates the call underwent Assisted Dialing. + * @hide + */ + public static final Integer FEATURES_ASSISTED_DIALING_USED = 0x10; /** * The phone number as the user entered it. diff --git a/android/provider/ContactsContract.java b/android/provider/ContactsContract.java index cc1c0677..c94da9a1 100644 --- a/android/provider/ContactsContract.java +++ b/android/provider/ContactsContract.java @@ -22,6 +22,7 @@ import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.app.Activity; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; @@ -42,10 +43,12 @@ import android.database.DatabaseUtils; import android.graphics.Rect; import android.net.Uri; import android.os.RemoteException; +import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Pair; import android.view.View; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -4237,6 +4240,45 @@ public final class ContactsContract { * current carrier. An allowed bitmask of {@link #CARRIER_PRESENCE}. */ public static final int CARRIER_PRESENCE_VT_CAPABLE = 0x01; + + /** + * The flattened {@link android.content.ComponentName} of a {@link + * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to + * call the contact with. + * + *

      On a multi-SIM device this field can be used in a {@link CommonDataKinds.Phone} row + * to indicate the {@link PhoneAccountHandle} to call the number with, instead of using + * {@link android.telecom.TelecomManager#getDefaultOutgoingPhoneAccount(String)} or asking + * every time. + * + *

      {@link android.telecom.TelecomManager#placeCall(Uri, android.os.Bundle)} + * should be called with {@link android.telecom.TelecomManager#EXTRA_PHONE_ACCOUNT_HANDLE} + * set to the {@link PhoneAccountHandle} using the {@link ComponentName} from this field. + * + * @see #PREFERRED_PHONE_ACCOUNT_ID + * @see PhoneAccountHandle#getComponentName() + * @see ComponentName#flattenToString() + */ + String PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME = "preferred_phone_account_component_name"; + + /** + * The ID of a {@link + * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to + * call the contact with. Used by {@link CommonDataKinds.Phone}. + * + *

      On a multi-SIM device this field can be used in a {@link CommonDataKinds.Phone} row + * to indicate the {@link PhoneAccountHandle} to call the number with, instead of using + * {@link android.telecom.TelecomManager#getDefaultOutgoingPhoneAccount(String)} or asking + * every time. + * + *

      {@link android.telecom.TelecomManager#placeCall(Uri, android.os.Bundle)} + * should be called with {@link android.telecom.TelecomManager#EXTRA_PHONE_ACCOUNT_HANDLE} + * set to the {@link PhoneAccountHandle} using the id from this field. + * + * @see #PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + * @see PhoneAccountHandle#getId() + */ + String PREFERRED_PHONE_ACCOUNT_ID = "preferred_phone_account_id"; } /** diff --git a/android/provider/FontsContract.java b/android/provider/FontsContract.java index d8540ffd..a1d1c573 100644 --- a/android/provider/FontsContract.java +++ b/android/provider/FontsContract.java @@ -283,7 +283,11 @@ public class FontsContract { public static final int STATUS_REJECTED = 3; /** @hide */ - @IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED}) + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_OK, + STATUS_WRONG_CERTIFICATES, + STATUS_UNEXPECTED_DATA_PROVIDED + }) @Retention(RetentionPolicy.SOURCE) @interface FontResultStatus {} @@ -438,9 +442,13 @@ public class FontsContract { public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY; /** @hide */ - @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR, - FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE, - FAIL_REASON_MALFORMED_QUERY }) + @IntDef(prefix = { "FAIL_" }, value = { + FAIL_REASON_PROVIDER_NOT_FOUND, + FAIL_REASON_FONT_LOAD_ERROR, + FAIL_REASON_FONT_NOT_FOUND, + FAIL_REASON_FONT_UNAVAILABLE, + FAIL_REASON_MALFORMED_QUERY + }) @Retention(RetentionPolicy.SOURCE) @interface FontRequestFailReason {} diff --git a/android/provider/MediaStore.java b/android/provider/MediaStore.java index 32d68cd9..d9808a3d 100644 --- a/android/provider/MediaStore.java +++ b/android/provider/MediaStore.java @@ -63,15 +63,6 @@ public final class MediaStore { private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; - /** - * Broadcast Action: A broadcast to indicate the end of an MTP session with the host. - * This broadcast is only sent if MTP activity has modified the media database during the - * most recent MTP session. - * - * @hide - */ - public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END"; - /** * The method name used by the media scanner and mtp to tell the media provider to * rescan and reclassify that have become unhidden because of renaming folders or diff --git a/android/provider/Settings.java b/android/provider/Settings.java index 1e0948a4..2f865141 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -1689,7 +1689,7 @@ public final class Settings { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ + @IntDef(prefix = { "RESET_MODE_" }, value = { RESET_MODE_PACKAGE_DEFAULTS, RESET_MODE_UNTRUSTED_DEFAULTS, RESET_MODE_UNTRUSTED_CHANGES, @@ -3123,6 +3123,10 @@ public final class Settings { * to dream after a period of inactivity. This value is also known as the * user activity timeout period since the screen isn't necessarily turned off * when it expires. + * + *

      + * This value is bounded by maximum timeout set by + * {@link android.app.admin.DevicePolicyManager#setMaximumTimeToLock(ComponentName, long)}. */ public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; @@ -5324,13 +5328,57 @@ public final class Settings { public static final String AUTOFILL_SERVICE = "autofill_service"; /** - * Experimental autofill feature. + * Boolean indicating if Autofill supports field classification. + * + * @see android.service.autofill.AutofillService + * + * @hide + */ + @SystemApi + @TestApi + public static final String AUTOFILL_FEATURE_FIELD_CLASSIFICATION = + "autofill_field_classification"; + + /** + * Defines value returned by {@link android.service.autofill.UserData#getMaxUserDataSize()}. + * + * @hide + */ + @SystemApi + @TestApi + public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE = + "autofill_user_data_max_user_data_size"; + + /** + * Defines value returned by + * {@link android.service.autofill.UserData#getMaxFieldClassificationIdsSize()}. + * + * @hide + */ + @SystemApi + @TestApi + public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE = + "autofill_user_data_max_field_classification_size"; + + /** + * Defines value returned by {@link android.service.autofill.UserData#getMaxValueLength()}. + * + * @hide + */ + @SystemApi + @TestApi + public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH = + "autofill_user_data_max_value_length"; + + /** + * Defines value returned by {@link android.service.autofill.UserData#getMinValueLength()}. * - *

      TODO(b/67867469): remove once feature is finished * @hide */ + @SystemApi @TestApi - public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection"; + public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH = + "autofill_user_data_min_value_length"; /** * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead @@ -5729,6 +5777,14 @@ public final class Settings { public static final String TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES = "touch_exploration_granted_accessibility_services"; + /** + * Uri of the slice that's presented on the keyguard. + * Defaults to a slice with the date and next alarm. + * + * @hide + */ + public static final String KEYGUARD_SLICE_URI = "keyguard_slice_uri"; + /** * Whether to speak passwords while in accessibility mode. * @@ -7204,8 +7260,11 @@ public final class Settings { * full_backup_interval_milliseconds (long) * full_backup_require_charging (boolean) * full_backup_required_network_type (int) + * backup_finished_notification_receivers (String[]) *

  • * + * backup_finished_notification_receivers uses ":" as delimeter for values. + * *

    * Type: string * @hide @@ -8650,6 +8709,12 @@ public final class Settings { public static final String WIFI_SCAN_ALWAYS_AVAILABLE = "wifi_scan_always_enabled"; + /** + * Whether soft AP will shut down after a timeout period when no devices are connected. + * @hide + */ + public static final String SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled"; + /** * Value to specify if Wi-Fi Wakeup feature is enabled. * @@ -9416,6 +9481,7 @@ public final class Settings { * service_min_restart_time_between (long) * service_max_inactivity (long) * service_bg_start_timeout (long) + * process_start_async (boolean) *

    * *

    @@ -9515,6 +9581,31 @@ public final class Settings { */ public static final String ANOMALY_DETECTION_CONSTANTS = "anomaly_detection_constants"; + /** + * Battery tip specific settings + * This is encoded as a key=value list, separated by commas. Ex: + * + * "battery_tip_enabled=true,summary_enabled=true,high_usage_enabled=true," + * "high_usage_app_count=3,reduced_battery_enabled=false,reduced_battery_percent=50" + * + * The following keys are supported: + * + *

    +         * battery_tip_enabled              (boolean)
    +         * summary_enabled                  (boolean)
    +         * battery_saver_tip_enabled        (boolean)
    +         * high_usage_enabled               (boolean)
    +         * high_usage_app_count             (int)
    +         * app_restriction_enabled          (boolean)
    +         * reduced_battery_enabled          (boolean)
    +         * reduced_battery_percent          (int)
    +         * low_battery_enabled              (boolean)
    +         * low_battery_hour                 (int)
    +         * 
    + * @hide + */ + public static final String BATTERY_TIP_CONSTANTS = "battery_tip_constants"; + /** * Always on display(AOD) specific settings * This is encoded as a key=value list, separated by commas. Ex: @@ -9524,8 +9615,8 @@ public final class Settings { * The following keys are supported: * *
    -         * screen_brightness_array         (string)
    -         * dimming_scrim_array             (string)
    +         * screen_brightness_array         (int[])
    +         * dimming_scrim_array             (int[])
              * prox_screen_off_delay           (long)
              * prox_cooldown_trigger           (long)
              * prox_cooldown_period            (long)
    @@ -9537,9 +9628,10 @@ public final class Settings {
             /**
              * App standby (app idle) specific settings.
              * This is encoded as a key=value list, separated by commas. Ex:
    -         *
    +         * 

    * "idle_duration=5000,parole_interval=4500" - * + *

    + * All durations are in millis. * The following keys are supported: * *

    @@ -9688,6 +9780,15 @@ public final class Settings {
              */
             public static final String TEXT_CLASSIFIER_CONSTANTS = "text_classifier_constants";
     
    +        /**
    +         * Whether or not App Standby feature is enabled. This controls throttling of apps
    +         * based on usage patterns and predictions.
    +         * Type: int (0 for false, 1 for true)
    +         * Default: 1
    +         * @hide
    +         */
    +        public static final java.lang.String APP_STANDBY_ENABLED = "app_standby_enabled";
    +
             /**
              * Get the key that retrieves a bluetooth headset's priority.
              * @hide
    @@ -10116,6 +10217,16 @@ public final class Settings {
              */
             public static final String POLICY_CONTROL = "policy_control";
     
    +        /**
    +         * {@link android.view.DisplayCutout DisplayCutout} emulation mode.
    +         *
    +         * @hide
    +         */
    +        public static final String EMULATE_DISPLAY_CUTOUT = "emulate_display_cutout";
    +
    +        /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_OFF = 0;
    +        /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_ON = 1;
    +
             /**
              * Defines global zen mode.  ZEN_MODE_OFF, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
              * or ZEN_MODE_NO_INTERRUPTIONS.
    @@ -10429,14 +10540,6 @@ public final class Settings {
             public static final String LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED =
                     "location_settings_link_to_permissions_enabled";
     
    -        /**
    -         * Flag to enable use of RefactoredBackupManagerService.
    -         *
    -         * @hide
    -         */
    -        public static final String BACKUP_REFACTORED_SERVICE_DISABLED =
    -            "backup_refactored_service_disabled";
    -
             /**
              * Flag to set the waiting time for euicc factory reset inside System > Settings
              * Type: long
    @@ -10501,7 +10604,17 @@ public final class Settings {
                 LOW_POWER_MODE_TRIGGER_LEVEL,
                 BLUETOOTH_ON,
                 PRIVATE_DNS_MODE,
    -            PRIVATE_DNS_SPECIFIER
    +            PRIVATE_DNS_SPECIFIER,
    +            SOFT_AP_TIMEOUT_ENABLED
    +        };
    +
    +        /**
    +         * Global settings that shouldn't be persisted.
    +         *
    +         * @hide
    +         */
    +        public static final String[] TRANSIENT_SETTINGS = {
    +                LOCATION_GLOBAL_KILL_SWITCH,
             };
     
             /** @hide */
    @@ -11031,6 +11144,7 @@ public final class Settings {
                 INSTANT_APP_SETTINGS.add(DEBUG_VIEW_ATTRIBUTES);
                 INSTANT_APP_SETTINGS.add(WTF_IS_FATAL);
                 INSTANT_APP_SETTINGS.add(SEND_ACTION_APP_ERROR);
    +            INSTANT_APP_SETTINGS.add(ZEN_MODE);
             }
     
             /**
    @@ -11075,7 +11189,7 @@ public final class Settings {
              *
              * 
              * default               (int)
    -         * options_array         (string)
    +         * options_array         (int[])
              * 
    * * All delays in integer minutes. Array order is respected. @@ -11084,6 +11198,28 @@ public final class Settings { */ public static final String NOTIFICATION_SNOOZE_OPTIONS = "notification_snooze_options"; + + /** + * Configuration flags for SQLite Compatibility WAL. Encoded as a key-value list, separated + * by commas. E.g.: compatibility_wal_supported=true, wal_syncmode=OFF + * + * Supported keys: + * compatibility_wal_supported (boolean) + * wal_syncmode (String) + * + * @hide + */ + public static final String SQLITE_COMPATIBILITY_WAL_FLAGS = + "sqlite_compatibility_wal_flags"; + + /** + * Enable GNSS Raw Measurements Full Tracking? + * 0 = no + * 1 = yes + * @hide + */ + public static final String ENABLE_GNSS_RAW_MEAS_FULL_TRACKING = + "enable_gnss_raw_meas_full_tracking"; } /** diff --git a/android/provider/Telephony.java b/android/provider/Telephony.java index d7b6142a..942ea009 100644 --- a/android/provider/Telephony.java +++ b/android/provider/Telephony.java @@ -3341,6 +3341,12 @@ public final class Telephony { */ public static final String APN = "apn"; + /** + * Prefix of Integrated Circuit Card Identifier. + *

    Type: TEXT

    + */ + public static final String ICCID_PREFIX = "iccid_prefix"; + /** * User facing carrier name. *

    Type: TEXT

    diff --git a/android/provider/VoicemailContract.java b/android/provider/VoicemailContract.java index 864a0fd7..6a3c55ef 100644 --- a/android/provider/VoicemailContract.java +++ b/android/provider/VoicemailContract.java @@ -16,7 +16,6 @@ package android.provider; -import android.Manifest; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; @@ -50,7 +49,7 @@ import java.util.List; * * *

    The minimum permission needed to access this content provider is - * {@link Manifest.permission#ADD_VOICEMAIL} + * {@link android.Manifest.permission#ADD_VOICEMAIL} * *

    Voicemails are inserted by what is called as a "voicemail source" * application, which is responsible for syncing voicemail data between a remote @@ -293,10 +292,25 @@ public class VoicemailContract { * Flag used to indicate that local, unsynced changes are present. * Currently, this is used to indicate that the voicemail was read or deleted. * The value will be 1 if dirty is true, 0 if false. + * + *

    When a caller updates a voicemail row (either with {@link ContentResolver#update} or + * {@link ContentResolver#applyBatch}), and if the {@link ContentValues} doesn't contain + * this column, the voicemail provider implicitly sets it to 0 if the calling package is + * the {@link #SOURCE_PACKAGE} or to 1 otherwise. To prevent this behavior, explicitly set + * {@link #DIRTY_RETAIN} to DIRTY in the {@link ContentValues}. + * *

    Type: INTEGER (boolean)

    + * + * @see #DIRTY_RETAIN */ public static final String DIRTY = "dirty"; + /** + * Value of {@link #DIRTY} when updating to indicate that the value should not be updated + * during this operation. + */ + public static final int DIRTY_RETAIN = -1; + /** * Flag used to indicate that the voicemail was deleted but not synced to the server. * A deleted row should be ignored. diff --git a/android/security/AttestedKeyPair.java b/android/security/AttestedKeyPair.java new file mode 100644 index 00000000..c6bff5c1 --- /dev/null +++ b/android/security/AttestedKeyPair.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security; + +import java.security.KeyPair; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The {@code AttestedKeyPair} class contains a {@code KeyPair} instance of + * keys generated by Keystore and owned by KeyChain, as well as an attestation + * record for the key. + * + *

    Such keys can be obtained by calling + * {@link android.app.admin.DevicePolicyManager#generateKeyPair}. + */ + +public final class AttestedKeyPair { + private final KeyPair mKeyPair; + private final Certificate[] mAttestationRecord; + + /** + * @hide Only created by the platform, no need to expose as public API. + */ + public AttestedKeyPair(KeyPair keyPair, Certificate[] attestationRecord) { + mKeyPair = keyPair; + mAttestationRecord = attestationRecord; + } + + /** + * Returns the generated key pair associated with the attestation record + * in this instance. + */ + public KeyPair getKeyPair() { + return mKeyPair; + } + + /** + * Returns the attestation record for the key pair in this instance. + * + * The attestation record is a chain of certificates. The leaf certificate links to the public + * key of this key pair and other properties of the key or the device. If the key is in secure + * hardware, and if the secure hardware supports attestation, the leaf certificate will be + * signed by a chain of certificates rooted at a trustworthy CA key. Otherwise the chain will be + * rooted at an untrusted certificate. + * + * The attestation record could be for properties of the key, or include device identifiers. + * + * See {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge} + * and + * Key Attestation for the format of the attestation record inside the certificate. + */ + public List getAttestationRecord() { + if (mAttestationRecord == null) { + return new ArrayList(); + } + return Arrays.asList(mAttestationRecord); + } +} diff --git a/android/security/Credentials.java b/android/security/Credentials.java index 6830a748..57db20be 100644 --- a/android/security/Credentials.java +++ b/android/security/Credentials.java @@ -60,10 +60,12 @@ public class Credentials { /** Key prefix for user certificates. */ public static final String USER_CERTIFICATE = "USRCERT_"; - /** Key prefix for user private keys. */ + /** Key prefix for user private and secret keys. */ public static final String USER_PRIVATE_KEY = "USRPKEY_"; - /** Key prefix for user secret keys. */ + /** Key prefix for user secret keys. + * @deprecated use {@code USER_PRIVATE_KEY} for this category instead. + */ public static final String USER_SECRET_KEY = "USRSKEY_"; /** Key prefix for VPN. */ @@ -235,8 +237,7 @@ public class Credentials { * Make sure every type is deleted. There can be all three types, so * don't use a conditional here. */ - return deletePrivateKeyTypeForAlias(keystore, alias, uid) - & deleteSecretKeyTypeForAlias(keystore, alias, uid) + return deleteUserKeyTypeForAlias(keystore, alias, uid) & deleteCertificateTypesForAlias(keystore, alias, uid); } @@ -264,34 +265,27 @@ public class Credentials { } /** - * Delete private key for a particular {@code alias}. - * Returns {@code true} if the entry no longer exists. - */ - static boolean deletePrivateKeyTypeForAlias(KeyStore keystore, String alias) { - return deletePrivateKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF); - } - - /** - * Delete private key for a particular {@code alias}. + * Delete user key for a particular {@code alias}. * Returns {@code true} if the entry no longer exists. */ - static boolean deletePrivateKeyTypeForAlias(KeyStore keystore, String alias, int uid) { - return keystore.delete(Credentials.USER_PRIVATE_KEY + alias, uid); + public static boolean deleteUserKeyTypeForAlias(KeyStore keystore, String alias) { + return deleteUserKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF); } /** - * Delete secret key for a particular {@code alias}. + * Delete user key for a particular {@code alias}. * Returns {@code true} if the entry no longer exists. */ - public static boolean deleteSecretKeyTypeForAlias(KeyStore keystore, String alias) { - return deleteSecretKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF); + public static boolean deleteUserKeyTypeForAlias(KeyStore keystore, String alias, int uid) { + return keystore.delete(Credentials.USER_PRIVATE_KEY + alias, uid) || + keystore.delete(Credentials.USER_SECRET_KEY + alias, uid); } /** - * Delete secret key for a particular {@code alias}. + * Delete legacy prefixed entry for a particular {@code alias} * Returns {@code true} if the entry no longer exists. */ - public static boolean deleteSecretKeyTypeForAlias(KeyStore keystore, String alias, int uid) { + public static boolean deleteLegacyKeyForAlias(KeyStore keystore, String alias, int uid) { return keystore.delete(Credentials.USER_SECRET_KEY + alias, uid); } } diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java index 399dddd7..fabcdf00 100644 --- a/android/security/KeyStore.java +++ b/android/security/KeyStore.java @@ -94,6 +94,16 @@ public class KeyStore { */ public static final int FLAG_ENCRYPTED = 1; + /** + * Select Software keymaster device, which as of this writing is the lowest security + * level available on an android device. If neither FLAG_STRONGBOX nor FLAG_SOFTWARE is provided + * A TEE based keymaster implementation is implied. + * + * Need to be in sync with KeyStoreFlag in system/security/keystore/include/keystore/keystore.h + * For historical reasons this corresponds to the KEYSTORE_FLAG_FALLBACK flag. + */ + public static final int FLAG_SOFTWARE = 1 << 1; + /** * A private flag that's only available to system server to indicate that this key is part of * device encryption flow so it receives special treatment from keystore. For example this key @@ -104,6 +114,16 @@ public class KeyStore { */ public static final int FLAG_CRITICAL_TO_DEVICE_ENCRYPTION = 1 << 3; + /** + * Select Strongbox keymaster device, which as of this writing the the highest security level + * available an android devices. If neither FLAG_STRONGBOX nor FLAG_SOFTWARE is provided + * A TEE based keymaster implementation is implied. + * + * Need to be in sync with KeyStoreFlag in system/security/keystore/include/keystore/keystore.h + */ + public static final int FLAG_STRONGBOX = 1 << 4; + + // States public enum State { UNLOCKED, LOCKED, UNINITIALIZED }; @@ -440,9 +460,9 @@ public class KeyStore { return mError; } - public boolean addRngEntropy(byte[] data) { + public boolean addRngEntropy(byte[] data, int flags) { try { - return mBinder.addRngEntropy(data) == NO_ERROR; + return mBinder.addRngEntropy(data, flags) == NO_ERROR; } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return false; diff --git a/android/security/keymaster/KeyAttestationPackageInfo.java b/android/security/keymaster/KeyAttestationPackageInfo.java index 5a3f3907..a93d1e11 100644 --- a/android/security/keymaster/KeyAttestationPackageInfo.java +++ b/android/security/keymaster/KeyAttestationPackageInfo.java @@ -28,7 +28,7 @@ import android.os.Parcelable; */ public class KeyAttestationPackageInfo implements Parcelable { private final String mPackageName; - private final int mPackageVersionCode; + private final long mPackageVersionCode; private final Signature[] mPackageSignatures; /** @@ -37,7 +37,7 @@ public class KeyAttestationPackageInfo implements Parcelable { * @param mPackageSignatures */ public KeyAttestationPackageInfo( - String mPackageName, int mPackageVersionCode, Signature[] mPackageSignatures) { + String mPackageName, long mPackageVersionCode, Signature[] mPackageSignatures) { super(); this.mPackageName = mPackageName; this.mPackageVersionCode = mPackageVersionCode; @@ -52,7 +52,7 @@ public class KeyAttestationPackageInfo implements Parcelable { /** * @return the mPackageVersionCode */ - public int getPackageVersionCode() { + public long getPackageVersionCode() { return mPackageVersionCode; } /** @@ -70,7 +70,7 @@ public class KeyAttestationPackageInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mPackageName); - dest.writeInt(mPackageVersionCode); + dest.writeLong(mPackageVersionCode); dest.writeTypedArray(mPackageSignatures, flags); } @@ -89,7 +89,7 @@ public class KeyAttestationPackageInfo implements Parcelable { private KeyAttestationPackageInfo(Parcel source) { mPackageName = source.readString(); - mPackageVersionCode = source.readInt(); + mPackageVersionCode = source.readLong(); mPackageSignatures = source.createTypedArray(Signature.CREATOR); } } diff --git a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java index 988e32cf..f1d1e166 100644 --- a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java +++ b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java @@ -305,7 +305,7 @@ public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng( mRng, (mKeySizeBits + 7) / 8); int flags = 0; - String keyAliasInKeystore = Credentials.USER_SECRET_KEY + spec.getKeystoreAlias(); + String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + spec.getKeystoreAlias(); KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics(); boolean success = false; try { diff --git a/android/security/keystore/AndroidKeyStoreProvider.java b/android/security/keystore/AndroidKeyStoreProvider.java index f36c00ce..55e6519d 100644 --- a/android/security/keystore/AndroidKeyStoreProvider.java +++ b/android/security/keystore/AndroidKeyStoreProvider.java @@ -196,7 +196,7 @@ public class AndroidKeyStoreProvider extends Provider { } @NonNull - public static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey( + private static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey( @NonNull AndroidKeyStorePublicKey publicKey) { String keyAlgorithm = publicKey.getAlgorithm(); if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) { @@ -212,17 +212,25 @@ public class AndroidKeyStoreProvider extends Provider { } @NonNull - public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( - @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) + private static KeyCharacteristics getKeyCharacteristics(@NonNull KeyStore keyStore, + @NonNull String alias, int uid) throws UnrecoverableKeyException { KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); int errorCode = keyStore.getKeyCharacteristics( - privateKeyAlias, null, null, uid, keyCharacteristics); + alias, null, null, uid, keyCharacteristics); if (errorCode != KeyStore.NO_ERROR) { throw (UnrecoverableKeyException) - new UnrecoverableKeyException("Failed to obtain information about private key") - .initCause(KeyStore.getKeyStoreException(errorCode)); + new UnrecoverableKeyException("Failed to obtain information about key") + .initCause(KeyStore.getKeyStoreException(errorCode)); } + return keyCharacteristics; + } + + @NonNull + private static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { ExportResult exportResult = keyStore.exportKey( privateKeyAlias, KeymasterDefs.KM_KEY_FORMAT_X509, null, null, uid); if (exportResult.resultCode != KeyStore.NO_ERROR) { @@ -252,37 +260,56 @@ public class AndroidKeyStoreProvider extends Provider { } @NonNull - public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( + public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore( @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) throws UnrecoverableKeyException { + return loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + + @NonNull + private static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { AndroidKeyStorePublicKey publicKey = - loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid); + loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid, + keyCharacteristics); AndroidKeyStorePrivateKey privateKey = AndroidKeyStoreProvider.getAndroidKeyStorePrivateKey(publicKey); return new KeyPair(publicKey, privateKey); } @NonNull - public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore( @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) throws UnrecoverableKeyException { - KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid); + return loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + + @NonNull + private static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid, + @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { + KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid, + keyCharacteristics); return (AndroidKeyStorePrivateKey) keyPair.getPrivate(); } @NonNull - public static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore( - @NonNull KeyStore keyStore, @NonNull String secretKeyAlias, int uid) + public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid) throws UnrecoverableKeyException { - KeyCharacteristics keyCharacteristics = new KeyCharacteristics(); - int errorCode = keyStore.getKeyCharacteristics( - secretKeyAlias, null, null, uid, keyCharacteristics); - if (errorCode != KeyStore.NO_ERROR) { - throw (UnrecoverableKeyException) - new UnrecoverableKeyException("Failed to obtain information about key") - .initCause(KeyStore.getKeyStoreException(errorCode)); - } + return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, privateKeyAlias, uid, + getKeyCharacteristics(keyStore, privateKeyAlias, uid)); + } + @NonNull + private static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore( + @NonNull String secretKeyAlias, int uid, @NonNull KeyCharacteristics keyCharacteristics) + throws UnrecoverableKeyException { Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM); if (keymasterAlgorithm == null) { throw new UnrecoverableKeyException("Key algorithm unknown"); @@ -310,6 +337,29 @@ public class AndroidKeyStoreProvider extends Provider { return new AndroidKeyStoreSecretKey(secretKeyAlias, uid, keyAlgorithmString); } + public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore( + @NonNull KeyStore keyStore, @NonNull String userKeyAlias, int uid) + throws UnrecoverableKeyException { + KeyCharacteristics keyCharacteristics = getKeyCharacteristics(keyStore, userKeyAlias, uid); + + Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM); + if (keymasterAlgorithm == null) { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + + if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) { + return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid, + keyCharacteristics); + } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA || + keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) { + return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, userKeyAlias, uid, + keyCharacteristics); + } else { + throw new UnrecoverableKeyException("Key algorithm unknown"); + } + } + /** * Returns an {@code AndroidKeyStore} {@link java.security.KeyStore}} of the specified UID. * The {@code KeyStore} contains keys and certificates owned by that UID. Such cross-UID diff --git a/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java b/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java index 0379863e..fdb885db 100644 --- a/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java +++ b/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java @@ -64,7 +64,10 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { AndroidKeyStoreKey keystoreKey = (AndroidKeyStoreKey) key; String keyAliasInKeystore = keystoreKey.getAlias(); String entryAlias; - if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)) { + if (keyAliasInKeystore.startsWith(Credentials.USER_PRIVATE_KEY)) { + entryAlias = keyAliasInKeystore.substring(Credentials.USER_PRIVATE_KEY.length()); + } else if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)){ + // key has legacy prefix entryAlias = keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length()); } else { throw new InvalidKeySpecException("Invalid key alias: " + keyAliasInKeystore); diff --git a/android/security/keystore/AndroidKeyStoreSpi.java b/android/security/keystore/AndroidKeyStoreSpi.java index bab4010b..d73a9e29 100644 --- a/android/security/keystore/AndroidKeyStoreSpi.java +++ b/android/security/keystore/AndroidKeyStoreSpi.java @@ -89,18 +89,14 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { @Override public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { - if (isPrivateKeyEntry(alias)) { - String privateKeyAlias = Credentials.USER_PRIVATE_KEY + alias; - return AndroidKeyStoreProvider.loadAndroidKeyStorePrivateKeyFromKeystore( - mKeyStore, privateKeyAlias, mUid); - } else if (isSecretKeyEntry(alias)) { - String secretKeyAlias = Credentials.USER_SECRET_KEY + alias; - return AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore( - mKeyStore, secretKeyAlias, mUid); - } else { - // Key not found - return null; + String userKeyAlias = Credentials.USER_PRIVATE_KEY + alias; + if (!mKeyStore.contains(userKeyAlias, mUid)) { + // try legacy prefix for backward compatibility + userKeyAlias = Credentials.USER_SECRET_KEY + alias; + if (!mKeyStore.contains(userKeyAlias, mUid)) return null; } + return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore, userKeyAlias, + mUid); } @Override @@ -540,7 +536,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } else { // Keep the stored private key around -- delete all other entry types Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid); - Credentials.deleteSecretKeyTypeForAlias(mKeyStore, alias, mUid); + Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid); } // Store the leaf certificate @@ -565,7 +561,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid); } else { Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid); - Credentials.deleteSecretKeyTypeForAlias(mKeyStore, alias, mUid); + Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid); } } } @@ -588,12 +584,17 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { if (keyAliasInKeystore == null) { throw new KeyStoreException("KeyStore-backed secret key does not have an alias"); } - if (!keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)) { - throw new KeyStoreException("KeyStore-backed secret key has invalid alias: " - + keyAliasInKeystore); + String keyAliasPrefix = Credentials.USER_PRIVATE_KEY; + if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) { + // try legacy prefix + keyAliasPrefix = Credentials.USER_SECRET_KEY; + if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) { + throw new KeyStoreException("KeyStore-backed secret key has invalid alias: " + + keyAliasInKeystore); + } } String keyEntryAlias = - keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length()); + keyAliasInKeystore.substring(keyAliasPrefix.length()); if (!entryAlias.equals(keyEntryAlias)) { throw new KeyStoreException("Can only replace KeyStore-backed keys with same" + " alias: " + entryAlias + " != " + keyEntryAlias); @@ -728,7 +729,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } Credentials.deleteAllTypesForAlias(mKeyStore, entryAlias, mUid); - String keyAliasInKeystore = Credentials.USER_SECRET_KEY + entryAlias; + String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + entryAlias; int errorCode = mKeyStore.importKey( keyAliasInKeystore, args, @@ -827,24 +828,10 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } private boolean isKeyEntry(String alias) { - return isPrivateKeyEntry(alias) || isSecretKeyEntry(alias); - } - - private boolean isPrivateKeyEntry(String alias) { - if (alias == null) { - throw new NullPointerException("alias == null"); - } - - return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid); + return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid) || + mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid); } - private boolean isSecretKeyEntry(String alias) { - if (alias == null) { - throw new NullPointerException("alias == null"); - } - - return mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid); - } private boolean isCertificateEntry(String alias) { if (alias == null) { diff --git a/android/security/keystore/AttestationUtils.java b/android/security/keystore/AttestationUtils.java index cf4347d1..0811100f 100644 --- a/android/security/keystore/AttestationUtils.java +++ b/android/security/keystore/AttestationUtils.java @@ -72,6 +72,33 @@ public abstract class AttestationUtils { */ public static final int ID_TYPE_MEID = 3; + /** + * Creates an array of X509Certificates from the provided KeymasterCertificateChain. + * + * @hide Only called by the DevicePolicyManager. + */ + @NonNull public static X509Certificate[] parseCertificateChain( + final KeymasterCertificateChain kmChain) throws + KeyAttestationException { + // Extract certificate chain. + final Collection rawChain = kmChain.getCertificates(); + if (rawChain.size() < 2) { + throw new KeyAttestationException("Attestation certificate chain contained " + + rawChain.size() + " entries. At least two are required."); + } + final ByteArrayOutputStream concatenatedRawChain = new ByteArrayOutputStream(); + try { + for (final byte[] cert : rawChain) { + concatenatedRawChain.write(cert); + } + return CertificateFactory.getInstance("X.509").generateCertificates( + new ByteArrayInputStream(concatenatedRawChain.toByteArray())) + .toArray(new X509Certificate[0]); + } catch (Exception e) { + throw new KeyAttestationException("Unable to construct certificate chain", e); + } + } + /** * Performs attestation of the device's identifiers. This method returns a certificate chain * whose first element contains the requested device identifiers in an extension. The device's @@ -173,22 +200,18 @@ public abstract class AttestationUtils { KeyStore.getKeyStoreException(errorCode)); } - // Extract certificate chain. - final Collection rawChain = outChain.getCertificates(); - if (rawChain.size() < 2) { - throw new DeviceIdAttestationException("Attestation certificate chain contained " - + rawChain.size() + " entries. At least two are required."); - } - final ByteArrayOutputStream concatenatedRawChain = new ByteArrayOutputStream(); try { - for (final byte[] cert : rawChain) { - concatenatedRawChain.write(cert); - } - return CertificateFactory.getInstance("X.509").generateCertificates( - new ByteArrayInputStream(concatenatedRawChain.toByteArray())) - .toArray(new X509Certificate[0]); - } catch (Exception e) { - throw new DeviceIdAttestationException("Unable to construct certificate chain", e); + return parseCertificateChain(outChain); + } catch (KeyAttestationException e) { + throw new DeviceIdAttestationException(e.getMessage(), e); } } + + /** + * Returns true if the attestation chain provided is a valid key attestation chain. + * @hide + */ + public static boolean isChainValid(KeymasterCertificateChain chain) { + return chain != null && chain.getCertificates().size() >= 2; + } } diff --git a/android/security/keystore/KeyAttestationException.java b/android/security/keystore/KeyAttestationException.java new file mode 100644 index 00000000..6cf5fb2f --- /dev/null +++ b/android/security/keystore/KeyAttestationException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.keystore; + +/** + * Thrown when {@link AttestationUtils} is unable to attest the given key or handle + * the resulting attestation record. + * + * @hide + */ +public class KeyAttestationException extends Exception { + /** + * Constructs a new {@code KeyAttestationException} with the current stack trace and the + * specified detail message. + * + * @param detailMessage the detail message for this exception. + */ + public KeyAttestationException(String detailMessage) { + super(detailMessage); + } + + /** + * Constructs a new {@code KeyAttestationException} with the current stack trace, the + * specified detail message and the specified cause. + * + * @param message the detail message for this exception. + * @param cause the cause of this exception, may be {@code null}. + */ + public KeyAttestationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java index ed40b77b..1238d877 100644 --- a/android/security/keystore/KeyGenParameterSpec.java +++ b/android/security/keystore/KeyGenParameterSpec.java @@ -195,7 +195,7 @@ import javax.security.auth.x500.X500Principal; *

     {@code
      * KeyGenerator keyGenerator = KeyGenerator.getInstance(
      *         KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    - * keyGenerator.initialize(
    + * keyGenerator.init(
      *         new KeyGenParameterSpec.Builder("key2",
      *                 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
      *                 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    @@ -219,7 +219,7 @@ import javax.security.auth.x500.X500Principal;
      * 
     {@code
      * KeyGenerator keyGenerator = KeyGenerator.getInstance(
      *         KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
    - * keyGenerator.initialize(
    + * keyGenerator.init(
      *         new KeyGenParameterSpec.Builder("key2", KeyProperties.PURPOSE_SIGN).build());
      * SecretKey key = keyGenerator.generateKey();
      * Mac mac = Mac.getInstance("HmacSHA256");
    @@ -679,6 +679,40 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
                 mPurposes = purposes;
             }
     
    +        /**
    +         * A Builder constructor taking in an already-built KeyGenParameterSpec, useful for
    +         * changing values of the KeyGenParameterSpec quickly.
    +         * @hide Should be used internally only.
    +         */
    +        public Builder(@NonNull KeyGenParameterSpec sourceSpec) {
    +            this(sourceSpec.getKeystoreAlias(), sourceSpec.getPurposes());
    +            mUid = sourceSpec.getUid();
    +            mKeySize = sourceSpec.getKeySize();
    +            mSpec = sourceSpec.getAlgorithmParameterSpec();
    +            mCertificateSubject = sourceSpec.getCertificateSubject();
    +            mCertificateSerialNumber = sourceSpec.getCertificateSerialNumber();
    +            mCertificateNotBefore = sourceSpec.getCertificateNotBefore();
    +            mCertificateNotAfter = sourceSpec.getCertificateNotAfter();
    +            mKeyValidityStart = sourceSpec.getKeyValidityStart();
    +            mKeyValidityForOriginationEnd = sourceSpec.getKeyValidityForOriginationEnd();
    +            mKeyValidityForConsumptionEnd = sourceSpec.getKeyValidityForConsumptionEnd();
    +            mPurposes = sourceSpec.getPurposes();
    +            if (sourceSpec.isDigestsSpecified()) {
    +                mDigests = sourceSpec.getDigests();
    +            }
    +            mEncryptionPaddings = sourceSpec.getEncryptionPaddings();
    +            mSignaturePaddings = sourceSpec.getSignaturePaddings();
    +            mBlockModes = sourceSpec.getBlockModes();
    +            mRandomizedEncryptionRequired = sourceSpec.isRandomizedEncryptionRequired();
    +            mUserAuthenticationRequired = sourceSpec.isUserAuthenticationRequired();
    +            mUserAuthenticationValidityDurationSeconds =
    +                sourceSpec.getUserAuthenticationValidityDurationSeconds();
    +            mAttestationChallenge = sourceSpec.getAttestationChallenge();
    +            mUniqueIdIncluded = sourceSpec.isUniqueIdIncluded();
    +            mUserAuthenticationValidWhileOnBody = sourceSpec.isUserAuthenticationValidWhileOnBody();
    +            mInvalidatedByBiometricEnrollment = sourceSpec.isInvalidatedByBiometricEnrollment();
    +        }
    +
             /**
              * Sets the UID which will own the key.
              *
    diff --git a/android/security/keystore/KeyProperties.java b/android/security/keystore/KeyProperties.java
    index d6b1cf1d..a250d1f0 100644
    --- a/android/security/keystore/KeyProperties.java
    +++ b/android/security/keystore/KeyProperties.java
    @@ -39,13 +39,12 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(flag = true,
    -            value = {
    -                PURPOSE_ENCRYPT,
    -                PURPOSE_DECRYPT,
    -                PURPOSE_SIGN,
    -                PURPOSE_VERIFY,
    -                })
    +    @IntDef(flag = true, prefix = { "PURPOSE_" }, value = {
    +            PURPOSE_ENCRYPT,
    +            PURPOSE_DECRYPT,
    +            PURPOSE_SIGN,
    +            PURPOSE_VERIFY,
    +    })
         public @interface PurposeEnum {}
     
         /**
    @@ -126,7 +125,7 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @StringDef({
    +    @StringDef(prefix = { "KEY_" }, value = {
             KEY_ALGORITHM_RSA,
             KEY_ALGORITHM_EC,
             KEY_ALGORITHM_AES,
    @@ -267,7 +266,7 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @StringDef({
    +    @StringDef(prefix = { "BLOCK_MODE_" }, value = {
             BLOCK_MODE_ECB,
             BLOCK_MODE_CBC,
             BLOCK_MODE_CTR,
    @@ -354,7 +353,7 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @StringDef({
    +    @StringDef(prefix = { "ENCRYPTION_PADDING_" }, value = {
             ENCRYPTION_PADDING_NONE,
             ENCRYPTION_PADDING_PKCS7,
             ENCRYPTION_PADDING_RSA_PKCS1,
    @@ -437,7 +436,7 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @StringDef({
    +    @StringDef(prefix = { "SIGNATURE_PADDING_" }, value = {
             SIGNATURE_PADDING_RSA_PKCS1,
             SIGNATURE_PADDING_RSA_PSS,
             })
    @@ -497,7 +496,7 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @StringDef({
    +    @StringDef(prefix = { "DIGEST_" }, value = {
             DIGEST_NONE,
             DIGEST_MD5,
             DIGEST_SHA1,
    @@ -647,11 +646,12 @@ public abstract class KeyProperties {
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef({
    -        ORIGIN_GENERATED,
    -        ORIGIN_IMPORTED,
    -        ORIGIN_UNKNOWN,
    -        })
    +    @IntDef(prefix = { "ORIGIN_" }, value = {
    +            ORIGIN_GENERATED,
    +            ORIGIN_IMPORTED,
    +            ORIGIN_UNKNOWN,
    +    })
    +
         public @interface OriginEnum {}
     
         /** Key was generated inside AndroidKeyStore. */
    diff --git a/android/security/keystore/ParcelableKeyGenParameterSpec.java b/android/security/keystore/ParcelableKeyGenParameterSpec.java
    new file mode 100644
    index 00000000..7cb8e375
    --- /dev/null
    +++ b/android/security/keystore/ParcelableKeyGenParameterSpec.java
    @@ -0,0 +1,185 @@
    +/*
    + * Copyright (C) 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.security.keystore;
    +
    +import android.os.Parcelable;
    +import android.os.Parcel;
    +
    +import java.math.BigInteger;
    +import java.security.spec.AlgorithmParameterSpec;
    +import java.security.spec.ECGenParameterSpec;
    +import java.security.spec.RSAKeyGenParameterSpec;
    +import java.util.Date;
    +
    +import javax.security.auth.x500.X500Principal;
    +
    +/**
    + * A parcelable version of KeyGenParameterSpec
    + * @hide only used for communicating with the DPMS.
    + */
    +public final class ParcelableKeyGenParameterSpec implements Parcelable {
    +    private static final int ALGORITHM_PARAMETER_SPEC_NONE = 1;
    +    private static final int ALGORITHM_PARAMETER_SPEC_RSA = 2;
    +    private static final int ALGORITHM_PARAMETER_SPEC_EC = 3;
    +
    +    private final KeyGenParameterSpec mSpec;
    +
    +    public ParcelableKeyGenParameterSpec(
    +            KeyGenParameterSpec spec) {
    +        mSpec = spec;
    +    }
    +
    +    public int describeContents() {
    +        return 0;
    +    }
    +
    +    private static void writeOptionalDate(Parcel out, Date date) {
    +        if (date != null) {
    +            out.writeBoolean(true);
    +            out.writeLong(date.getTime());
    +        } else {
    +            out.writeBoolean(false);
    +        }
    +    }
    +
    +    public void writeToParcel(Parcel out, int flags) {
    +        out.writeString(mSpec.getKeystoreAlias());
    +        out.writeInt(mSpec.getPurposes());
    +        out.writeInt(mSpec.getUid());
    +        out.writeInt(mSpec.getKeySize());
    +
    +        // Only needs to support RSAKeyGenParameterSpec and ECGenParameterSpec.
    +        AlgorithmParameterSpec algoSpec = mSpec.getAlgorithmParameterSpec();
    +        if (algoSpec == null) {
    +            out.writeInt(ALGORITHM_PARAMETER_SPEC_NONE);
    +        } else if (algoSpec instanceof RSAKeyGenParameterSpec) {
    +            RSAKeyGenParameterSpec rsaSpec = (RSAKeyGenParameterSpec) algoSpec;
    +            out.writeInt(ALGORITHM_PARAMETER_SPEC_RSA);
    +            out.writeInt(rsaSpec.getKeysize());
    +            out.writeByteArray(rsaSpec.getPublicExponent().toByteArray());
    +        } else if (algoSpec instanceof ECGenParameterSpec) {
    +            ECGenParameterSpec ecSpec = (ECGenParameterSpec) algoSpec;
    +            out.writeInt(ALGORITHM_PARAMETER_SPEC_EC);
    +            out.writeString(ecSpec.getName());
    +        } else {
    +            throw new IllegalArgumentException(
    +                    String.format("Unknown algorithm parameter spec: %s", algoSpec.getClass()));
    +        }
    +        out.writeByteArray(mSpec.getCertificateSubject().getEncoded());
    +        out.writeByteArray(mSpec.getCertificateSerialNumber().toByteArray());
    +        out.writeLong(mSpec.getCertificateNotBefore().getTime());
    +        out.writeLong(mSpec.getCertificateNotAfter().getTime());
    +        writeOptionalDate(out, mSpec.getKeyValidityStart());
    +        writeOptionalDate(out, mSpec.getKeyValidityForOriginationEnd());
    +        writeOptionalDate(out, mSpec.getKeyValidityForConsumptionEnd());
    +        if (mSpec.isDigestsSpecified()) {
    +            out.writeStringArray(mSpec.getDigests());
    +        } else {
    +            out.writeStringArray(null);
    +        }
    +        out.writeStringArray(mSpec.getEncryptionPaddings());
    +        out.writeStringArray(mSpec.getSignaturePaddings());
    +        out.writeStringArray(mSpec.getBlockModes());
    +        out.writeBoolean(mSpec.isRandomizedEncryptionRequired());
    +        out.writeBoolean(mSpec.isUserAuthenticationRequired());
    +        out.writeInt(mSpec.getUserAuthenticationValidityDurationSeconds());
    +        out.writeByteArray(mSpec.getAttestationChallenge());
    +        out.writeBoolean(mSpec.isUniqueIdIncluded());
    +        out.writeBoolean(mSpec.isUserAuthenticationValidWhileOnBody());
    +        out.writeBoolean(mSpec.isInvalidatedByBiometricEnrollment());
    +    }
    +
    +    private static Date readDateOrNull(Parcel in) {
    +        boolean hasDate = in.readBoolean();
    +        if (hasDate) {
    +            return new Date(in.readLong());
    +        } else {
    +            return null;
    +        }
    +    }
    +
    +    private ParcelableKeyGenParameterSpec(Parcel in) {
    +        String keystoreAlias = in.readString();
    +        int purposes = in.readInt();
    +        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
    +                keystoreAlias, purposes);
    +        builder.setUid(in.readInt());
    +        // KeySize is -1 by default, if the KeyGenParameterSpec previously parcelled had the default
    +        // value, do not set it as this will cause setKeySize to throw.
    +        int keySize = in.readInt();
    +        if (keySize >= 0) {
    +            builder.setKeySize(keySize);
    +        }
    +
    +        int keySpecType = in.readInt();
    +        AlgorithmParameterSpec algorithmSpec = null;
    +        if (keySpecType == ALGORITHM_PARAMETER_SPEC_NONE) {
    +            algorithmSpec = null;
    +        } else if (keySpecType == ALGORITHM_PARAMETER_SPEC_RSA) {
    +            int rsaKeySize = in.readInt();
    +            BigInteger publicExponent = new BigInteger(in.createByteArray());
    +            algorithmSpec = new RSAKeyGenParameterSpec(rsaKeySize, publicExponent);
    +        } else if (keySpecType == ALGORITHM_PARAMETER_SPEC_EC) {
    +            String stdName = in.readString();
    +            algorithmSpec = new ECGenParameterSpec(stdName);
    +        } else {
    +            throw new IllegalArgumentException(
    +                    String.format("Unknown algorithm parameter spec: %d", keySpecType));
    +        }
    +        if (algorithmSpec != null) {
    +            builder.setAlgorithmParameterSpec(algorithmSpec);
    +        }
    +        builder.setCertificateSubject(new X500Principal(in.createByteArray()));
    +        builder.setCertificateSerialNumber(new BigInteger(in.createByteArray()));
    +        builder.setCertificateNotBefore(new Date(in.readLong()));
    +        builder.setCertificateNotAfter(new Date(in.readLong()));
    +        builder.setKeyValidityStart(readDateOrNull(in));
    +        builder.setKeyValidityForOriginationEnd(readDateOrNull(in));
    +        builder.setKeyValidityForConsumptionEnd(readDateOrNull(in));
    +        String[] digests = in.createStringArray();
    +        if (digests != null) {
    +            builder.setDigests(digests);
    +        }
    +        builder.setEncryptionPaddings(in.createStringArray());
    +        builder.setSignaturePaddings(in.createStringArray());
    +        builder.setBlockModes(in.createStringArray());
    +        builder.setRandomizedEncryptionRequired(in.readBoolean());
    +        builder.setUserAuthenticationRequired(in.readBoolean());
    +        builder.setUserAuthenticationValidityDurationSeconds(in.readInt());
    +        builder.setAttestationChallenge(in.createByteArray());
    +        builder.setUniqueIdIncluded(in.readBoolean());
    +        builder.setUserAuthenticationValidWhileOnBody(in.readBoolean());
    +        builder.setInvalidatedByBiometricEnrollment(in.readBoolean());
    +        mSpec = builder.build();
    +    }
    +
    +    public static final Creator CREATOR = new Creator() {
    +        @Override
    +        public ParcelableKeyGenParameterSpec createFromParcel(Parcel in) {
    +            return new ParcelableKeyGenParameterSpec(in);
    +        }
    +
    +        @Override
    +        public ParcelableKeyGenParameterSpec[] newArray(int size) {
    +            return new ParcelableKeyGenParameterSpec[size];
    +        }
    +    };
    +
    +    public KeyGenParameterSpec getSpec() {
    +        return mSpec;
    +    }
    +}
    diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/recoverablekeystore/KeyDerivationParameters.java
    new file mode 100644
    index 00000000..978e60ee
    --- /dev/null
    +++ b/android/security/recoverablekeystore/KeyDerivationParameters.java
    @@ -0,0 +1,112 @@
    +/*
    + * Copyright (C) 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.security.recoverablekeystore;
    +
    +import android.annotation.IntDef;
    +import android.annotation.NonNull;
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import com.android.internal.util.Preconditions;
    +
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +
    +/**
    + * Collection of parameters which define a key derivation function.
    + * Supports
    + *
    + * 
      + *
    • SHA256 + *
    • Argon2id + *
    + * @hide + */ +public final class KeyDerivationParameters implements Parcelable { + private final int mAlgorithm; + private byte[] mSalt; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID}) + public @interface KeyDerivationAlgorithm { + } + + /** + * Salted SHA256 + */ + public static final int ALGORITHM_SHA256 = 1; + + /** + * Argon2ID + */ + // TODO: add Argon2ID support. + public static final int ALGORITHM_ARGON2ID = 2; + + /** + * Creates instance of the class to to derive key using salted SHA256 hash. + */ + public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) { + return new KeyDerivationParameters(ALGORITHM_SHA256, salt); + } + + private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) { + mAlgorithm = algorithm; + mSalt = Preconditions.checkNotNull(salt); + } + + /** + * Gets algorithm. + */ + public @KeyDerivationAlgorithm int getAlgorithm() { + return mAlgorithm; + } + + /** + * Gets salt. + */ + public @NonNull byte[] getSalt() { + return mSalt; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public KeyDerivationParameters createFromParcel(Parcel in) { + return new KeyDerivationParameters(in); + } + + public KeyDerivationParameters[] newArray(int length) { + return new KeyDerivationParameters[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mAlgorithm); + out.writeByteArray(mSalt); + } + + protected KeyDerivationParameters(Parcel in) { + mAlgorithm = in.readInt(); + mSalt = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/recoverablekeystore/KeyEntryRecoveryData.java b/android/security/recoverablekeystore/KeyEntryRecoveryData.java new file mode 100644 index 00000000..80f5aa71 --- /dev/null +++ b/android/security/recoverablekeystore/KeyEntryRecoveryData.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.recoverablekeystore; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + + +/** + * Helper class with data necessary recover a single application key, given a recovery key. + * + *
      + *
    • Alias - Keystore alias of the key. + *
    • Encrypted key material. + *
    + * + * 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 CREATOR = + new Parcelable.Creator() { + public KeyEntryRecoveryData createFromParcel(Parcel in) { + return new KeyEntryRecoveryData(in); + } + + public KeyEntryRecoveryData[] newArray(int length) { + return new KeyEntryRecoveryData[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeByteArray(mAlias); + out.writeByteArray(mEncryptedKeyMaterial); + } + + protected KeyEntryRecoveryData(Parcel in) { + mAlias = in.createByteArray(); + mEncryptedKeyMaterial = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryData.java b/android/security/recoverablekeystore/KeyStoreRecoveryData.java new file mode 100644 index 00000000..087f7a25 --- /dev/null +++ b/android/security/recoverablekeystore/KeyStoreRecoveryData.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.recoverablekeystore; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * Helper class which returns data necessary to recover keys. + * Contains + * + *
      + *
    • Snapshot version. + *
    • Recovery metadata with UI and key derivation parameters. + *
    • List of application keys encrypted by recovery key. + *
    • Encrypted recovery key. + *
    + * + * @hide + */ +public final class KeyStoreRecoveryData implements Parcelable { + private final int mSnapshotVersion; + private final List mRecoveryMetadata; + private final List mApplicationKeyBlobs; + private final byte[] mEncryptedRecoveryKeyBlob; + + public KeyStoreRecoveryData(int snapshotVersion, @NonNull List + recoveryMetadata, @NonNull List 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 getRecoveryMetadata() { + return mRecoveryMetadata; + } + + /** + * List of application keys, with key material encrypted by + * the recovery key ({@link #getEncryptedRecoveryKeyBlob}). + */ + public @NonNull List 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 CREATOR = + new Parcelable.Creator() { + public KeyStoreRecoveryData createFromParcel(Parcel in) { + return new KeyStoreRecoveryData(in); + } + + public KeyStoreRecoveryData[] newArray(int length) { + return new KeyStoreRecoveryData[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mSnapshotVersion); + out.writeTypedList(mRecoveryMetadata); + out.writeByteArray(mEncryptedRecoveryKeyBlob); + out.writeTypedList(mApplicationKeyBlobs); + } + + protected KeyStoreRecoveryData(Parcel in) { + mSnapshotVersion = in.readInt(); + mRecoveryMetadata = in.createTypedArrayList(KeyStoreRecoveryMetadata.CREATOR); + mEncryptedRecoveryKeyBlob = in.createByteArray(); + mApplicationKeyBlobs = in.createTypedArrayList(KeyEntryRecoveryData.CREATOR); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java new file mode 100644 index 00000000..43f9c805 --- /dev/null +++ b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.recoverablekeystore; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * Helper class with data necessary to recover Keystore on a new device. + * It defines UI shown to the user and a way to derive a cryptographic key from user output. + * + * @hide + */ +public final class KeyStoreRecoveryMetadata implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD}) + public @interface UserSecretType { + } + + /** + * Lockscreen secret is required to recover KeyStore. + */ + public static final int TYPE_LOCKSCREEN = 1; + + /** + * Custom passphrase, unrelated to lock screen, is required to recover KeyStore. + */ + public static final int TYPE_CUSTOM_PASSWORD = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN}) + public @interface LockScreenUiFormat { + } + + /** + * Pin with digits only. + */ + public static final int TYPE_PIN = 1; + + /** + * Password. String with latin-1 characters only. + */ + public static final int TYPE_PASSWORD = 2; + + /** + * Pattern with 3 by 3 grid. + */ + public static final int TYPE_PATTERN = 3; + + @UserSecretType + private final int mUserSecretType; + + @LockScreenUiFormat + private final int mLockScreenUiFormat; + + /** + * Parameters of key derivation function, including algorithm, difficulty, salt. + */ + private KeyDerivationParameters mKeyDerivationParameters; + private byte[] mSecret; // Derived from user secret. The field must have limited visibility. + + /** + * @param secret Constructor creates a reference to the secret. Caller must use + * @link {#clearSecret} to overwrite its value in memory. + */ + public KeyStoreRecoveryMetadata(@UserSecretType int userSecretType, + @LockScreenUiFormat int lockScreenUiFormat, + @NonNull KeyDerivationParameters keyDerivationParameters, @NonNull byte[] secret) { + mUserSecretType = userSecretType; + mLockScreenUiFormat = lockScreenUiFormat; + mKeyDerivationParameters = Preconditions.checkNotNull(keyDerivationParameters); + mSecret = Preconditions.checkNotNull(secret); + } + + /** + * Specifies UX shown to user during recovery. + * + * @see KeyStore.TYPE_PIN + * @see KeyStore.TYPE_PASSWORD + * @see KeyStore.TYPE_PATTERN + */ + public @LockScreenUiFormat int getLockScreenUiFormat() { + return mLockScreenUiFormat; + } + + /** + * Specifies function used to derive symmetric key from user input + * Format is defined in separate util class. + */ + public @NonNull KeyDerivationParameters getKeyDerivationParameters() { + return mKeyDerivationParameters; + } + + /** + * Secret string derived from user input. + */ + public @NonNull byte[] getSecret() { + return mSecret; + } + + /** + * @see KeyStore.TYPE_LOCKSCREEN + * @see KeyStore.TYPE_CUSTOM_PASSWORD + */ + public @UserSecretType int getUserSecretType() { + return mUserSecretType; + } + + /** + * Removes secret from memory than object is no longer used. + * Since finalizer call is not reliable, please use @link {#clearSecret} directly. + */ + @Override + protected void finalize() throws Throwable { + clearSecret(); + super.finalize(); + } + + /** + * Fills mSecret with zeroes. + */ + public void clearSecret() { + Arrays.fill(mSecret, (byte) 0); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public KeyStoreRecoveryMetadata createFromParcel(Parcel in) { + return new KeyStoreRecoveryMetadata(in); + } + + public KeyStoreRecoveryMetadata[] newArray(int length) { + return new KeyStoreRecoveryMetadata[length]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mUserSecretType); + out.writeInt(mLockScreenUiFormat); + out.writeTypedObject(mKeyDerivationParameters, flags); + out.writeByteArray(mSecret); + } + + protected KeyStoreRecoveryMetadata(Parcel in) { + mUserSecretType = in.readInt(); + mLockScreenUiFormat = in.readInt(); + mKeyDerivationParameters = in.readTypedObject(KeyDerivationParameters.CREATOR); + mSecret = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java new file mode 100644 index 00000000..72a138a6 --- /dev/null +++ b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.recoverablekeystore; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.security.KeyStore; +import android.util.AndroidException; + +import com.android.internal.widget.ILockSettings; + +import java.util.List; +import java.util.Map; + +/** + * A wrapper around KeyStore which lets key be exported to trusted hardware on server side and + * recovered later. + * + * @hide + */ +public class RecoverableKeyStoreLoader { + + public static final String PERMISSION_RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE"; + + public static final int NO_ERROR = KeyStore.NO_ERROR; + public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR; + public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20; + public static final int NO_SNAPSHOT_PENDING_ERROR = 21; + + /** + * Rate limit is enforced to prevent using too many trusted remote devices, since each device + * can have its own number of user secret guesses allowed. + * + * @hide + */ + public static final int RATE_LIMIT_EXCEEDED = 21; + + /** Key has been successfully synced. */ + public static final int RECOVERY_STATUS_SYNCED = 0; + /** Waiting for recovery agent to sync the key. */ + public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1; + /** Recovery account is not available. */ + public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2; + /** Key cannot be synced. */ + public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3; + + private final ILockSettings mBinder; + + private RecoverableKeyStoreLoader(ILockSettings binder) { + mBinder = binder; + } + + /** @hide */ + public static RecoverableKeyStoreLoader getInstance() { + ILockSettings lockSettings = + ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); + return new RecoverableKeyStoreLoader(lockSettings); + } + + /** + * Exceptions returned by {@link RecoverableKeyStoreLoader}. + * + * @hide + */ + public static class RecoverableKeyStoreLoaderException extends AndroidException { + private int mErrorCode; + + /** + * Creates new {@link #RecoverableKeyStoreLoaderException} instance from the error code. + * + * @param errorCode + * @hide + */ + public static RecoverableKeyStoreLoaderException fromErrorCode(int errorCode) { + return new RecoverableKeyStoreLoaderException( + errorCode, getMessageFromErrorCode(errorCode)); + } + + /** + * Creates new {@link #RecoverableKeyStoreLoaderException} from {@link + * ServiceSpecificException}. + * + * @param e exception thrown on service side. + * @hide + */ + static RecoverableKeyStoreLoaderException fromServiceSpecificException( + ServiceSpecificException e) throws RecoverableKeyStoreLoaderException { + throw RecoverableKeyStoreLoaderException.fromErrorCode(e.errorCode); + } + + private RecoverableKeyStoreLoaderException(int errorCode, String message) { + super(message); + } + + /** Returns errorCode. */ + public int getErrorCode() { + return mErrorCode; + } + + /** @hide */ + private static String getMessageFromErrorCode(int errorCode) { + switch (errorCode) { + case NO_ERROR: + return "OK"; + case SYSTEM_ERROR: + return "System error"; + case UNINITIALIZED_RECOVERY_PUBLIC_KEY: + return "Recovery service is not initialized"; + case RATE_LIMIT_EXCEEDED: + return "Rate limit exceeded"; + default: + return String.valueOf("Unknown error code " + errorCode); + } + } + } + + /** + * Initializes key recovery service for the calling application. RecoverableKeyStoreLoader + * randomly chooses one of the keys from the list and keeps it to use for future key export + * operations. Collection of all keys in the list must be signed by the provided {@code + * rootCertificateAlias}, which must also be present in the list of root certificates + * preinstalled on the device. The random selection allows RecoverableKeyStoreLoader to select + * which of a set of remote recovery service devices will be used. + * + *

    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 getRecoverySnapshotVersions() + throws RecoverableKeyStoreLoaderException { + try { + // IPC doesn't support generic Maps. + @SuppressWarnings("unchecked") + Map result = + (Map) + 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: + * + *

      + *
    • {@link #RECOVERY_STATUS_SYNCED} + *
    • {@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} + *
    • {@link #RECOVERY_STATUS_MISSING_ACCOUNT} + *
    • {@link #RECOVERY_STATUS_PERMANENT_FAILURE} + *
    + * + * @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 getRecoveryStatus(@Nullable String packageName) + throws RecoverableKeyStoreLoaderException { + try { + // IPC doesn't support generic Maps. + @SuppressWarnings("unchecked") + Map result = + (Map) + 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 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 recoverKeys( + @NonNull String sessionId, + @NonNull byte[] recoveryKeyBlob, + @NonNull List applicationKeys) + throws RecoverableKeyStoreLoaderException { + try { + return (Map) mBinder.recoverKeys( + sessionId, recoveryKeyBlob, applicationKeys, UserHandle.getCallingUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); + } + } + + /** + * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the + * raw material of the key. + * + * @throws RecoverableKeyStoreLoaderException if an error occurred generating and storing the + * key. + */ + public byte[] generateAndStoreKey(String alias) throws RecoverableKeyStoreLoaderException { + try { + return mBinder.generateAndStoreKey(alias); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e); + } + } +} diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java index cd362c71..917efa8b 100644 --- a/android/service/autofill/AutofillService.java +++ b/android/service/autofill/AutofillService.java @@ -438,22 +438,61 @@ import com.android.internal.os.SomeArgs; * AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString(); * * save(username, password); - *
    - * + *
    * * *

    Privacy

    * *

    The {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} method is called * without the user content. The Android system strips some properties of the - * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to these calls, but not all + * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to this call, but not all * of them. For example, the data provided in the {@link android.view.ViewStructure.HtmlInfo} * objects set by {@link android.webkit.WebView} is never stripped out. * *

    Because this data could contain PII (Personally Identifiable Information, such as username or * email address), the service should only use it locally (i.e., in the app's process) for * heuristics purposes, but it should not be sent to external servers. + * + * + *

    Metrics and field classification

    The service can call {@link #getFillEventHistory()} to get metrics representing the user + * actions, and then use these metrics to improve its heuristics. + * + *

    Prior to Android {@link android.os.Build.VERSION_CODES#P}, the metrics covered just the + * scenarios where the service knew how to autofill an activity, but Android + * {@link android.os.Build.VERSION_CODES#P} introduced a new mechanism called field classification, + * which allows the service to dinamically classify the meaning of fields based on the existing user + * data known by the service. + * + *

    Typically, field classification can be used to detect fields that can be autofilled with + * user data that is not associated with a specific app—such as email and physical + * address. Once the service identifies that a such field was manually filled by the user, the + * service could use this signal to improve its heuristics, either locally (i.e., in the same + * device) or globally (i.e., by crowdsourcing the results back to the service's server so it can + * be used by other users). + * + *

    The field classification workflow involves 4 steps: + * + *

      + *
    1. Set the user data through {@link AutofillManager#setUserData(UserData)}. This data is + * cached until the system restarts (or the service is disabled), so it doesn't need to be set for + * all requests. + *
    2. Identify which fields should be analysed by calling + * {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)}. + *
    3. Verify the results through {@link FillEventHistory.Event#getFieldsClassification()}. + *
    4. Use the results to dynamically create {@link Dataset} or {@link SaveInfo} objects in future + * requests. + *
    + * + *

    The field classification is an expensive operation and should be used carefully, otherwise it + * can reach its rate limit and get blocked by the Android System. Ideally, it should be used just + * in cases where the service could not determine how an activity can be autofilled, but it has a + * strong suspicious that it could. For example, if an activity has four or more fields and one of + * them is a list, chances are that these are address fields (like address, city, state, and + * zip code). */ +// TODO(b/70407264): add code snippets above??? public abstract class AutofillService extends Service { private static final String TAG = "AutofillService"; diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java index cb20e71b..266bcda7 100644 --- a/android/service/autofill/Dataset.java +++ b/android/service/autofill/Dataset.java @@ -303,6 +303,10 @@ public final class Dataset implements Parcelable { * in the constructor in a dataset that is meant to be shown to the user, the autofill UI * for this field will not be displayed. * + *

    Note: On Android {@link android.os.Build.VERSION_CODES#P} and + * higher, datasets that require authentication can be also be filtered by passing a + * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value value to be autofilled. Pass {@code null} if you do not have the value @@ -320,6 +324,10 @@ public final class Dataset implements Parcelable { * Sets the value of a field, using a custom {@link RemoteViews presentation} to * visualize it. * + *

    Note: On Android {@link android.os.Build.VERSION_CODES#P} and + * higher, datasets that require authentication can be also be filtered by passing a + * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value the value to be autofilled. Pass {@code null} if you do not have the value @@ -340,12 +348,16 @@ public final class Dataset implements Parcelable { /** * Sets the value of a field using an explicit filter. * - *

    This method is typically used when the dataset is authenticated and the service + *

    This method is typically used when the dataset requires authentication and the service * does not know its value but wants to hide the dataset after the user enters a minimum * number of characters. For example, if the dataset represents a credit card number and the * service does not want to show the "Tap to authenticate" message until the user tapped * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. * + *

    Note: If the dataset requires authentication but the service knows its text + * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and + * use the value to filter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value the value to be autofilled. Pass {@code null} if you do not have the value @@ -371,12 +383,16 @@ public final class Dataset implements Parcelable { * Sets the value of a field, using a custom {@link RemoteViews presentation} to * visualize it and a explicit filter. * - *

    This method is typically used when the dataset is authenticated and the service + *

    This method is typically used when the dataset requires authentication and the service * does not know its value but wants to hide the dataset after the user enters a minimum * number of characters. For example, if the dataset represents a credit card number and the * service does not want to show the "Tap to authenticate" message until the user tapped * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. * + *

    Note: If the dataset requires authentication but the service knows its text + * value it's easier to filter by calling + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value the value to be autofilled. Pass {@code null} if you do not have the value @@ -405,6 +421,7 @@ public final class Dataset implements Parcelable { if (existingIdx >= 0) { mFieldValues.set(existingIdx, value); mFieldPresentations.set(existingIdx, presentation); + mFieldFilters.set(existingIdx, filter); return; } } else { diff --git a/android/service/autofill/EditDistanceScorer.java b/android/service/autofill/EditDistanceScorer.java new file mode 100644 index 00000000..0706b377 --- /dev/null +++ b/android/service/autofill/EditDistanceScorer.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Helper used to calculate the classification score between an actual {@link AutofillValue} filled + * by the user and the expected value predicted by an autofill service. + */ +// TODO(b/70291841): explain algorithm once it's fully implemented +public final class EditDistanceScorer extends InternalScorer implements Scorer, Parcelable { + + private static final EditDistanceScorer sInstance = new EditDistanceScorer(); + + /** + * Gets the singleton instance. + */ + public static EditDistanceScorer getInstance() { + return sInstance; + } + + private EditDistanceScorer() { + } + + /** @hide */ + @Override + public float getScore(@NonNull AutofillValue actualValue, @NonNull String userData) { + if (actualValue == null || !actualValue.isText() || userData == null) return 0; + // TODO(b/70291841): implement edit distance - currently it's returning either 0, 100%, or + // partial match when number of chars match + final String textValue = actualValue.getTextValue().toString(); + final int total = textValue.length(); + if (total != userData.length()) return 0F; + + int matches = 0; + for (int i = 0; i < total; i++) { + if (Character.toLowerCase(textValue.charAt(i)) == Character + .toLowerCase(userData.charAt(i))) { + matches++; + } + } + + return ((float) matches) / total; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + return "EditDistanceScorer"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + // Do nothing + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public EditDistanceScorer createFromParcel(Parcel parcel) { + return EditDistanceScorer.getInstance(); + } + + @Override + public EditDistanceScorer[] newArray(int size) { + return new EditDistanceScorer[size]; + } + }; +} diff --git a/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java new file mode 100644 index 00000000..001b2917 --- /dev/null +++ b/android/service/autofill/FieldClassification.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.view.autofill.Helper; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Represents the field classification + * results for a given field. + */ +public final class FieldClassification { + + private final ArrayList mMatches; + + /** @hide */ + public FieldClassification(@NonNull ArrayList matches) { + mMatches = Preconditions.checkNotNull(matches); + Collections.sort(mMatches, new Comparator() { + @Override + public int compare(Match o1, Match o2) { + if (o1.mScore > o2.mScore) return -1; + if (o1.mScore < o2.mScore) return 1; + return 0; + }} + ); + } + + /** + * Gets the {@link Match matches} with the highest {@link Match#getScore() scores} (sorted in + * descending order). + * + *

    Note: There's no guarantee of how many matches will be returned. In fact, + * the Android System might return just the top match to minimize the impact of field + * classification in the device's health. + */ + @NonNull + public List getMatches() { + return mMatches; + } + + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "FieldClassification: " + mMatches; + } + + private void writeToParcel(Parcel parcel) { + parcel.writeInt(mMatches.size()); + for (int i = 0; i < mMatches.size(); i++) { + mMatches.get(i).writeToParcel(parcel); + } + } + + private static FieldClassification readFromParcel(Parcel parcel) { + final int size = parcel.readInt(); + final ArrayList matches = new ArrayList<>(); + for (int i = 0; i < size; i++) { + matches.add(i, Match.readFromParcel(parcel)); + } + + return new FieldClassification(matches); + } + + static FieldClassification[] readArrayFromParcel(Parcel parcel) { + final int length = parcel.readInt(); + final FieldClassification[] fcs = new FieldClassification[length]; + for (int i = 0; i < length; i++) { + fcs[i] = readFromParcel(parcel); + } + return fcs; + } + + static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull FieldClassification[] fcs) { + parcel.writeInt(fcs.length); + for (int i = 0; i < fcs.length; i++) { + fcs[i].writeToParcel(parcel); + } + } + + /** + * Represents the score of a {@link UserData} entry for the field. + * + *

    The score is defined by {@link #getScore()} and the entry is identified by + * {@link #getRemoteId()}. + */ + public static final class Match { + + private final String mRemoteId; + private final float mScore; + + /** @hide */ + public Match(String remoteId, float score) { + mRemoteId = Preconditions.checkNotNull(remoteId); + mScore = score; + } + + /** + * Gets the remote id of the {@link UserData} entry. + */ + @NonNull + public String getRemoteId() { + return mRemoteId; + } + + /** + * Gets a classification score for the value of this field compared to the value of the + * {@link UserData} entry. + * + *

    The score is based in a comparison of the field value and the user data entry, and it + * ranges from {@code 0.0F} to {@code 1.0F}: + *

      + *
    • {@code 1.0F} represents a full match ({@code 100%}). + *
    • {@code 0.0F} represents a full mismatch ({@code 0%}). + *
    • Any other value is a partial match. + *
    + * + *

    How the score is calculated depends on the algorithm used by the {@link Scorer} + * implementation. + */ + public float getScore() { + return mScore; + } + + @Override + public String toString() { + if (!sDebug) return super.toString(); + + final StringBuilder string = new StringBuilder("Match: remoteId="); + Helper.appendRedacted(string, mRemoteId); + return string.append(", score=").append(mScore).toString(); + } + + private void writeToParcel(@NonNull Parcel parcel) { + parcel.writeString(mRemoteId); + parcel.writeFloat(mScore); + } + + private static Match readFromParcel(@NonNull Parcel parcel) { + return new Match(parcel.readString(), parcel.readFloat()); + } + } +} diff --git a/android/service/autofill/FieldsDetection.java b/android/service/autofill/FieldsDetection.java deleted file mode 100644 index 550ecf68..00000000 --- a/android/service/autofill/FieldsDetection.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.service.autofill; - -import android.annotation.TestApi; -import android.os.Parcel; -import android.os.Parcelable; -import android.view.autofill.AutofillId; - -/** - * Class by service to improve autofillable fields detection by tracking the meaning of fields - * manually edited by the user (when they match values provided by the service). - * - * TODO(b/67867469): - * - proper javadoc - * - unhide / remove testApi - * - add FieldsDetection management so service can set it just once and reference it in further - * calls to improve performance (and also API to refresh it) - * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests) - * - add FieldsDetectionUnitTest once API is well-defined - * @hide - */ -@TestApi -public final class FieldsDetection implements Parcelable { - - private final AutofillId mFieldId; - private final String mRemoteId; - private final String mValue; - - /** - * Creates a field detection for just one field / value pair. - * - * @param fieldId autofill id of the field in the screen. - * @param remoteId id used by the service to identify the field later. - * @param value field value known to the service. - * - * TODO(b/67867469): - * - proper javadoc - * - change signature to allow more fields / values / match methods - * - might also need to use a builder, where the constructor is the id for the fieldsdetector - * - might need id for values as well - * - add @NonNull / check it / add unit tests - * - make 'value' input more generic so it can accept distance-based match and other matches - * - throw exception if field value is less than X characters (somewhere between 7-10) - * - make sure to limit total number of fields to around 10 or so - * - use AutofillValue instead of String (so it can compare dates, for example) - */ - public FieldsDetection(AutofillId fieldId, String remoteId, String value) { - mFieldId = fieldId; - mRemoteId = remoteId; - mValue = value; - } - - /** @hide */ - public AutofillId getFieldId() { - return mFieldId; - } - - /** @hide */ - public String getRemoteId() { - return mRemoteId; - } - - /** @hide */ - public String getValue() { - return mValue; - } - - ///////////////////////////////////// - // Object "contract" methods. // - ///////////////////////////////////// - @Override - public String toString() { - // Cannot disclose remoteId or value because they could contain PII - return new StringBuilder("FieldsDetection: [field=").append(mFieldId) - .append(", remoteId_length=").append(mRemoteId.length()) - .append(", value_length=").append(mValue.length()) - .append("]").toString(); - } - - ///////////////////////////////////// - // Parcelable "contract" methods. // - ///////////////////////////////////// - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeParcelable(mFieldId, flags); - parcel.writeString(mRemoteId); - parcel.writeString(mValue); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public FieldsDetection createFromParcel(Parcel parcel) { - // TODO(b/67867469): remove comment below if it does not use a builder at the end - // Always go through the builder to ensure the data ingested by - // the system obeys the contract of the builder to avoid attacks - // using specially crafted parcels. - return new FieldsDetection(parcel.readParcelable(null), parcel.readString(), - parcel.readString()); - } - - @Override - public FieldsDetection[] newArray(int size) { - return new FieldsDetection[size]; - } - }; -} diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java index 736d9ef4..df624464 100644 --- a/android/service/autofill/FillEventHistory.java +++ b/android/service/autofill/FillEventHistory.java @@ -16,16 +16,18 @@ package android.service.autofill; +import static android.view.autofill.Helper.sVerbose; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.TestApi; import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -35,6 +37,7 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -57,10 +60,7 @@ import java.util.Set; * the history will clear out after some pre-defined time). */ public final class FillEventHistory implements Parcelable { - /** - * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}. - */ - private final int mServiceUid; + private static final String TAG = "FillEventHistory"; /** * Not in parcel. The ID of the autofill session that created the {@link FillResponse}. @@ -70,17 +70,6 @@ public final class FillEventHistory implements Parcelable { @Nullable private final Bundle mClientState; @Nullable List mEvents; - /** - * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}. - * - * @return The UID of the {@link AutofillService} - * - * @hide - */ - public int getServiceUid() { - return mServiceUid; - } - /** @hide */ public int getSessionId() { return mSessionId; @@ -123,9 +112,8 @@ public final class FillEventHistory implements Parcelable { /** * @hide */ - public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) { + public FillEventHistory(int sessionId, @Nullable Bundle clientState) { mClientState = clientState; - mServiceUid = serviceUid; mSessionId = sessionId; } @@ -140,34 +128,36 @@ public final class FillEventHistory implements Parcelable { } @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeBundle(mClientState); + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBundle(mClientState); if (mEvents == null) { - dest.writeInt(0); + parcel.writeInt(0); } else { - dest.writeInt(mEvents.size()); + parcel.writeInt(mEvents.size()); int numEvents = mEvents.size(); for (int i = 0; i < numEvents; i++) { Event event = mEvents.get(i); - dest.writeInt(event.mEventType); - dest.writeString(event.mDatasetId); - dest.writeBundle(event.mClientState); - dest.writeStringList(event.mSelectedDatasetIds); - dest.writeArraySet(event.mIgnoredDatasetIds); - dest.writeTypedList(event.mChangedFieldIds); - dest.writeStringList(event.mChangedDatasetIds); - - dest.writeTypedList(event.mManuallyFilledFieldIds); + parcel.writeInt(event.mEventType); + parcel.writeString(event.mDatasetId); + parcel.writeBundle(event.mClientState); + parcel.writeStringList(event.mSelectedDatasetIds); + parcel.writeArraySet(event.mIgnoredDatasetIds); + parcel.writeTypedList(event.mChangedFieldIds); + parcel.writeStringList(event.mChangedDatasetIds); + + parcel.writeTypedList(event.mManuallyFilledFieldIds); if (event.mManuallyFilledFieldIds != null) { final int size = event.mManuallyFilledFieldIds.size(); for (int j = 0; j < size; j++) { - dest.writeStringList(event.mManuallyFilledDatasetIds.get(j)); + parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j)); } } - dest.writeString(event.mDetectedRemoteId); - if (event.mDetectedRemoteId != null) { - dest.writeInt(event.mDetectedFieldScore); + final AutofillId[] detectedFields = event.mDetectedFieldIds; + parcel.writeParcelableArray(detectedFields, flags); + if (detectedFields != null) { + FieldClassification.writeArrayToParcel(parcel, + event.mDetectedFieldClassifications); } } } @@ -217,6 +207,7 @@ public final class FillEventHistory implements Parcelable { * ({@link #getIgnoredDatasetIds()}). *

  • Which fields in the selected datasets were changed by the user after the dataset * was selected ({@link #getChangedFields()}. + *
  • Which fields match the {@link UserData} set by the service. * * *

    Note: This event is only generated when: @@ -231,16 +222,16 @@ public final class FillEventHistory implements Parcelable { *

    See {@link android.view.autofill.AutofillManager} for more information about autofill * contexts. */ - // TODO(b/67867469): update with field detection behavior public static final int TYPE_CONTEXT_COMMITTED = 4; /** @hide */ - @IntDef( - value = {TYPE_DATASET_SELECTED, - TYPE_DATASET_AUTHENTICATION_SELECTED, - TYPE_AUTHENTICATION_SELECTED, - TYPE_SAVE_SHOWN, - TYPE_CONTEXT_COMMITTED}) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_DATASET_SELECTED, + TYPE_DATASET_AUTHENTICATION_SELECTED, + TYPE_AUTHENTICATION_SELECTED, + TYPE_SAVE_SHOWN, + TYPE_CONTEXT_COMMITTED + }) @Retention(RetentionPolicy.SOURCE) @interface EventIds{} @@ -259,8 +250,8 @@ public final class FillEventHistory implements Parcelable { @Nullable private final ArrayList mManuallyFilledFieldIds; @Nullable private final ArrayList> mManuallyFilledDatasetIds; - @Nullable private final String mDetectedRemoteId; - private final int mDetectedFieldScore; + @Nullable private final AutofillId[] mDetectedFieldIds; + @Nullable private final FieldClassification[] mDetectedFieldClassifications; /** * Returns the type of the event. @@ -364,35 +355,27 @@ public final class FillEventHistory implements Parcelable { } /** - * Gets the results of the last {@link FieldsDetection} request. - * - * @return map of edit-distance match ({@code 0} means full match, - * {@code 1} means 1 character different, etc...) by remote id (as set in the - * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values - * matched the requested detection. + * Gets the field classification + * results. * *

    Note: Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the - * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields - * detection}. - * - * TODO(b/67867469): - * - improve javadoc - * - refine score meaning (for example, should 1 be different of -1?) - * - mention when it's set - * - unhide - * - unhide / remove testApi - * - add @NonNull / check it / add unit tests - * - * @hide + * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...) + * field classification}. */ - @TestApi - @NonNull public Map getDetectedFields() { - if (mDetectedRemoteId == null || mDetectedFieldScore == -1) { + @NonNull public Map getFieldsClassification() { + if (mDetectedFieldIds == null) { return Collections.emptyMap(); } - - final ArrayMap map = new ArrayMap<>(1); - map.put(mDetectedRemoteId, mDetectedFieldScore); + final int size = mDetectedFieldIds.length; + final ArrayMap map = new ArrayMap<>(size); + for (int i = 0; i < size; i++) { + final AutofillId id = mDetectedFieldIds[i]; + final FieldClassification fc = mDetectedFieldClassifications[i]; + if (sVerbose) { + Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc); + } + map.put(id, fc); + } return map; } @@ -472,6 +455,8 @@ public final class FillEventHistory implements Parcelable { * and belonged to datasets. * @param manuallyFilledDatasetIds The ids of datasets that had values matching the * respective entry on {@code manuallyFilledFieldIds}. + * @param detectedFieldClassifications the field classification matches. + * * @throws IllegalArgumentException If the length of {@code changedFieldIds} and * {@code changedDatasetIds} doesn't match. * @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and @@ -479,7 +464,6 @@ public final class FillEventHistory implements Parcelable { * * @hide */ - // TODO(b/67867469): document detection field parameters once stable public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState, @Nullable List selectedDatasetIds, @Nullable ArraySet ignoredDatasetIds, @@ -487,7 +471,8 @@ public final class FillEventHistory implements Parcelable { @Nullable ArrayList changedDatasetIds, @Nullable ArrayList manuallyFilledFieldIds, @Nullable ArrayList> manuallyFilledDatasetIds, - @Nullable String detectedRemoteId, int detectedFieldScore) { + @Nullable AutofillId[] detectedFieldIds, + @Nullable FieldClassification[] detectedFieldClassifications) { mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED, "eventType"); mDatasetId = datasetId; @@ -510,8 +495,9 @@ public final class FillEventHistory implements Parcelable { } mManuallyFilledFieldIds = manuallyFilledFieldIds; mManuallyFilledDatasetIds = manuallyFilledDatasetIds; - mDetectedRemoteId = detectedRemoteId; - mDetectedFieldScore = detectedFieldScore; + + mDetectedFieldIds = detectedFieldIds; + mDetectedFieldClassifications = detectedFieldClassifications; } @Override @@ -524,8 +510,9 @@ public final class FillEventHistory implements Parcelable { + ", changedDatasetsIds=" + mChangedDatasetIds + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds - + ", detectedRemoteId=" + mDetectedRemoteId - + ", detectedFieldScore=" + mDetectedFieldScore + + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds) + + ", detectedFieldClassifications =" + + Arrays.toString(mDetectedFieldClassifications) + "]"; } } @@ -534,7 +521,7 @@ public final class FillEventHistory implements Parcelable { new Parcelable.Creator() { @Override public FillEventHistory createFromParcel(Parcel parcel) { - FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle()); + FillEventHistory selection = new FillEventHistory(0, parcel.readBundle()); final int numEvents = parcel.readInt(); for (int i = 0; i < numEvents; i++) { @@ -561,15 +548,18 @@ public final class FillEventHistory implements Parcelable { } else { manuallyFilledDatasetIds = null; } - final String detectedRemoteId = parcel.readString(); - final int detectedFieldScore = detectedRemoteId == null ? -1 - : parcel.readInt(); + final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null, + AutofillId.class); + final FieldClassification[] detectedFieldClassifications = + (detectedFieldIds != null) + ? FieldClassification.readArrayFromParcel(parcel) + : null; selection.addEvent(new Event(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, - detectedRemoteId, detectedFieldScore)); + detectedFieldIds, detectedFieldClassifications)); } return selection; } diff --git a/android/service/autofill/FillRequest.java b/android/service/autofill/FillRequest.java index 3a842240..33619ac6 100644 --- a/android/service/autofill/FillRequest.java +++ b/android/service/autofill/FillRequest.java @@ -69,9 +69,9 @@ public final class FillRequest implements Parcelable { public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE; /** @hide */ - @IntDef( - flag = true, - value = {FLAG_MANUAL_REQUEST}) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_MANUAL_REQUEST + }) @Retention(RetentionPolicy.SOURCE) @interface RequestFlags{} @@ -127,12 +127,15 @@ public final class FillRequest implements Parcelable { } /** - * Gets the extra client state returned from the last {@link - * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback) - * fill request}, so the service can use it for state management. + * Gets the latest client state bundle set by the service in a + * {@link FillResponse.Builder#setClientState(Bundle) fill response}. * - *

    Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) - * save request} is made, the client state is cleared. + *

    Note: Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state + * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On + * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of + * an authenticated request through the + * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are + * also considered (and take precedence when set). * * @return The client state. */ diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java index 84a0974d..3a4b6bb8 100644 --- a/android/service/autofill/FillResponse.java +++ b/android/service/autofill/FillResponse.java @@ -41,7 +41,7 @@ import java.util.Arrays; import java.util.List; /** - * Response for a {@link + * Response for an {@link * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}. * *

    See the main {@link AutofillService} documentation for more details and examples. @@ -49,19 +49,21 @@ import java.util.List; public final class FillResponse implements Parcelable { /** - * Must be set in the last response to generate - * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events. + * Flag used to generate {@link FillEventHistory.Event events} of type + * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}—if this flag is not passed to + * {@link Builder#setFlags(int)}, these events are not generated. */ public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1; /** - * Used in conjunction to {@link FillResponse.Builder#disableAutofill(long)} to disable autofill - * only for the activiy associated with the {@link FillResponse}, instead of the whole app. + * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}— + * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the + * activiy that generated the {@link FillRequest}, not the whole app. */ public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2; /** @hide */ - @IntDef(flag = true, value = { + @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_TRACK_CONTEXT_COMMITED, FLAG_DISABLE_ACTIVITY_ONLY }) @@ -72,11 +74,13 @@ public final class FillResponse implements Parcelable { private final @Nullable SaveInfo mSaveInfo; private final @Nullable Bundle mClientState; private final @Nullable RemoteViews mPresentation; + private final @Nullable RemoteViews mHeader; + private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; private final long mDisableDuration; - private final @Nullable FieldsDetection mFieldsDetection; + private final @Nullable AutofillId[] mFieldClassificationIds; private final int mFlags; private int mRequestId; @@ -85,11 +89,13 @@ public final class FillResponse implements Parcelable { mSaveInfo = builder.mSaveInfo; mClientState = builder.mClientState; mPresentation = builder.mPresentation; + mHeader = builder.mHeader; + mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; - mFieldsDetection = builder.mFieldsDetection; + mFieldClassificationIds = builder.mFieldClassificationIds; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -114,6 +120,16 @@ public final class FillResponse implements Parcelable { return mPresentation; } + /** @hide */ + public @Nullable RemoteViews getHeader() { + return mHeader; + } + + /** @hide */ + public @Nullable RemoteViews getFooter() { + return mFooter; + } + /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; @@ -135,11 +151,12 @@ public final class FillResponse implements Parcelable { } /** @hide */ - public @Nullable FieldsDetection getFieldsDetection() { - return mFieldsDetection; + public @Nullable AutofillId[] getFieldClassificationIds() { + return mFieldClassificationIds; } /** @hide */ + @TestApi public int getFlags() { return mFlags; } @@ -171,11 +188,13 @@ public final class FillResponse implements Parcelable { private SaveInfo mSaveInfo; private Bundle mClientState; private RemoteViews mPresentation; + private RemoteViews mHeader; + private RemoteViews mFooter; private IntentSender mAuthentication; private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; - private FieldsDetection mFieldsDetection; + private AutofillId[] mFieldClassificationIds; private int mFlags; private boolean mDestroyed; @@ -226,16 +245,24 @@ public final class FillResponse implements Parcelable { * @param ids id of Views that when focused will display the authentication UI. * * @return This builder. + * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if * both {@code authentication} and {@code presentation} are {@code null}, or if * both {@code authentication} and {@code presentation} are non-{@code null} * + * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a + * {@link #setFooter(RemoteViews) footer} are already set for this builder. + * * @see android.app.PendingIntent#getIntentSender() */ public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); + if (mHeader != null || mFooter != null) { + throw new IllegalStateException("Already called #setHeader() or #setFooter()"); + } + if (ids == null || ids.length == 0) { throw new IllegalArgumentException("ids cannot be null or empry"); } @@ -305,19 +332,16 @@ public final class FillResponse implements Parcelable { } /** - * Sets a {@link Bundle state} that will be passed to subsequent APIs that - * manipulate this response. For example, they are passed to subsequent - * calls to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, - * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}. - * You can use this to store intermediate state that is persistent across multiple - * fill requests and the subsequent save request. + * Sets a bundle with state that is passed to subsequent APIs that manipulate this response. + * + *

    You can use this bundle to store intermediate state that is passed to subsequent calls + * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, + * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and + * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}. * *

    If this method is called on multiple {@link FillResponse} objects for the same * screen, just the latest bundle is passed back to the service. * - *

    Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) - * save request} is made the client state is cleared. - * * @param clientState The custom client state. * @return This builder. */ @@ -329,21 +353,26 @@ public final class FillResponse implements Parcelable { } /** - * TODO(b/67867469): - * - javadoc it - * - javadoc how to check results - * - unhide - * - unhide / remove testApi - * - throw exception (and document) if response has datasets or saveinfo - * - throw exception (and document) if id on fieldsDetection is ignored - * - * @hide + * Sets which fields are used for + * field classification + * + *

    Note: This method automatically adds the + * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} to the {@link #setFlags(int) flags}. + + * @throws IllegalArgumentException is length of {@code ids} args is more than + * {@link UserData#getMaxFieldClassificationIdsSize()}. + * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was + * already called. + * @throws NullPointerException if {@code ids} or any element on it is {@code null}. */ - @TestApi - public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) { + public Builder setFieldClassificationIds(@NonNull AutofillId... ids) { throwIfDestroyed(); throwIfDisableAutofillCalled(); - mFieldsDetection = Preconditions.checkNotNull(fieldsDetection); + Preconditions.checkArrayElementsNotNull(ids, "ids"); + Preconditions.checkArgumentInRange(ids.length, 1, + UserData.getMaxFieldClassificationIdsSize(), "ids length"); + mFieldClassificationIds = ids; + mFlags |= FLAG_TRACK_CONTEXT_COMMITED; return this; } @@ -391,8 +420,8 @@ public final class FillResponse implements Parcelable { * @throws IllegalArgumentException if {@code duration} is not a positive number. * @throws IllegalStateException if either {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, - * {@link #setSaveInfo(SaveInfo)}, or {@link #setClientState(Bundle)} - * was already called. + * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or + * {@link #setFieldClassificationIds(AutofillId...)} was already called. */ public Builder disableAutofill(long duration) { throwIfDestroyed(); @@ -400,7 +429,7 @@ public final class FillResponse implements Parcelable { throw new IllegalArgumentException("duration must be greater than 0"); } if (mAuthentication != null || mDatasets != null || mSaveInfo != null - || mFieldsDetection != null || mClientState != null) { + || mFieldClassificationIds != null || mClientState != null) { throw new IllegalStateException("disableAutofill() must be the only method called"); } @@ -408,6 +437,62 @@ public final class FillResponse implements Parcelable { return this; } + /** + * Sets a header to be shown as the first element in the list of datasets. + * + *

    When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, + * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this + * method should only be used on {@link FillResponse FillResponses} that do not require + * authentication (as the header could have been set directly in the main presentation in + * these cases). + * + * @param header a presentation to represent the header. This presentation is not clickable + * —calling + * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would + * have no effect. + * + * @return this builder + * + * @throws IllegalStateException if an + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was + * already set for this builder. + */ + // TODO(b/69796626): make it sticky / update javadoc + public Builder setHeader(@NonNull RemoteViews header) { + throwIfDestroyed(); + throwIfAuthenticationCalled(); + mHeader = Preconditions.checkNotNull(header); + return this; + } + + /** + * Sets a footer to be shown as the last element in the list of datasets. + * + *

    When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, + * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this + * method should only be used on {@link FillResponse FillResponses} that do not require + * authentication (as the footer could have been set directly in the main presentation in + * these cases). + * + * @param footer a presentation to represent the footer. This presentation is not clickable + * —calling + * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would + * have no effect. + * + * @return this builder + * + * @throws IllegalStateException if the FillResponse + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) + * requires authentication}. + */ + // TODO(b/69796626): make it sticky / update javadoc + public Builder setFooter(@NonNull RemoteViews footer) { + throwIfDestroyed(); + throwIfAuthenticationCalled(); + mFooter = Preconditions.checkNotNull(footer); + return this; + } + /** * Builds a new {@link FillResponse} instance. * @@ -417,7 +502,10 @@ public final class FillResponse implements Parcelable { *

  • No call was made to {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)}, - * or {@link #setClientState(Bundle)}. + * {@link #setClientState(Bundle)}, + * or {@link #setFieldClassificationIds(AutofillId...)}. + *
  • {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called + * without any previous calls to {@link #addDataset(Dataset)}. * * * @return A built response. @@ -425,11 +513,16 @@ public final class FillResponse implements Parcelable { public FillResponse build() { throwIfDestroyed(); if (mAuthentication == null && mDatasets == null && mSaveInfo == null - && mDisableDuration == 0 && mFieldsDetection == null && mClientState == null) { + && mDisableDuration == 0 && mFieldClassificationIds == null + && mClientState == null) { throw new IllegalStateException("need to provide: at least one DataSet, or a " + "SaveInfo, or an authentication with a presentation, " + "or a FieldsDetection, or a client state, or disable autofill"); } + if (mDatasets == null && (mHeader != null || mFooter != null)) { + throw new IllegalStateException( + "must add at least 1 dataset when using header or footer"); + } mDestroyed = true; return new FillResponse(this); } @@ -445,6 +538,12 @@ public final class FillResponse implements Parcelable { throw new IllegalStateException("Already called #disableAutofill()"); } } + + private void throwIfAuthenticationCalled() { + if (mAuthentication != null) { + throw new IllegalStateException("Already called #setAuthentication()"); + } + } } ///////////////////////////////////// @@ -461,12 +560,15 @@ public final class FillResponse implements Parcelable { .append(", saveInfo=").append(mSaveInfo) .append(", clientState=").append(mClientState != null) .append(", hasPresentation=").append(mPresentation != null) + .append(", hasHeader=").append(mHeader != null) + .append(", hasFooter=").append(mFooter != null) .append(", hasAuthentication=").append(mAuthentication != null) .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)) .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) .append(", disableDuration=").append(mDisableDuration) .append(", flags=").append(mFlags) - .append(", fieldDetection=").append(mFieldsDetection) + .append(", fieldClassificationIds=") + .append(Arrays.toString(mFieldClassificationIds)) .append("]") .toString(); } @@ -488,9 +590,11 @@ public final class FillResponse implements Parcelable { parcel.writeParcelableArray(mAuthenticationIds, flags); parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); + parcel.writeParcelable(mHeader, flags); + parcel.writeParcelable(mFooter, flags); parcel.writeParcelableArray(mIgnoredIds, flags); parcel.writeLong(mDisableDuration); - parcel.writeParcelable(mFieldsDetection, flags); + parcel.writeParcelableArray(mFieldClassificationIds, flags); parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -520,15 +624,24 @@ public final class FillResponse implements Parcelable { if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation); } + final RemoteViews header = parcel.readParcelable(null); + if (header != null) { + builder.setHeader(header); + } + final RemoteViews footer = parcel.readParcelable(null); + if (footer != null) { + builder.setFooter(footer); + } builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class)); final long disableDuration = parcel.readLong(); if (disableDuration > 0) { builder.disableAutofill(disableDuration); } - final FieldsDetection fieldsDetection = parcel.readParcelable(null); - if (fieldsDetection != null) { - builder.setFieldsDetection(fieldsDetection); + final AutofillId[] fieldClassifactionIds = + parcel.readParcelableArray(null, AutofillId.class); + if (fieldClassifactionIds != null) { + builder.setFieldClassificationIds(fieldClassifactionIds); } builder.setFlags(parcel.readInt()); diff --git a/android/service/autofill/InternalScorer.java b/android/service/autofill/InternalScorer.java new file mode 100644 index 00000000..0da5afc2 --- /dev/null +++ b/android/service/autofill/InternalScorer.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all scorer the system understands. As this is not public all + * subclasses have to implement {@link Scorer} again. + * + * @hide + */ +@TestApi +public abstract class InternalScorer implements Scorer, Parcelable { + + /** + * Returns the classification score between an actual {@link AutofillValue} filled + * by the user and the expected value predicted by an autofill service. + * + *

    A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and + * partial mathces are something in between, typically using edit-distance algorithms. + */ + public abstract float getScore(@NonNull AutofillValue actualValue, @NonNull String userData); +} diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java index 0b50f074..a5a6177d 100644 --- a/android/service/autofill/SaveInfo.java +++ b/android/service/autofill/SaveInfo.java @@ -198,23 +198,22 @@ public final class SaveInfo implements Parcelable { public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; /** @hide */ - @IntDef( - value = { - NEGATIVE_BUTTON_STYLE_CANCEL, - NEGATIVE_BUTTON_STYLE_REJECT}) + @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = { + NEGATIVE_BUTTON_STYLE_CANCEL, + NEGATIVE_BUTTON_STYLE_REJECT + }) @Retention(RetentionPolicy.SOURCE) @interface NegativeButtonStyle{} /** @hide */ - @IntDef( - flag = true, - value = { - SAVE_DATA_TYPE_GENERIC, - SAVE_DATA_TYPE_PASSWORD, - SAVE_DATA_TYPE_ADDRESS, - SAVE_DATA_TYPE_CREDIT_CARD, - SAVE_DATA_TYPE_USERNAME, - SAVE_DATA_TYPE_EMAIL_ADDRESS}) + @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = { + SAVE_DATA_TYPE_GENERIC, + SAVE_DATA_TYPE_PASSWORD, + SAVE_DATA_TYPE_ADDRESS, + SAVE_DATA_TYPE_CREDIT_CARD, + SAVE_DATA_TYPE_USERNAME, + SAVE_DATA_TYPE_EMAIL_ADDRESS + }) @Retention(RetentionPolicy.SOURCE) @interface SaveDataType{} @@ -235,9 +234,10 @@ public final class SaveInfo implements Parcelable { public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; /** @hide */ - @IntDef( - flag = true, - value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, FLAG_DONT_SAVE_ON_FINISH}) + @IntDef(flag = true, prefix = { "FLAG_" }, value = { + FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, + FLAG_DONT_SAVE_ON_FINISH + }) @Retention(RetentionPolicy.SOURCE) @interface SaveInfoFlags{} diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java index f53967bd..4f85e6b9 100644 --- a/android/service/autofill/SaveRequest.java +++ b/android/service/autofill/SaveRequest.java @@ -59,10 +59,11 @@ public final class SaveRequest implements Parcelable { } /** - * Gets the latest client state extra returned from the service. + * Gets the latest client state bundle set by the service in a + * {@link FillResponse.Builder#setClientState(Bundle) fill response}. * *

    Note: Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state - * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} where considered. On + * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of * an authenticated request through the * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are diff --git a/android/service/autofill/Scorer.java b/android/service/autofill/Scorer.java new file mode 100644 index 00000000..c4018558 --- /dev/null +++ b/android/service/autofill/Scorer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Helper class used to calculate a score. + * + *

    Typically used to calculate the + * field classification score between an + * actual {@link android.view.autofill.AutofillValue} filled by the user and the expected value + * predicted by an autofill service. + */ +public interface Scorer { + +} diff --git a/android/service/autofill/UserData.java b/android/service/autofill/UserData.java new file mode 100644 index 00000000..f0cc360f --- /dev/null +++ b/android/service/autofill/UserData.java @@ -0,0 +1,307 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH; +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.content.ContentResolver; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.Settings; +import android.util.Log; +import android.view.autofill.Helper; + +import com.android.internal.util.Preconditions; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Defines the user data used for + * field classification. + */ +public final class UserData implements Parcelable { + + private static final String TAG = "UserData"; + + private static final int DEFAULT_MAX_USER_DATA_SIZE = 10; + private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10; + private static final int DEFAULT_MIN_VALUE_LENGTH = 5; + private static final int DEFAULT_MAX_VALUE_LENGTH = 100; + + private final InternalScorer mScorer; + private final String[] mRemoteIds; + private final String[] mValues; + + private UserData(Builder builder) { + mScorer = builder.mScorer; + mRemoteIds = new String[builder.mRemoteIds.size()]; + builder.mRemoteIds.toArray(mRemoteIds); + mValues = new String[builder.mValues.size()]; + builder.mValues.toArray(mValues); + } + + /** @hide */ + public InternalScorer getScorer() { + return mScorer; + } + + /** @hide */ + public String[] getRemoteIds() { + return mRemoteIds; + } + + /** @hide */ + public String[] getValues() { + return mValues; + } + + /** @hide */ + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer); + // Cannot disclose remote ids or values because they could contain PII + pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length); + for (int i = 0; i < mRemoteIds.length; i++) { + pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); + pw.println(Helper.getRedacted(mRemoteIds[i])); + } + pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length); + for (int i = 0; i < mValues.length; i++) { + pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); + pw.println(Helper.getRedacted(mValues[i])); + } + } + + /** @hide */ + public static void dumpConstraints(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize()); + pw.print(prefix); pw.print("maxFieldClassificationIdsSize: "); + pw.println(getMaxFieldClassificationIdsSize()); + pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength()); + pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength()); + } + + /** + * A builder for {@link UserData} objects. + */ + public static final class Builder { + private final InternalScorer mScorer; + private final ArrayList mRemoteIds; + private final ArrayList mValues; + private boolean mDestroyed; + + /** + * Creates a new builder for the user data used for field + * classification. + * + * @throws IllegalArgumentException if any of the following occurs: + *

      + *
    1. {@code remoteId} is empty + *
    2. {@code value} is empty + *
    3. the length of {@code value} is lower than {@link UserData#getMinValueLength()} + *
    4. the length of {@code value} is higher than {@link UserData#getMaxValueLength()} + *
    5. {@code scorer} is not instance of a class provided by the Android System. + *
    + */ + public Builder(@NonNull Scorer scorer, @NonNull String remoteId, @NonNull String value) { + Preconditions.checkArgument((scorer instanceof InternalScorer), + "not provided by Android System: " + scorer); + mScorer = (InternalScorer) scorer; + checkValidRemoteId(remoteId); + checkValidValue(value); + final int capacity = getMaxUserDataSize(); + mRemoteIds = new ArrayList<>(capacity); + mValues = new ArrayList<>(capacity); + mRemoteIds.add(remoteId); + mValues.add(value); + } + + /** + * Adds a new value for user data. + * + * @param remoteId unique string used to identify the user data. + * @param value value of the user data. + * + * @throws IllegalStateException if {@link #build()} or + * {@link #add(String, String)} with the same {@code remoteId} has already + * been called, or if the number of values add (i.e., calls made to this method plus + * constructor) is more than {@link UserData#getMaxUserDataSize()}. + * + * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the + * length of {@code value} is lower than {@link UserData#getMinValueLength()} + * or higher than {@link UserData#getMaxValueLength()}. + */ + public Builder add(@NonNull String remoteId, @NonNull String value) { + throwIfDestroyed(); + checkValidRemoteId(remoteId); + checkValidValue(value); + + Preconditions.checkState(!mRemoteIds.contains(remoteId), + // Don't include remoteId on message because it could contain PII + "already has entry with same remoteId"); + Preconditions.checkState(!mValues.contains(value), + // Don't include remoteId on message because it could contain PII + "already has entry with same value"); + Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(), + "already added " + mRemoteIds.size() + " elements"); + mRemoteIds.add(remoteId); + mValues.add(value); + + return this; + } + + private void checkValidRemoteId(@Nullable String remoteId) { + Preconditions.checkNotNull(remoteId); + Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty"); + } + + private void checkValidValue(@Nullable String value) { + Preconditions.checkNotNull(value); + final int length = value.length(); + Preconditions.checkArgumentInRange(length, getMinValueLength(), + getMaxValueLength(), "value length (" + length + ")"); + } + + /** + * Creates a new {@link UserData} instance. + * + *

    You should not interact with this builder once this method is called. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return The built dataset. + */ + public UserData build() { + throwIfDestroyed(); + mDestroyed = true; + return new UserData(this); + } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer); + // Cannot disclose remote ids or values because they could contain PII + builder.append(", remoteIds="); + Helper.appendRedacted(builder, mRemoteIds); + builder.append(", values="); + Helper.appendRedacted(builder, mValues); + return builder.append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mScorer, flags); + parcel.writeStringArray(mRemoteIds); + parcel.writeStringArray(mValues); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public UserData createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final InternalScorer scorer = parcel.readParcelable(null); + final String[] remoteIds = parcel.readStringArray(); + final String[] values = parcel.readStringArray(); + final Builder builder = new Builder(scorer, remoteIds[0], values[0]); + for (int i = 1; i < remoteIds.length; i++) { + builder.add(remoteIds[i], values[i]); + } + return builder.build(); + } + + @Override + public UserData[] newArray(int size) { + return new UserData[size]; + } + }; + + /** + * Gets the maximum number of values that can be added to a {@link UserData}. + */ + public static int getMaxUserDataSize() { + return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE); + } + + /** + * Gets the maximum number of ids that can be passed to {@link + * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}. + */ + public static int getMaxFieldClassificationIdsSize() { + return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, + DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE); + } + + /** + * Gets the minimum length of values passed to the builder's constructor or + * or {@link Builder#add(String, String)}. + */ + public static int getMinValueLength() { + return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH); + } + + /** + * Gets the maximum length of values passed to the builder's constructor or + * or {@link Builder#add(String, String)}. + */ + public static int getMaxValueLength() { + return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH); + } + + private static int getInt(String settings, int defaultValue) { + ContentResolver cr = null; + final ActivityThread at = ActivityThread.currentActivityThread(); + if (at != null) { + cr = at.getApplication().getContentResolver(); + } + + if (cr == null) { + Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue); + return defaultValue; + } + return Settings.Secure.getInt(cr, settings, defaultValue); + } +} diff --git a/android/service/autofill/Validators.java b/android/service/autofill/Validators.java index 1c838687..0f1ba989 100644 --- a/android/service/autofill/Validators.java +++ b/android/service/autofill/Validators.java @@ -33,6 +33,8 @@ public final class Validators { /** * Creates a validator that is only valid if all {@code validators} are valid. * + *

    Used to represent an {@code AND} boolean operation in a chain of validators. + * * @throws IllegalArgumentException if any element of {@code validators} is an instance of a * class that is not provided by the Android System. */ @@ -44,6 +46,8 @@ public final class Validators { /** * Creates a validator that is valid if any of the {@code validators} is valid. * + *

    Used to represent an {@code OR} boolean operation in a chain of validators. + * * @throws IllegalArgumentException if any element of {@code validators} is an instance of a * class that is not provided by the Android System. */ @@ -53,7 +57,9 @@ public final class Validators { } /** - * Creates a validator that is valid only if {@code validator} is not. + * Creates a validator that is valid when {@code validator} is not, and vice versa. + * + *

    Used to represent a {@code NOT} boolean operation in a chain of validators. * * @throws IllegalArgumentException if {@code validator} is an instance of a class that is not * provided by the Android System. diff --git a/android/service/carrier/CarrierService.java b/android/service/carrier/CarrierService.java index 2707f146..b94ccf9e 100644 --- a/android/service/carrier/CarrierService.java +++ b/android/service/carrier/CarrierService.java @@ -33,8 +33,8 @@ import com.android.internal.telephony.ITelephonyRegistry; * To extend this class, you must declare the service in your manifest file to require the * {@link android.Manifest.permission#BIND_CARRIER_SERVICES} permission and include an intent * filter with the {@link #CARRIER_SERVICE_INTERFACE}. If the service should have a long-lived - * binding, set android.service.carrier.LONG_LIVED_BINDING to true in the service's metadata. - * For example: + * binding, set android.service.carrier.LONG_LIVED_BINDING to true in the + * service's metadata. For example: *

    * *
    {@code
    diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java
    index df0842f7..fb530074 100644
    --- a/android/service/euicc/EuiccService.java
    +++ b/android/service/euicc/EuiccService.java
    @@ -23,6 +23,7 @@ import android.os.IBinder;
     import android.os.RemoteException;
     import android.telephony.euicc.DownloadableSubscription;
     import android.telephony.euicc.EuiccInfo;
    +import android.telephony.euicc.EuiccManager.OtaStatus;
     import android.util.ArraySet;
     
     import java.util.concurrent.LinkedBlockingQueue;
    @@ -202,6 +203,16 @@ public abstract class EuiccService extends Service {
         // TODO(b/36260308): Update doc when we have multi-SIM support.
         public abstract String onGetEid(int slotId);
     
    +    /**
    +     * Return the status of OTA update.
    +     *
    +     * @param slotId ID of the SIM slot to use for the operation. This is currently not populated
    +     *     but is here to future-proof the APIs.
    +     * @return The status of Euicc OTA update.
    +     * @see android.telephony.euicc.EuiccManager#getOtaStatus
    +     */
    +    public abstract @OtaStatus int onGetOtaStatus(int slotId);
    +
         /**
          * Populate {@link DownloadableSubscription} metadata for the given downloadable subscription.
          *
    @@ -384,6 +395,21 @@ public abstract class EuiccService extends Service {
                 });
             }
     
    +        @Override
    +        public void getOtaStatus(int slotId, IGetOtaStatusCallback callback) {
    +            mExecutor.execute(new Runnable() {
    +                @Override
    +                public void run() {
    +                    int status = EuiccService.this.onGetOtaStatus(slotId);
    +                    try {
    +                        callback.onSuccess(status);
    +                    } catch (RemoteException e) {
    +                        // Can't communicate with the phone process; ignore.
    +                    }
    +                }
    +            });
    +        }
    +
             @Override
             public void getDownloadableSubscriptionMetadata(int slotId,
                     DownloadableSubscription subscription,
    diff --git a/android/service/notification/Condition.java b/android/service/notification/Condition.java
    index 447afe62..2a352adc 100644
    --- a/android/service/notification/Condition.java
    +++ b/android/service/notification/Condition.java
    @@ -39,7 +39,12 @@ public final class Condition implements Parcelable {
         public static final String SCHEME = "condition";
     
         /** @hide */
    -    @IntDef({STATE_FALSE, STATE_TRUE, STATE_TRUE, STATE_ERROR})
    +    @IntDef(prefix = { "STATE_" }, value = {
    +            STATE_FALSE,
    +            STATE_TRUE,
    +            STATE_UNKNOWN,
    +            STATE_ERROR
    +    })
         @Retention(RetentionPolicy.SOURCE)
         public @interface State {}
     
    diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
    index dac663e7..18d4a1e6 100644
    --- a/android/service/notification/NotificationListenerService.java
    +++ b/android/service/notification/NotificationListenerService.java
    @@ -229,8 +229,11 @@ public abstract class NotificationListenerService extends Service {
     
     
         /** @hide */
    -    @IntDef({NOTIFICATION_CHANNEL_OR_GROUP_ADDED, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
    -            NOTIFICATION_CHANNEL_OR_GROUP_DELETED})
    +    @IntDef(prefix = { "NOTIFICATION_CHANNEL_OR_GROUP_" }, value = {
    +            NOTIFICATION_CHANNEL_OR_GROUP_ADDED,
    +            NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
    +            NOTIFICATION_CHANNEL_OR_GROUP_DELETED
    +    })
         @Retention(RetentionPolicy.SOURCE)
         public @interface ChannelOrGroupModificationTypes {}
     
    diff --git a/android/service/notification/ScheduleCalendar.java b/android/service/notification/ScheduleCalendar.java
    new file mode 100644
    index 00000000..8a7ff4da
    --- /dev/null
    +++ b/android/service/notification/ScheduleCalendar.java
    @@ -0,0 +1,177 @@
    +/*
    + * Copyright (c) 2017 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.service.notification;
    +
    +import android.service.notification.ZenModeConfig.ScheduleInfo;
    +import android.util.ArraySet;
    +import android.util.Log;
    +
    +import java.util.Calendar;
    +import java.util.Objects;
    +import java.util.TimeZone;
    +
    +/**
    + * @hide
    + */
    +public class ScheduleCalendar {
    +    public static final String TAG = "ScheduleCalendar";
    +    public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
    +    private final ArraySet mDays = new ArraySet();
    +    private final Calendar mCalendar = Calendar.getInstance();
    +
    +    private ScheduleInfo mSchedule;
    +
    +    @Override
    +    public String toString() {
    +        return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]";
    +    }
    +
    +    /**
    +     * @return true if schedule will exit on alarm, else false
    +     */
    +    public boolean exitAtAlarm() {
    +        return mSchedule.exitAtAlarm;
    +    }
    +
    +    /**
    +     * Sets schedule information
    +     */
    +    public void setSchedule(ScheduleInfo schedule) {
    +        if (Objects.equals(mSchedule, schedule)) return;
    +        mSchedule = schedule;
    +        updateDays();
    +    }
    +
    +    /**
    +     * Sets next alarm of the schedule if the saved next alarm has passed or is further
    +     * in the future than given nextAlarm
    +     * @param now current time in milliseconds
    +     * @param nextAlarm time of next alarm in milliseconds
    +     */
    +    public void maybeSetNextAlarm(long now, long nextAlarm) {
    +        if (mSchedule != null && mSchedule.exitAtAlarm) {
    +            // alarm canceled
    +            if (nextAlarm == 0) {
    +                mSchedule.nextAlarm = 0;
    +            }
    +            // only allow alarms in the future
    +            if (nextAlarm > now) {
    +                // store earliest alarm
    +                if (mSchedule.nextAlarm == 0) {
    +                    mSchedule.nextAlarm = nextAlarm;
    +                } else {
    +                    mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
    +                }
    +            } else if (mSchedule.nextAlarm < now) {
    +                if (DEBUG) {
    +                    Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm);
    +                }
    +                mSchedule.nextAlarm = 0;
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Set calendar time zone to tz
    +     * @param tz current time zone
    +     */
    +    public void setTimeZone(TimeZone tz) {
    +        mCalendar.setTimeZone(tz);
    +    }
    +
    +    /**
    +     * @param now current time in milliseconds
    +     * @return next time this rule changes (starts or ends)
    +     */
    +    public long getNextChangeTime(long now) {
    +        if (mSchedule == null) return 0;
    +        final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
    +        final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
    +        long nextScheduleTime = Math.min(nextStart, nextEnd);
    +
    +        return nextScheduleTime;
    +    }
    +
    +    private long getNextTime(long now, int hr, int min) {
    +        final long time = getTime(now, hr, min);
    +        return time <= now ? addDays(time, 1) : time;
    +    }
    +
    +    private long getTime(long millis, int hour, int min) {
    +        mCalendar.setTimeInMillis(millis);
    +        mCalendar.set(Calendar.HOUR_OF_DAY, hour);
    +        mCalendar.set(Calendar.MINUTE, min);
    +        mCalendar.set(Calendar.SECOND, 0);
    +        mCalendar.set(Calendar.MILLISECOND, 0);
    +        return mCalendar.getTimeInMillis();
    +    }
    +
    +    /**
    +     * @param time milliseconds since Epoch
    +     * @return true if time is within the schedule, else false
    +     */
    +    public boolean isInSchedule(long time) {
    +        if (mSchedule == null || mDays.size() == 0) return false;
    +        final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute);
    +        long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
    +        if (end <= start) {
    +            end = addDays(end, 1);
    +        }
    +        return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end);
    +    }
    +
    +    /**
    +     * @param time milliseconds since Epoch
    +     * @return true if should exit at time for next alarm, else false
    +     */
    +    public boolean shouldExitForAlarm(long time) {
    +        if (mSchedule == null) {
    +            return false;
    +        }
    +        return mSchedule.exitAtAlarm
    +                && mSchedule.nextAlarm != 0
    +                && time >= mSchedule.nextAlarm;
    +    }
    +
    +    private boolean isInSchedule(int daysOffset, long time, long start, long end) {
    +        final int n = Calendar.SATURDAY;
    +        final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
    +        start = addDays(start, daysOffset);
    +        end = addDays(end, daysOffset);
    +        return mDays.contains(day) && time >= start && time < end;
    +    }
    +
    +    private int getDayOfWeek(long time) {
    +        mCalendar.setTimeInMillis(time);
    +        return mCalendar.get(Calendar.DAY_OF_WEEK);
    +    }
    +
    +    private void updateDays() {
    +        mDays.clear();
    +        if (mSchedule != null && mSchedule.days != null) {
    +            for (int i = 0; i < mSchedule.days.length; i++) {
    +                mDays.add(mSchedule.days[i]);
    +            }
    +        }
    +    }
    +
    +    private long addDays(long time, int days) {
    +        mCalendar.setTimeInMillis(time);
    +        mCalendar.add(Calendar.DATE, days);
    +        return mCalendar.getTimeInMillis();
    +    }
    +}
    diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java
    index 735b8223..f658ae03 100644
    --- a/android/service/notification/ZenModeConfig.java
    +++ b/android/service/notification/ZenModeConfig.java
    @@ -46,8 +46,10 @@ import java.util.Arrays;
     import java.util.Calendar;
     import java.util.Date;
     import java.util.GregorianCalendar;
    +import java.util.List;
     import java.util.Locale;
     import java.util.Objects;
    +import java.util.TimeZone;
     import java.util.UUID;
     
     /**
    @@ -64,11 +66,13 @@ public class ZenModeConfig implements Parcelable {
         public static final int MAX_SOURCE = SOURCE_STAR;
         private static final int DEFAULT_SOURCE = SOURCE_CONTACT;
     
    +    public static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
    +    public static final String EVERY_NIGHT_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
    +    public static final List DEFAULT_RULE_IDS = Arrays.asList(EVERY_NIGHT_DEFAULT_RULE_ID,
    +            EVENTS_DEFAULT_RULE_ID);
    +
         public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
                 Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
    -    public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
    -            Calendar.WEDNESDAY, Calendar.THURSDAY };
    -    public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY };
     
         public static final int[] MINUTE_BUCKETS = generateMinuteBuckets();
         private static final int SECONDS_MS = 1000;
    @@ -529,6 +533,13 @@ public class ZenModeConfig implements Parcelable {
             rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
             rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER);
             rt.condition = readConditionXml(parser);
    +
    +        // all default rules and user created rules updated to zenMode important interruptions
    +        if (rt.zenMode != Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
    +                && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) {
    +            Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name);
    +            rt.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
    +        }
             return rt;
         }
     
    @@ -692,6 +703,20 @@ public class ZenModeConfig implements Parcelable {
                     suppressedVisualEffects);
         }
     
    +    /**
    +     * Creates scheduleCalendar from a condition id
    +     * @param conditionId
    +     * @return ScheduleCalendar with info populated with conditionId
    +     */
    +    public static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
    +        final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
    +        if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
    +        final ScheduleCalendar sc = new ScheduleCalendar();
    +        sc.setSchedule(schedule);
    +        sc.setTimeZone(TimeZone.getDefault());
    +        return sc;
    +    }
    +
         private static int sourceToPrioritySenders(int source, int def) {
             switch (source) {
                 case SOURCE_ANYONE: return Policy.PRIORITY_SENDERS_ANY;
    @@ -793,7 +818,10 @@ public class ZenModeConfig implements Parcelable {
                     Condition.FLAG_RELEVANT_NOW);
         }
     
    -    private static CharSequence getFormattedTime(Context context, long time, boolean isSameDay,
    +    /**
    +     * Creates readable time from time in milliseconds
    +     */
    +    public static CharSequence getFormattedTime(Context context, long time, boolean isSameDay,
                 int userHandle) {
             String skeleton = (!isSameDay ? "EEE " : "")
                     + (DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma");
    @@ -801,7 +829,10 @@ public class ZenModeConfig implements Parcelable {
             return DateFormat.format(pattern, time);
         }
     
    -    private static boolean isToday(long time) {
    +    /**
    +     * Determines whether a time in milliseconds is today or not
    +     */
    +    public static boolean isToday(long time) {
             GregorianCalendar now = new GregorianCalendar();
             GregorianCalendar endTime = new GregorianCalendar();
             endTime.setTimeInMillis(time);
    @@ -890,7 +921,17 @@ public class ZenModeConfig implements Parcelable {
         }
     
         public static boolean isValidScheduleConditionId(Uri conditionId) {
    -        return tryParseScheduleConditionId(conditionId) != null;
    +        ScheduleInfo info;
    +        try {
    +            info = tryParseScheduleConditionId(conditionId);
    +        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
    +            return false;
    +        }
    +
    +        if (info == null || info.days == null || info.days.length == 0) {
    +            return false;
    +        }
    +        return true;
         }
     
         public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
    @@ -1071,7 +1112,10 @@ public class ZenModeConfig implements Parcelable {
             return UUID.randomUUID().toString().replace("-", "");
         }
     
    -    private static String getOwnerCaption(Context context, String owner) {
    +    /**
    +     * Gets the name of the app associated with owner
    +     */
    +    public static String getOwnerCaption(Context context, String owner) {
             final PackageManager pm = context.getPackageManager();
             try {
                 final ApplicationInfo info = pm.getApplicationInfo(owner, 0);
    diff --git a/android/service/persistentdata/PersistentDataBlockManager.java b/android/service/persistentdata/PersistentDataBlockManager.java
    index 9332a5be..0bf68b73 100644
    --- a/android/service/persistentdata/PersistentDataBlockManager.java
    +++ b/android/service/persistentdata/PersistentDataBlockManager.java
    @@ -65,10 +65,10 @@ public class PersistentDataBlockManager {
          */
         public static final int FLASH_LOCK_LOCKED = 1;
     
    -    @IntDef({
    -        FLASH_LOCK_UNKNOWN,
    -        FLASH_LOCK_LOCKED,
    -        FLASH_LOCK_UNLOCKED,
    +    @IntDef(prefix = { "FLASH_LOCK_" }, value = {
    +            FLASH_LOCK_UNKNOWN,
    +            FLASH_LOCK_LOCKED,
    +            FLASH_LOCK_UNLOCKED,
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface FlashLockState {}
    diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java
    index cfeb7fce..11e1e674 100644
    --- a/android/service/settings/suggestions/Suggestion.java
    +++ b/android/service/settings/suggestions/Suggestion.java
    @@ -38,7 +38,7 @@ public final class Suggestion implements Parcelable {
         /**
          * @hide
          */
    -    @IntDef(flag = true, value = {
    +    @IntDef(flag = true, prefix = { "FLAG_" }, value = {
                 FLAG_HAS_BUTTON,
         })
         @Retention(RetentionPolicy.SOURCE)
    diff --git a/android/service/trust/TrustAgentService.java b/android/service/trust/TrustAgentService.java
    index 5ef934ed..4bade9f9 100644
    --- a/android/service/trust/TrustAgentService.java
    +++ b/android/service/trust/TrustAgentService.java
    @@ -114,11 +114,10 @@ public class TrustAgentService extends Service {
     
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(flag = true,
    -            value = {
    -                    FLAG_GRANT_TRUST_INITIATED_BY_USER,
    -                    FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
    -            })
    +    @IntDef(flag = true, prefix = { "FLAG_GRANT_TRUST_" }, value = {
    +            FLAG_GRANT_TRUST_INITIATED_BY_USER,
    +            FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
    +    })
         public @interface GrantTrustFlags {}
     
     
    @@ -138,11 +137,10 @@ public class TrustAgentService extends Service {
     
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(flag = true,
    -            value = {
    -                TOKEN_STATE_ACTIVE,
    -                TOKEN_STATE_INACTIVE,
    -            })
    +    @IntDef(flag = true, prefix = { "TOKEN_STATE_" }, value = {
    +            TOKEN_STATE_ACTIVE,
    +            TOKEN_STATE_INACTIVE,
    +    })
         public @interface TokenState {}
     
         private static final int MSG_UNLOCK_ATTEMPT = 1;
    diff --git a/android/service/voice/AlwaysOnHotwordDetector.java b/android/service/voice/AlwaysOnHotwordDetector.java
    index 9464a875..76d89ef0 100644
    --- a/android/service/voice/AlwaysOnHotwordDetector.java
    +++ b/android/service/voice/AlwaysOnHotwordDetector.java
    @@ -87,11 +87,11 @@ public class AlwaysOnHotwordDetector {
     
         // Keyphrase management actions. Used in getManageIntent() ----//
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(value = {
    -                MANAGE_ACTION_ENROLL,
    -                MANAGE_ACTION_RE_ENROLL,
    -                MANAGE_ACTION_UN_ENROLL
    -            })
    +    @IntDef(prefix = { "MANAGE_ACTION_" }, value = {
    +            MANAGE_ACTION_ENROLL,
    +            MANAGE_ACTION_RE_ENROLL,
    +            MANAGE_ACTION_UN_ENROLL
    +    })
         private @interface ManageActions {}
     
         /**
    @@ -116,12 +116,11 @@ public class AlwaysOnHotwordDetector {
         //-- Flags for startRecognition    ----//
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(flag = true,
    -            value = {
    -                RECOGNITION_FLAG_NONE,
    -                RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
    -                RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
    -            })
    +    @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, value = {
    +            RECOGNITION_FLAG_NONE,
    +            RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
    +            RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
    +    })
         public @interface RecognitionFlags {}
     
         /**
    @@ -150,11 +149,10 @@ public class AlwaysOnHotwordDetector {
     
         /** @hide */
         @Retention(RetentionPolicy.SOURCE)
    -    @IntDef(flag = true,
    -            value = {
    -                RECOGNITION_MODE_VOICE_TRIGGER,
    -                RECOGNITION_MODE_USER_IDENTIFICATION,
    -            })
    +    @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = {
    +            RECOGNITION_MODE_VOICE_TRIGGER,
    +            RECOGNITION_MODE_USER_IDENTIFICATION,
    +    })
         public @interface RecognitionModes {}
     
         /**
    diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
    index dd0ae339..595bfb7a 100644
    --- a/android/service/wallpaper/WallpaperService.java
    +++ b/android/service/wallpaper/WallpaperService.java
    @@ -42,6 +42,7 @@ import android.os.SystemClock;
     import android.util.Log;
     import android.util.MergedConfiguration;
     import android.view.Display;
    +import android.view.DisplayCutout;
     import android.view.Gravity;
     import android.view.IWindowSession;
     import android.view.InputChannel;
    @@ -101,6 +102,7 @@ public abstract class WallpaperService extends Service {
         private static final int DO_DETACH = 20;
         private static final int DO_SET_DESIRED_SIZE = 30;
         private static final int DO_SET_DISPLAY_PADDING = 40;
    +    private static final int DO_IN_AMBIENT_MODE = 50;
     
         private static final int MSG_UPDATE_SURFACE = 10000;
         private static final int MSG_VISIBILITY_CHANGED = 10010;
    @@ -176,6 +178,9 @@ public abstract class WallpaperService extends Service {
             final Rect mFinalSystemInsets = new Rect();
             final Rect mFinalStableInsets = new Rect();
             final Rect mBackdropFrame = new Rect();
    +        final DisplayCutout.ParcelableWrapper mDisplayCutout =
    +                new DisplayCutout.ParcelableWrapper();
    +        DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT;
             final MergedConfiguration mMergedConfiguration = new MergedConfiguration();
     
             final WindowManager.LayoutParams mLayout
    @@ -191,6 +196,7 @@ public abstract class WallpaperService extends Service {
             float mPendingYOffsetStep;
             boolean mPendingSync;
             MotionEvent mPendingMove;
    +        boolean mIsInAmbientMode;
     
             // Needed for throttling onComputeColors.
             private long mLastColorInvalidation;
    @@ -302,7 +308,8 @@ public abstract class WallpaperService extends Service {
                 public void resized(Rect frame, Rect overscanInsets, Rect contentInsets,
                         Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
                         MergedConfiguration mergedConfiguration, Rect backDropRect, boolean forceLayout,
    -                    boolean alwaysConsumeNavBar, int displayId) {
    +                    boolean alwaysConsumeNavBar, int displayId,
    +                    DisplayCutout.ParcelableWrapper displayCutout) {
                     Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED,
                             reportDraw ? 1 : 0, outsets);
                     mCaller.sendMessage(msg);
    @@ -426,6 +433,15 @@ public abstract class WallpaperService extends Service {
             public boolean isPreview() {
                 return mIWallpaperEngine.mIsPreview;
             }
    +
    +        /**
    +         * Returns true if this engine is running in ambient mode -- that is,
    +         * it is being shown in low power mode, in always on display.
    +         * @hide
    +         */
    +        public boolean isInAmbientMode() {
    +            return mIsInAmbientMode;
    +        }
             
             /**
              * Control whether this wallpaper will receive raw touch events
    @@ -543,6 +559,15 @@ public abstract class WallpaperService extends Service {
                 return null;
             }
     
    +        /**
    +         * Called when the device enters or exits ambient mode.
    +         *
    +         * @param inAmbientMode {@code true} if in ambient mode.
    +         * @hide
    +         */
    +        public void onAmbientModeChanged(boolean inAmbientMode) {
    +        }
    +
             /**
              * Called when an application has changed the desired virtual size of
              * the wallpaper.
    @@ -626,6 +651,16 @@ public abstract class WallpaperService extends Service {
                 return null;
             }
     
    +        /**
    +         * Sets internal engine state. Only for testing.
    +         * @param created {@code true} or {@code false}.
    +         * @hide
    +         */
    +        @VisibleForTesting
    +        public void setCreated(boolean created) {
    +            mCreated = created;
    +        }
    +
             protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
                 out.print(prefix); out.print("mInitializing="); out.print(mInitializing);
                         out.print(" mDestroyed="); out.println(mDestroyed);
    @@ -678,7 +713,8 @@ public abstract class WallpaperService extends Service {
                     }
                     Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
                     mCaller.sendMessage(msg);
    -            } else {event.recycle();
    +            } else {
    +                event.recycle();
                 }
             }
     
    @@ -750,7 +786,7 @@ public abstract class WallpaperService extends Service {
                             mInputChannel = new InputChannel();
                             if (mSession.addToDisplay(mWindow, mWindow.mSeq, mLayout, View.VISIBLE,
                                 Display.DEFAULT_DISPLAY, mContentInsets, mStableInsets, mOutsets,
    -                                mInputChannel) < 0) {
    +                                mDisplayCutout, mInputChannel) < 0) {
                                 Log.w(TAG, "Failed to add window while updating wallpaper surface.");
                                 return;
                             }
    @@ -776,7 +812,7 @@ public abstract class WallpaperService extends Service {
                             mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
                                 View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets,
                                 mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame,
    -                            mMergedConfiguration, mSurfaceHolder.mSurface);
    +                            mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface);
     
                         if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface
                                 + ", frame=" + mWinFrame);
    @@ -800,6 +836,8 @@ public abstract class WallpaperService extends Service {
                             mStableInsets.top += padding.top;
                             mStableInsets.right += padding.right;
                             mStableInsets.bottom += padding.bottom;
    +                        mDisplayCutout.set(mDisplayCutout.get().inset(-padding.left, -padding.top,
    +                                -padding.right, -padding.bottom));
                         }
     
                         if (mCurWidth != w) {
    @@ -819,6 +857,7 @@ public abstract class WallpaperService extends Service {
                         insetsChanged |= !mDispatchedContentInsets.equals(mContentInsets);
                         insetsChanged |= !mDispatchedStableInsets.equals(mStableInsets);
                         insetsChanged |= !mDispatchedOutsets.equals(mOutsets);
    +                    insetsChanged |= !mDispatchedDisplayCutout.equals(mDisplayCutout.get());
     
                         mSurfaceHolder.setSurfaceFrameSize(w, h);
                         mSurfaceHolder.mSurfaceLock.unlock();
    @@ -885,12 +924,13 @@ public abstract class WallpaperService extends Service {
                                 mDispatchedContentInsets.set(mContentInsets);
                                 mDispatchedStableInsets.set(mStableInsets);
                                 mDispatchedOutsets.set(mOutsets);
    +                            mDispatchedDisplayCutout = mDisplayCutout.get();
                                 mFinalSystemInsets.set(mDispatchedOverscanInsets);
                                 mFinalStableInsets.set(mDispatchedStableInsets);
                                 WindowInsets insets = new WindowInsets(mFinalSystemInsets,
                                         null, mFinalStableInsets,
                                         getResources().getConfiguration().isScreenRound(), false,
    -                                    null /* displayCutout */);
    +                                    mDispatchedDisplayCutout);
                                 if (DEBUG) {
                                     Log.v(TAG, "dispatching insets=" + insets);
                                 }
    @@ -977,6 +1017,26 @@ public abstract class WallpaperService extends Service {
                 updateSurface(false, false, false);
             }
     
    +        /**
    +         * Executes life cycle event and updates internal ambient mode state based on
    +         * message sent from handler.
    +         *
    +         * @param inAmbientMode True if in ambient mode.
    +         * @hide
    +         */
    +        @VisibleForTesting
    +        public void doAmbientModeChanged(boolean inAmbientMode) {
    +            if (!mDestroyed) {
    +                if (DEBUG) {
    +                    Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + "): " + this);
    +                }
    +                mIsInAmbientMode = inAmbientMode;
    +                if (mCreated) {
    +                    onAmbientModeChanged(inAmbientMode);
    +                }
    +            }
    +        }
    +
             void doDesiredSizeChanged(int desiredWidth, int desiredHeight) {
                 if (!mDestroyed) {
                     if (DEBUG) Log.v(TAG, "onDesiredSizeChanged("
    @@ -1217,6 +1277,12 @@ public abstract class WallpaperService extends Service {
                 mCaller.sendMessage(msg);
             }
     
    +        @Override
    +        public void setInAmbientMode(boolean inAmbientDisplay) throws RemoteException {
    +            Message msg = mCaller.obtainMessageI(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0);
    +            mCaller.sendMessage(msg);
    +        }
    +
             public void dispatchPointer(MotionEvent event) {
                 if (mEngine != null) {
                     mEngine.dispatchPointer(event);
    @@ -1254,6 +1320,7 @@ public abstract class WallpaperService extends Service {
                 mCaller.sendMessage(msg);
             }
     
    +        @Override
             public void executeMessage(Message message) {
                 switch (message.what) {
                     case DO_ATTACH: {
    @@ -1280,6 +1347,11 @@ public abstract class WallpaperService extends Service {
                     }
                     case DO_SET_DISPLAY_PADDING: {
                         mEngine.doDisplayPaddingChanged((Rect) message.obj);
    +                    return;
    +                }
    +                case DO_IN_AMBIENT_MODE: {
    +                    mEngine.doAmbientModeChanged(message.arg1 != 0);
    +                    return;
                     }
                     case MSG_UPDATE_SURFACE:
                         mEngine.updateSurface(true, false, false);
    diff --git a/android/speech/tts/TextToSpeech.java b/android/speech/tts/TextToSpeech.java
    index 763ea2ca..01562b32 100644
    --- a/android/speech/tts/TextToSpeech.java
    +++ b/android/speech/tts/TextToSpeech.java
    @@ -80,8 +80,15 @@ public class TextToSpeech {
         public static final int STOPPED = -2;
     
         /** @hide */
    -    @IntDef({ERROR_SYNTHESIS, ERROR_SERVICE, ERROR_OUTPUT, ERROR_NETWORK, ERROR_NETWORK_TIMEOUT,
    -             ERROR_INVALID_REQUEST, ERROR_NOT_INSTALLED_YET})
    +    @IntDef(prefix = { "ERROR_" }, value = {
    +            ERROR_SYNTHESIS,
    +            ERROR_SERVICE,
    +            ERROR_OUTPUT,
    +            ERROR_NETWORK,
    +            ERROR_NETWORK_TIMEOUT,
    +            ERROR_INVALID_REQUEST,
    +            ERROR_NOT_INSTALLED_YET
    +    })
         @Retention(RetentionPolicy.SOURCE)
         public @interface Error {}
     
    diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java
    index efa0cbae..6d8d6bf5 100644
    --- a/android/support/LibraryVersions.java
    +++ b/android/support/LibraryVersions.java
    @@ -36,14 +36,24 @@ public class LibraryVersions {
         public static final Version ROOM = FLATFOOT_1_0_BATCH;
     
         /**
    -     * Version code for Lifecycle extensions (live data, view model etc)
    +     * Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
          */
    -    public static final Version LIFECYCLES_EXT = FLATFOOT_1_0_BATCH;
    +    public static final Version LIFECYCLES_EXT = new Version("1.1.0-SNAPSHOT");
    +
    +    /**
    +     * Version code for Lifecycle LiveData
    +     */
    +    public static final Version LIFECYCLES_LIVEDATA = LIFECYCLES_EXT;
    +
    +    /**
    +     * Version code for Lifecycle ViewModel
    +     */
    +    public static final Version LIFECYCLES_VIEWMODEL = LIFECYCLES_EXT;
     
         /**
          * Version code for RecyclerView & Room paging
          */
    -    public static final Version PAGING = new Version("1.0.0-alpha3");
    +    public static final Version PAGING = new Version("1.0.0-alpha4-1");
     
         private static final Version LIFECYCLES = new Version("1.0.3");
     
    diff --git a/android/support/Version.java b/android/support/Version.java
    index 69b7f5e2..36c7728b 100644
    --- a/android/support/Version.java
    +++ b/android/support/Version.java
    @@ -16,6 +16,7 @@
     
     package android.support;
     
    +import java.io.File;
     import java.util.regex.Matcher;
     import java.util.regex.Pattern;
     
    @@ -23,17 +24,28 @@ import java.util.regex.Pattern;
      * Utility class which represents a version
      */
     public class Version implements Comparable {
    +    private static final Pattern VERSION_FILE_REGEX = Pattern.compile("^(\\d+\\.\\d+\\.\\d+).txt$");
    +    private static final Pattern VERSION_REGEX = Pattern
    +            .compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
    +
         private final int mMajor;
         private final int mMinor;
         private final int mPatch;
         private final String mExtra;
     
         public Version(String versionString) {
    -        Pattern compile = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
    -        Matcher matcher = compile.matcher(versionString);
    +        this(checkedMatcher(versionString));
    +    }
    +
    +    private static Matcher checkedMatcher(String versionString) {
    +        Matcher matcher = VERSION_REGEX.matcher(versionString);
             if (!matcher.matches()) {
                 throw new IllegalArgumentException("Can not parse version: " + versionString);
             }
    +        return matcher;
    +    }
    +
    +    private Version(Matcher matcher) {
             mMajor = Integer.parseInt(matcher.group(1));
             mMinor = Integer.parseInt(matcher.group(2));
             mPatch = Integer.parseInt(matcher.group(3));
    @@ -117,4 +129,29 @@ public class Version implements Comparable {
             result = 31 * result + (mExtra != null ? mExtra.hashCode() : 0);
             return result;
         }
    +
    +    /**
    +     * @return Version or null, if a name of the given file doesn't match
    +     */
    +    public static Version from(File file) {
    +        if (!file.isFile()) {
    +            return null;
    +        }
    +        Matcher matcher = VERSION_FILE_REGEX.matcher(file.getName());
    +        if (!matcher.matches()) {
    +            return null;
    +        }
    +        return new Version(matcher.group(1));
    +    }
    +
    +    /**
    +     * @return Version or null, if the given string doesn't match
    +     */
    +    public static Version from(String versionString) {
    +        Matcher matcher = VERSION_REGEX.matcher(versionString);
    +        if (!matcher.matches()) {
    +            return null;
    +        }
    +        return new Version(matcher);
    +    }
     }
    diff --git a/android/support/animation/AnimationHandler.java b/android/support/animation/AnimationHandler.java
    index 9f63bd15..6c39b23a 100644
    --- a/android/support/animation/AnimationHandler.java
    +++ b/android/support/animation/AnimationHandler.java
    @@ -16,11 +16,11 @@
     
     package android.support.animation;
     
    -import android.annotation.TargetApi;
     import android.os.Build;
     import android.os.Handler;
     import android.os.Looper;
     import android.os.SystemClock;
    +import android.support.annotation.RequiresApi;
     import android.support.v4.util.SimpleArrayMap;
     import android.view.Choreographer;
     
    @@ -191,7 +191,7 @@ class AnimationHandler {
         /**
          * Default provider of timing pulse that uses Choreographer for frame callbacks.
          */
    -    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    +    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
         private static class FrameCallbackProvider16 extends AnimationFrameCallbackProvider {
     
             private final Choreographer mChoreographer = Choreographer.getInstance();
    diff --git a/android/support/animation/FloatPropertyCompat.java b/android/support/animation/FloatPropertyCompat.java
    index cde340c8..ec8d0cac 100644
    --- a/android/support/animation/FloatPropertyCompat.java
    +++ b/android/support/animation/FloatPropertyCompat.java
    @@ -16,7 +16,7 @@
     
     package android.support.animation;
     
    -import android.annotation.TargetApi;
    +import android.support.annotation.RequiresApi;
     import android.util.FloatProperty;
     
     /**
    @@ -51,7 +51,7 @@ public abstract class FloatPropertyCompat {
          * @param  the class on which the Property is declared
          * @return a new {@link FloatPropertyCompat} wrapper for the given {@link FloatProperty} object
          */
    -    @TargetApi(24)
    +    @RequiresApi(24)
         public static  FloatPropertyCompat createFloatPropertyCompat(
                 final FloatProperty property) {
             return new FloatPropertyCompat(property.getName()) {
    diff --git a/android/support/annotation/IntDef.java b/android/support/annotation/IntDef.java
    index f621b7f4..99457264 100644
    --- a/android/support/annotation/IntDef.java
    +++ b/android/support/annotation/IntDef.java
    @@ -46,12 +46,14 @@ import java.lang.annotation.Target;
      *      flag = true,
      *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
      * 
    + * + * @see LongDef */ @Retention(SOURCE) @Target({ANNOTATION_TYPE}) public @interface IntDef { /** Defines the allowed constants for this element */ - long[] value() default {}; + int[] value() default {}; /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ boolean flag() default false; diff --git a/android/support/annotation/LongDef.java b/android/support/annotation/LongDef.java new file mode 100644 index 00000000..3dea338d --- /dev/null +++ b/android/support/annotation/LongDef.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.annotation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that the annotated long element represents + * a logical type and that its value should be one of the explicitly + * named constants. If the LongDef#flag() attribute is set to true, + * multiple constants can be combined. + *

    + * Example: + *

    
    + *  @Retention(SOURCE)
    + *  @LongDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
    + *  public @interface NavigationMode {}
    + *  public static final long NAVIGATION_MODE_STANDARD = 0;
    + *  public static final long NAVIGATION_MODE_LIST = 1;
    + *  public static final long NAVIGATION_MODE_TABS = 2;
    + *  ...
    + *  public abstract void setNavigationMode(@NavigationMode long mode);
    + *  @NavigationMode
    + *  public abstract long getNavigationMode();
    + * 
    + * For a flag, set the flag attribute: + *
    
    + *  @LongDef(
    + *      flag = true,
    + *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
    + * 
    + * + * @see IntDef + */ +@Retention(SOURCE) +@Target({ANNOTATION_TYPE}) +public @interface LongDef { + /** Defines the allowed constants for this element */ + long[] value() default {}; + + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} \ No newline at end of file diff --git a/android/support/car/drawer/CarDrawerActivity.java b/android/support/car/drawer/CarDrawerActivity.java deleted file mode 100644 index f46c652b..00000000 --- a/android/support/car/drawer/CarDrawerActivity.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.drawer; - -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.annotation.LayoutRes; -import android.support.annotation.Nullable; -import android.support.car.R; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -/** - * Common base Activity for car apps that need to present a Drawer. - * - *

    This Activity manages the overall layout. To use it, sub-classes need to: - * - *

      - *
    • Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}. - *
    • Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}. - * They can also add fragments to the main-content container by obtaining its id using - * {@link #getContentContainerId()} - *
    - * - *

    This class will take care of drawer toggling and display. - * - *

    The rootAdapter can implement nested-navigation, in its click-handling, by passing the - * CarDrawerAdapter for the next level to - * {@link CarDrawerController#pushAdapter(CarDrawerAdapter)}. - * - *

    Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a - * derivative. - */ -public abstract class CarDrawerActivity extends AppCompatActivity { - private CarDrawerController mDrawerController; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.car_drawer_activity); - - DrawerLayout drawerLayout = findViewById(R.id.drawer_layout); - ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle( - this /* activity */, - drawerLayout, /* DrawerLayout object */ - R.string.car_drawer_open, - R.string.car_drawer_close); - - Toolbar toolbar = findViewById(R.id.car_toolbar); - setSupportActionBar(toolbar); - - mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle); - mDrawerController.setRootAdapter(getRootAdapter()); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); - } - - /** - * Returns the {@link CarDrawerController} that is responsible for handling events relating - * to the drawer in this Activity. - * - * @return The {@link CarDrawerController} linked to this Activity. This value will be - * {@code null} if this method is called before {@code onCreate()} has been called. - */ - @Nullable - protected CarDrawerController getDrawerController() { - return mDrawerController; - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - mDrawerController.syncState(); - } - - /** - * @return Adapter for root content of the Drawer. - */ - protected abstract CarDrawerAdapter getRootAdapter(); - - /** - * Set main content to display in this Activity. It will be added to R.id.content_frame in - * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}. - * - * @param view View to display as main content. - */ - public void setMainContent(View view) { - ViewGroup parent = findViewById(getContentContainerId()); - parent.addView(view); - } - - /** - * Set main content to display in this Activity. It will be added to R.id.content_frame in - * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}. - * - * @param resourceId Layout to display as main content. - */ - public void setMainContent(@LayoutRes int resourceId) { - ViewGroup parent = findViewById(getContentContainerId()); - LayoutInflater inflater = getLayoutInflater(); - inflater.inflate(resourceId, parent, true); - } - - /** - * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own - * content/fragments inside here. - * - * @return Id of FrameLayout where main content of the subclass Activity can be added. - */ - protected int getContentContainerId() { - return R.id.content_frame; - } - - @Override - protected void onStop() { - super.onStop(); - mDrawerController.closeDrawer(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - mDrawerController.onConfigurationChanged(newConfig); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); - } -} diff --git a/android/support/car/drawer/CarDrawerAdapter.java b/android/support/car/drawer/CarDrawerAdapter.java deleted file mode 100644 index b0fd965d..00000000 --- a/android/support/car/drawer/CarDrawerAdapter.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.drawer; - -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.car.R; -import android.support.car.widget.PagedListView; -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -/** - * Base adapter for displaying items in the car navigation drawer, which uses a - * {@link PagedListView}. - * - *

    Subclasses must set the title that will be displayed when displaying the contents of the - * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The - * title of the root adapter will also be the main title showed in the toolbar when the drawer is - * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information. - * - *

    This class also takes care of implementing the PageListView.ItemCamp contract and subclasses - * should implement {@link #getActualItemCount()}. - */ -public abstract class CarDrawerAdapter extends RecyclerView.Adapter - implements PagedListView.ItemCap, DrawerItemClickListener { - private final boolean mShowDisabledListOnEmpty; - private final Drawable mEmptyListDrawable; - private int mMaxItems = PagedListView.ItemCap.UNLIMITED; - private CharSequence mTitle; - private TitleChangeListener mTitleChangeListener; - - /** - * Interface for a class that will be notified a new title has been set on this adapter. - */ - interface TitleChangeListener { - /** - * Called when {@link #setTitle(CharSequence)} has been called and the title has been - * changed. - */ - void onTitleChanged(CharSequence newTitle); - } - - protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) { - mShowDisabledListOnEmpty = showDisabledListOnEmpty; - - mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable); - mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint), - PorterDuff.Mode.SRC_IN); - } - - /** Returns the title set via {@link #setTitle(CharSequence)}. */ - CharSequence getTitle() { - return mTitle; - } - - /** Updates the title to display in the toolbar for this Adapter. */ - public final void setTitle(@NonNull CharSequence title) { - if (title == null) { - throw new IllegalArgumentException("setTitle() cannot be passed a null title!"); - } - - mTitle = title; - - if (mTitleChangeListener != null) { - mTitleChangeListener.onTitleChanged(mTitle); - } - } - - /** Sets a listener to be notified whenever the title of this adapter has been changed. */ - void setTitleChangeListener(@Nullable TitleChangeListener listener) { - mTitleChangeListener = listener; - } - - @Override - public final void setMaxItems(int maxItems) { - mMaxItems = maxItems; - } - - @Override - public final int getItemCount() { - if (shouldShowDisabledListItem()) { - return 1; - } - return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount(); - } - - /** - * Returns the absolute number of items that can be displayed in the list. - * - *

    A class should implement this method to supply the number of items to be displayed. - * Returning 0 from this method will cause an empty list icon to be displayed in the drawer. - * - *

    A class should override this method rather than {@link #getItemCount()} because that - * method is handling the logic of when to display the empty list icon. It will return 1 when - * {@link #getActualItemCount()} returns 0. - * - * @return The number of items to be displayed in the list. - */ - protected abstract int getActualItemCount(); - - @Override - public final int getItemViewType(int position) { - if (shouldShowDisabledListItem()) { - return R.layout.car_drawer_list_item_empty; - } - - return usesSmallLayout(position) - ? R.layout.car_drawer_list_item_small - : R.layout.car_drawer_list_item_normal; - } - - /** - * Used to indicate the layout used for the Drawer item at given position. Subclasses can - * override this to use normal layout which includes text element below title. - * - *

    A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}. - * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used. - * - * @param position Adapter position of item. - * @return Whether the item at this position will use a small layout (default) or normal layout. - */ - protected boolean usesSmallLayout(int position) { - return true; - } - - @Override - public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - return new DrawerItemViewHolder(view); - } - - @Override - public final void onBindViewHolder(DrawerItemViewHolder holder, int position) { - if (shouldShowDisabledListItem()) { - holder.getTitle().setText(null); - holder.getIcon().setImageDrawable(mEmptyListDrawable); - holder.setItemClickListener(null); - } else { - holder.setItemClickListener(this); - populateViewHolder(holder, position); - } - } - - /** - * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it - * has been configured to show and there are no items to be displayed. - */ - private boolean shouldShowDisabledListItem() { - return mShowDisabledListOnEmpty && getActualItemCount() == 0; - } - - /** - * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some - * element is not used, it should be nulled out since these ViewHolder/View's are recycled. - */ - protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position); - - /** - * Called when this adapter has been popped off the stack and is no longer needed. Subclasses - * can override to do any necessary cleanup. - */ - public void cleanup() {} -} diff --git a/android/support/car/drawer/CarDrawerController.java b/android/support/car/drawer/CarDrawerController.java deleted file mode 100644 index 7b23714c..00000000 --- a/android/support/car/drawer/CarDrawerController.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.drawer; - -import android.content.Context; -import android.content.res.Configuration; -import android.os.Bundle; -import android.support.annotation.AnimRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.car.R; -import android.support.car.widget.PagedListView; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; -import android.view.Gravity; -import android.view.MenuItem; -import android.view.View; -import android.view.animation.AnimationUtils; -import android.widget.ProgressBar; - -import java.util.Stack; - -/** - * A controller that will handle the set up of the navigation drawer. It will hook up the - * necessary buttons for up navigation, as well as expose methods to allow for a drill down - * navigation. - */ -public class CarDrawerController { - /** An animation for when a user navigates into a submenu. */ - @AnimRes - private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim; - - /** An animation for when a user navigates up (when the back button is pressed). */ - @AnimRes - private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim; - - /** The amount that the drawer has been opened before its color should be switched. */ - private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f; - - /** - * A representation of the hierarchy of navigation being displayed in the list. The ordering of - * this stack is the order that the user has visited each level. When the user navigates up, - * the adapters are popped from this list. - */ - private final Stack mAdapterStack = new Stack<>(); - - private final Context mContext; - - private final Toolbar mToolbar; - private final DrawerLayout mDrawerLayout; - private final ActionBarDrawerToggle mDrawerToggle; - - private final PagedListView mDrawerList; - private final ProgressBar mProgressBar; - private final View mDrawerContent; - - /** - * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by - * {@code drawerLayout}. - * - *

    The given {@code drawerLayout} should either have a child View that is inflated from - * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that - * layout. - * - * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity. - * @param drawerLayout The top-level container for the window content that shows the - * interactive drawer. - * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar} - * and {@code drawerLayout}. - */ - public CarDrawerController(Toolbar toolbar, - DrawerLayout drawerLayout, - ActionBarDrawerToggle drawerToggle) { - mToolbar = toolbar; - mContext = drawerLayout.getContext(); - mDrawerToggle = drawerToggle; - mDrawerLayout = drawerLayout; - - mDrawerContent = drawerLayout.findViewById(R.id.drawer_content); - mDrawerList = drawerLayout.findViewById(R.id.drawer_list); - mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); - mProgressBar = drawerLayout.findViewById(R.id.drawer_progress); - - setupDrawerToggling(); - } - - /** - * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of - * this root adapter are shown when the drawer is first opened. It is also the top-most level of - * navigation in the drawer. - * - * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then - * this method will do nothing. - */ - public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) { - if (rootAdapter == null) { - return; - } - - // The root adapter is always the last item in the stack. - if (mAdapterStack.size() > 0) { - mAdapterStack.set(0, rootAdapter); - } else { - mAdapterStack.push(rootAdapter); - } - - setToolbarTitleFrom(rootAdapter); - mDrawerList.setAdapter(rootAdapter); - } - - /** - * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display - * in the navigation drawer. The title will also be updated from the adapter. - * - *

    This switch is treated as a navigation to the next level in the drawer. Navigation away - * from this level will pop the given adapter off and surface contents of the previous adapter - * that was set via this method. If no such adapter exists, then the root adapter set by - * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead. - * - * @param adapter Adapter for next level of content in the drawer. - */ - public final void pushAdapter(CarDrawerAdapter adapter) { - mAdapterStack.peek().setTitleChangeListener(null); - mAdapterStack.push(adapter); - setDisplayAdapter(adapter); - runLayoutAnimation(DRILL_DOWN_ANIM); - } - - /** Close the drawer. */ - public void closeDrawer() { - if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { - mDrawerLayout.closeDrawer(Gravity.LEFT); - } - } - - /** Opens the drawer. */ - public void openDrawer() { - if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { - mDrawerLayout.openDrawer(Gravity.LEFT); - } - } - - /** Sets a listener to be notified of Drawer events. */ - public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { - mDrawerLayout.addDrawerListener(listener); - } - - /** Removes a listener to be notified of Drawer events. */ - public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { - mDrawerLayout.removeDrawerListener(listener); - } - - /** - * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true}, - * the progress bar is displayed and the navigation list is hidden and vice versa. - */ - public void showLoadingProgressBar(boolean show) { - mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE); - mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE); - } - - /** Scroll to given position in the list. */ - public void scrollToPosition(int position) { - mDrawerList.getRecyclerView().smoothScrollToPosition(position); - } - - /** - * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this - * controller's internal Toolbar. - */ - private void setToolbarTitleFrom(CarDrawerAdapter adapter) { - if (adapter.getTitle() == null) { - throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()"); - } - - mToolbar.setTitle(adapter.getTitle()); - adapter.setTitleChangeListener(mToolbar::setTitle); - } - - /** - * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer - * hierarchy is properly displayed. - */ - private void setupDrawerToggling() { - mDrawerLayout.addDrawerListener(mDrawerToggle); - mDrawerLayout.addDrawerListener( - new DrawerLayout.DrawerListener() { - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - // Correctly set the title and arrow colors as they are different between - // the open and close states. - updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET); - } - - @Override - public void onDrawerClosed(View drawerView) { - // If drawer is closed, revert stack/drawer to initial root state. - cleanupStackAndShowRoot(); - scrollToPosition(0); - } - - @Override - public void onDrawerOpened(View drawerView) {} - - @Override - public void onDrawerStateChanged(int newState) {} - }); - } - - /** Sets the title and arrow color of the drawer depending on if it is open or not. */ - private void updateTitleAndArrowColor(boolean drawerOpen) { - // When the drawer is open, use car_title, which resolves to appropriate color depending on - // day-night mode. When drawer is closed, we always use light color. - int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light; - int titleColor = mContext.getColor(titleColorResId); - mToolbar.setTitleTextColor(titleColor); - mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor); - } - - /** - * Synchronizes the display of the drawer with its linked {@link DrawerLayout}. - * - *

    This should be called from the associated Activity's - * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize - * after teh DRawerLayout's instance state has been restored, and any other time when the - * state may have diverged in such a way that this controller's associated - * {@link ActionBarDrawerToggle} had not been notified. - */ - public void syncState() { - mDrawerToggle.syncState(); - - // In case we're restarting after a config change (e.g. day, night switch), set colors - // again. Doing it here so that Drawer state is fully synced and we know if its open or not. - // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout. - updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent)); - } - - /** - * Notify this controller that device configurations may have changed. - * - *

    This method should be called from the associated Activity's - * {@code onConfigurationChanged()} method. - */ - public void onConfigurationChanged(Configuration newConfig) { - // Pass any configuration change to the drawer toggle. - mDrawerToggle.onConfigurationChanged(newConfig); - } - - /** - * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called - * when the Activity's method is called and will return {@code true} if the selection has - * been handled. - * - * @return {@code true} if the item processing was handled by this class. - */ - public boolean onOptionsItemSelected(MenuItem item) { - // Handle home-click and see if we can navigate up in the drawer. - if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { - return true; - } - - // DrawerToggle gets next chance to handle up-clicks (and any other clicks). - return mDrawerToggle.onOptionsItemSelected(item); - } - - /** - * Sets the given adapter as the one displaying the current contents of the drawer. - * - *

    The drawer's title will also be derived from the given adapter. - */ - private void setDisplayAdapter(CarDrawerAdapter adapter) { - setToolbarTitleFrom(adapter); - // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between - // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts. - mDrawerList.getRecyclerView().setAdapter(adapter); - } - - /** - * Switches to the previous level in the drawer hierarchy if the current list being displayed - * is not the root adapter. This is analogous to a navigate up. - * - * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise. - */ - private boolean maybeHandleUpClick() { - // Check if already at the root level. - if (mAdapterStack.size() <= 1) { - return false; - } - - CarDrawerAdapter adapter = mAdapterStack.pop(); - adapter.setTitleChangeListener(null); - adapter.cleanup(); - setDisplayAdapter(mAdapterStack.peek()); - runLayoutAnimation(NAVIGATE_UP_ANIM); - return true; - } - - /** Clears stack down to root adapter and switches to root adapter. */ - private void cleanupStackAndShowRoot() { - while (mAdapterStack.size() > 1) { - CarDrawerAdapter adapter = mAdapterStack.pop(); - adapter.setTitleChangeListener(null); - adapter.cleanup(); - } - setDisplayAdapter(mAdapterStack.peek()); - runLayoutAnimation(NAVIGATE_UP_ANIM); - } - - /** - * Runs the given layout animation on the PagedListView. Running this animation will also - * refresh the contents of the list. - */ - private void runLayoutAnimation(@AnimRes int animation) { - RecyclerView recyclerView = mDrawerList.getRecyclerView(); - recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation)); - recyclerView.getAdapter().notifyDataSetChanged(); - recyclerView.scheduleLayoutAnimation(); - } -} diff --git a/android/support/car/drawer/DrawerItemClickListener.java b/android/support/car/drawer/DrawerItemClickListener.java deleted file mode 100644 index d707dbd0..00000000 --- a/android/support/car/drawer/DrawerItemClickListener.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.drawer; - -/** - * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}. - */ -public interface DrawerItemClickListener { - /** - * Callback when item is clicked. - * - * @param position Adapter position of the clicked item. - */ - void onItemClick(int position); -} diff --git a/android/support/car/drawer/DrawerItemViewHolder.java b/android/support/car/drawer/DrawerItemViewHolder.java deleted file mode 100644 index d016b2de..00000000 --- a/android/support/car/drawer/DrawerItemViewHolder.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.drawer; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.car.R; -import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -/** - * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the - * {@link android.support.car.drawer.CarDrawerAdapter}. - */ -public class DrawerItemViewHolder extends RecyclerView.ViewHolder { - private final ImageView mIcon; - private final TextView mTitle; - private final TextView mText; - private final ImageView mEndIcon; - - DrawerItemViewHolder(View view) { - super(view); - mIcon = view.findViewById(R.id.icon); - if (mIcon == null) { - throw new IllegalArgumentException("Icon view cannot be null!"); - } - - mTitle = view.findViewById(R.id.title); - if (mTitle == null) { - throw new IllegalArgumentException("Title view cannot be null!"); - } - - // Next two are optional and may be null. - mText = view.findViewById(R.id.text); - mEndIcon = view.findViewById(R.id.end_icon); - } - - /** Returns the view that should be used to display the main icon. */ - @NonNull - public ImageView getIcon() { - return mIcon; - } - - /** Returns the view that will display the main title. */ - @NonNull - public TextView getTitle() { - return mTitle; - } - - /** Returns the view that is used for text that is smaller than the title text. */ - @Nullable - public TextView getText() { - return mText; - } - - /** Returns the icon that is displayed at the end of the view. */ - @Nullable - public ImageView getEndIcon() { - return mEndIcon; - } - - /** - * Sets the listener that will be notified when the view held by this ViewHolder has been - * clicked. Passing {@code null} will clear any previously set listeners. - */ - void setItemClickListener(@Nullable DrawerItemClickListener listener) { - itemView.setOnClickListener(listener != null - ? v -> listener.onItemClick(getAdapterPosition()) - : null); - } -} diff --git a/android/support/car/utils/ColumnCalculator.java b/android/support/car/utils/ColumnCalculator.java deleted file mode 100644 index fa5dd432..00000000 --- a/android/support/car/utils/ColumnCalculator.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.utils; - -import android.content.Context; -import android.content.res.Resources; -import android.support.car.R; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.WindowManager; - -/** - * Utility class that calculates the size of the columns that will fit on the screen. A column's - * width is determined by the size of the margins and gutters (space between the columns) that fit - * on-screen. - * - *

    Refer to the appropriate dimens and integers for the size of the margins and number of - * columns. - */ -public class ColumnCalculator { - private static final String TAG = "ColumnCalculator"; - - private static ColumnCalculator sInstance; - private static int sScreenWidth; - - private int mNumOfColumns; - private int mNumOfGutters; - private int mColumnWidth; - private int mGutterSize; - - /** - * Gets an instance of the {@link ColumnCalculator}. If this is the first time that this - * method has been called, then the given {@link Context} will be used to retrieve resources. - * - * @param context The current calling Context. - * @return An instance of {@link ColumnCalculator}. - */ - public static ColumnCalculator getInstance(Context context) { - if (sInstance == null) { - WindowManager windowManager = (WindowManager) context.getSystemService( - Context.WINDOW_SERVICE); - DisplayMetrics displayMetrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(displayMetrics); - sScreenWidth = displayMetrics.widthPixels; - - sInstance = new ColumnCalculator(context); - } - - return sInstance; - } - - private ColumnCalculator(Context context) { - Resources res = context.getResources(); - int marginSize = res.getDimensionPixelSize(R.dimen.car_margin); - mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size); - mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d", - marginSize, mNumOfColumns, mGutterSize)); - } - - // The gutters appear between each column. As a result, the number of gutters is one less - // than the number of columns. - mNumOfGutters = mNumOfColumns - 1; - - // Determine the spacing that is allowed to be filled by the columns by subtracting margins - // on both size of the screen and the space taken up by the gutters. - int spaceForColumns = sScreenWidth - (2 * marginSize) - (mNumOfGutters * mGutterSize); - - mColumnWidth = spaceForColumns / mNumOfColumns; - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "mColumnWidth: " + mColumnWidth); - } - } - - /** - * Returns the total number of columns that fit on the current screen. - * - * @return The total number of columns that fit on the screen. - */ - public int getNumOfColumns() { - return mNumOfColumns; - } - - /** - * Returns the size in pixels of each column. The column width is determined by the size of the - * screen divided by the number of columns, size of gutters and margins. - * - * @return The width of a single column in pixels. - */ - public int getColumnWidth() { - return mColumnWidth; - } - - /** - * Returns the total number of gutters that fit on screen. A gutter is the space between each - * column. This value is always one less than the number of columns. - * - * @return The number of gutters on screen. - */ - public int getNumOfGutters() { - return mNumOfGutters; - } - - /** - * Returns the size of each gutter in pixels. A gutter is the space between each column. - * - * @return The size of a single gutter in pixels. - */ - public int getGutterSize() { - return mGutterSize; - } - - /** - * Returns the size in pixels for the given number of columns. This value takes into account - * the size of the gutter between the columns as well. For example, for a column span of four, - * the size returned is the sum of four columns and three gutters. - * - * @return The size in pixels for a given column span. - */ - public int getSizeForColumnSpan(int columnSpan) { - int gutterSpan = columnSpan - 1; - return columnSpan * mColumnWidth + gutterSpan * mGutterSize; - } -} diff --git a/android/support/car/widget/CarItemAnimator.java b/android/support/car/widget/CarItemAnimator.java deleted file mode 100644 index ef22c484..00000000 --- a/android/support/car/widget/CarItemAnimator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.support.v7.widget.DefaultItemAnimator; -import android.support.v7.widget.RecyclerView; - -/** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */ -public class CarItemAnimator extends DefaultItemAnimator { - - private final PagedLayoutManager mLayoutManager; - - public CarItemAnimator(PagedLayoutManager layoutManager) { - mLayoutManager = layoutManager; - } - - @Override - public boolean animateChange(RecyclerView.ViewHolder oldHolder, - RecyclerView.ViewHolder newHolder, - int fromX, - int fromY, - int toX, - int toY) { - // The default behavior will cross fade the old view and the new one. However, if we - // have a card on a colored background, it will make it appear as if a changing card - // fades in and out. - float alpha = 0f; - if (newHolder != null) { - alpha = newHolder.itemView.getAlpha(); - } - boolean ret = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY); - if (newHolder != null) { - newHolder.itemView.setAlpha(alpha); - } - return ret; - } - - @Override - public void onMoveFinished(RecyclerView.ViewHolder item) { - // The item animator uses translation heavily internally. However, we also use translation - // to create the paging affect. When an item's move is animated, it will mess up the - // translation we have set on it so we must re-offset the rows once the animations finish. - - // isRunning(ItemAnimationFinishedListener) is the awkward API used to determine when all - // animations have finished. - isRunning(mFinishedListener); - } - - private final ItemAnimatorFinishedListener mFinishedListener = - new ItemAnimatorFinishedListener() { - @Override - public void onAnimationsFinished() { - mLayoutManager.offsetRows(); - } - }; -} diff --git a/android/support/car/widget/CarRecyclerView.java b/android/support/car/widget/CarRecyclerView.java deleted file mode 100644 index bb9cb71a..00000000 --- a/android/support/car/widget/CarRecyclerView.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -/** - * Custom {@link RecyclerView} that helps {@link PagedLayoutManager} properly fling and paginate. - * - *

    It also has the ability to fade children as they scroll off screen that can be set with {@link - * #setFadeLastItem(boolean)}. - */ -public class CarRecyclerView extends RecyclerView { - private boolean mFadeLastItem; - /** - * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be - * called. However, we want to make sure that the list still snaps to the next page when this - * happens. - */ - private boolean mWasFlingCalledForGesture; - - public CarRecyclerView(Context context) { - this(context, null); - } - - public CarRecyclerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - setFocusableInTouchMode(false); - setFocusable(false); - } - - @Override - public boolean fling(int velocityX, int velocityY) { - mWasFlingCalledForGesture = true; - return ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY); - } - - @Override - public boolean onTouchEvent(MotionEvent e) { - // We want the parent to handle all touch events. There's a lot going on there, - // and there is no reason to overwrite that functionality. If we do, bad things will happen. - final boolean ret = super.onTouchEvent(e); - - int action = e.getActionMasked(); - if (action == MotionEvent.ACTION_UP) { - if (!mWasFlingCalledForGesture) { - ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, 0); - } - mWasFlingCalledForGesture = false; - } - - return ret; - } - - @Override - public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) { - if (mFadeLastItem) { - float onScreen = 1f; - if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) { - onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight(); - } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) { - onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight(); - } - float alpha = 1 - (1 - onScreen) * (1 - onScreen); - fadeChild(child, alpha); - } - - return super.drawChild(canvas, child, drawingTime); - } - - public void setFadeLastItem(boolean fadeLastItem) { - mFadeLastItem = fadeLastItem; - } - - /** - * Scrolls the contents of this {@link CarRecyclerView} up one page. A page is defined as the - * number of items that fit completely on the screen. - */ - public void pageUp() { - PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager(); - int pageUpPosition = lm.getPageUpPosition(); - if (pageUpPosition == -1) { - return; - } - - smoothScrollToPosition(pageUpPosition); - } - - /** - * Scrolls the contents of this {@link CarRecyclerView} down one page. A page is defined as the - * number of items that fit completely on the screen. - */ - public void pageDown() { - PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager(); - int pageDownPosition = lm.getPageDownPosition(); - if (pageDownPosition == -1) { - return; - } - - smoothScrollToPosition(pageDownPosition); - } - - /** - * Fades child by alpha. If child is a {@link ViewGroup} then it will recursively fade its - * children instead. - */ - private void fadeChild(@NonNull View child, float alpha) { - if (child instanceof ViewGroup) { - ViewGroup vg = (ViewGroup) child; - for (int i = 0; i < vg.getChildCount(); i++) { - fadeChild(vg.getChildAt(i), alpha); - } - } else { - child.setAlpha(alpha); - } - } -} diff --git a/android/support/car/widget/ColumnCardView.java b/android/support/car/widget/ColumnCardView.java deleted file mode 100644 index 06f85536..00000000 --- a/android/support/car/widget/ColumnCardView.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.car.R; -import android.support.car.utils.ColumnCalculator; -import android.support.v7.widget.CardView; -import android.util.AttributeSet; -import android.util.Log; - -/** - * A {@link CardView} whose width can be specified by the number of columns that it will span. - * - *

    The {@code ColumnCardView} works similarly to a regular {@link CardView}, except that - * its {@code layout_width} attribute is always ignored. Instead, its width is automatically - * calculated based on a specified {@code columnSpan} attribute. Alternatively, a user can call - * {@link #setColumnSpan(int)}. If no column span is given, the {@code ColumnCardView} will have - * a default span value that it uses. - * - *

    - * <android.support.car.widget.ColumnCardView
    - *     android:layout_width="wrap_content"
    - *     android:layout_height="wrap_content"
    - *     app:columnSpan="4" />
    - * 
    - * - * @see ColumnCalculator - */ -public final class ColumnCardView extends CardView { - private static final String TAG = "ColumnCardView"; - - private ColumnCalculator mColumnCalculator; - private int mColumnSpan; - - public ColumnCardView(Context context) { - super(context); - init(context, null, 0 /* defStyleAttrs */); - } - - public ColumnCardView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0 /* defStyleAttrs */); - } - - public ColumnCardView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr); - } - - private void init(Context context, AttributeSet attrs, int defStyleAttrs) { - mColumnCalculator = ColumnCalculator.getInstance(context); - - int defaultColumnSpan = getResources().getInteger( - R.integer.column_card_default_column_span); - - TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColumnCardView, - defStyleAttrs, 0 /* defStyleRes */); - mColumnSpan = ta.getInteger(R.styleable.ColumnCardView_columnSpan, defaultColumnSpan); - ta.recycle(); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Column span: " + mColumnSpan); - } - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // Override any specified width so that the width is one that is calculated based on - // column and gutter span. - int width = mColumnCalculator.getSizeForColumnSpan(mColumnSpan); - super.onMeasure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - heightMeasureSpec); - } - - /** - * Sets the number of columns that this {@code ColumnCardView} will span. The given span is - * ignored if it is less than 0 or greater than the number of columns that fit on screen. - * - * @param columnSpan The number of columns this {@code ColumnCardView} will span across. - */ - public void setColumnSpan(int columnSpan) { - if (columnSpan <= 0 || columnSpan > mColumnCalculator.getNumOfColumns()) { - return; - } - - mColumnSpan = columnSpan; - requestLayout(); - } - - /** - * Returns the currently number of columns that this {@code ColumnCardView} spans. - * - * @return The number of columns this {@code ColumnCardView} spans across. - */ - public int getColumnSpan() { - return mColumnSpan; - } -} diff --git a/android/support/car/widget/DayNightStyle.java b/android/support/car/widget/DayNightStyle.java deleted file mode 100644 index ff5a1b33..00000000 --- a/android/support/car/widget/DayNightStyle.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.support.annotation.IntDef; - -/** - * Specifies how the system UI should respond to day/night mode events. - * - *

    By default, the Android Auto system UI assumes the app content background is light during the - * day and dark during the night. The system UI updates the foreground color (such as status bar - * icon colors) to be dark during day mode and light during night mode. By setting the - * DayNightStyle, the app can specify how the system should respond to a day/night mode event. For - * example, if the app has a dark content background for both day and night time, the app can tell - * the system to use {@link #FORCE_NIGHT} style so the foreground color is locked to light color for - * both cases. - * - *

    Note: Not all system UI elements can be customized with a DayNightStyle. - */ -@IntDef({ - DayNightStyle.AUTO, - DayNightStyle.AUTO_INVERSE, - DayNightStyle.FORCE_NIGHT, - DayNightStyle.FORCE_DAY, -}) -public @interface DayNightStyle { - /** - * Sets the foreground color to be automatically changed based on day/night mode, assuming the - * app content background is light during the day and dark during the night. - * - *

    This is the default behavior. - */ - int AUTO = 0; - - /** - * Sets the foreground color to be automatically changed based on day/night mode, assuming the - * app content background is dark during the day and light during the night. - */ - int AUTO_INVERSE = 1; - - /** - * Sets the foreground color to be locked to the night version, which assumes the app content - * background is always dark during both day and night. - */ - int FORCE_NIGHT = 2; - - /** - * Sets the foreground color to be locked to the day version, which assumes the app content - * background is always light during both day and night. - */ - int FORCE_DAY = 3; -} diff --git a/android/support/car/widget/PagedLayoutManager.java b/android/support/car/widget/PagedLayoutManager.java deleted file mode 100644 index c4f469a3..00000000 --- a/android/support/car/widget/PagedLayoutManager.java +++ /dev/null @@ -1,1687 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.content.Context; -import android.graphics.PointF; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.support.car.R; -import android.support.v7.widget.LinearSmoothScroller; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.Recycler; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.LruCache; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; -import android.view.animation.Transformation; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; - -/** - * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that - * it has a few tricks up its sleeve. - * - *

      - *
    1. In a normal ListView, when views reach the top of the list, they are clipped. In - * PagedLayoutManager, views have the option of flying off of the top of the screen as the - * next row settles in to place. This functionality can be enabled or disabled with - * {@link #setOffsetRows(boolean)}. - *
    2. Standard list physics is disabled. Instead, when the user scrolls, it will settle on the - * next page. - *
    3. Items can scroll past the bottom edge of the screen. This helps with pagination so that the - * last page can be properly aligned. - *
    - * - * This LayoutManger should be used with {@link CarRecyclerView}. - */ -public class PagedLayoutManager extends RecyclerView.LayoutManager { - private static final String TAG = "PagedLayoutManager"; - - /** - * Any fling below the threshold will just scroll to the top fully visible row. The units is - * whatever {@link android.widget.Scroller} would return. - * - *

    A reasonable value is ~200 - * - *

    This can be disabled by setting the threshold to -1. - */ - private static final int FLING_THRESHOLD_TO_PAGINATE = -1; - - /** - * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row. - * - *

    A reasonable value is 15. - * - *

    This can be disabled by setting the distance to -1. - */ - private static final int DRAG_DISTANCE_TO_PAGINATE = -1; - - /** - * If you scroll really quickly, you can hit the end of the laid out rows before Android has a - * chance to layout more. To help counter this, we can layout a number of extra rows past - * wherever the focus is if necessary. - */ - private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2; - - /** - * Scroll bar calculation is a bit complicated. This basically defines the granularity we want - * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement. - * Setting it too big will risk an overflow (although there is no performance impact). Ideally - * we want to set this higher than the height of our list view. We can't use our list view - * height directly though because we might run into situations where getHeight() returns 0, - * for example, when the view is not yet measured. - */ - private static final int SCROLL_RANGE = 1000; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({BEFORE, AFTER}) - private @interface LayoutDirection {} - - private static final int BEFORE = 0; - private static final int AFTER = 1; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE}) - public @interface RowOffsetMode {} - - public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0; - public static final int ROW_OFFSET_MODE_PAGE = 1; - - private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2); - private final Context mContext; - - /** Determines whether or not rows will be offset as they slide off screen * */ - private boolean mOffsetRows; - - /** Determines whether rows will be offset individually or a page at a time * */ - @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE; - - /** - * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the - * scroll state to be used anywhere. - */ - private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; - - /** Used to inspect the current scroll state to help with the various calculations. */ - private CarSmoothScroller mSmoothScroller; - - private PagedListView.OnScrollListener mOnScrollListener; - - /** The distance that the list has actually scrolled in the most recent drag gesture. */ - private int mLastDragDistance = 0; - - /** {@code True} if the current drag was limited/capped because it was at some boundary. */ - private boolean mReachedLimitOfDrag; - - /** The index of the first item on the current page. */ - private int mAnchorPageBreakPosition = 0; - - /** The index of the first item on the previous page. */ - private int mUpperPageBreakPosition = -1; - - /** The index of the first item on the next page. */ - private int mLowerPageBreakPosition = -1; - - /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. */ - private int mLastChildPositionToRequestFocus = -1; - - private int mSampleViewHeight = -1; - - /** Used for onPageUp and onPageDown */ - private int mViewsPerPage = 1; - - private int mCurrentPage = 0; - - private static final int MAX_ANIMATIONS_IN_CACHE = 30; - /** - * Cache of TranslateAnimation per child view. These are needed since using a single animation - * for all children doesn't apply the animation effect multiple times. Key = the view the - * animation will transform. - */ - private LruCache mFlyOffscreenAnimations; - - /** Set the anchor to the following position on the next layout pass. */ - private int mPendingScrollPosition = -1; - - public PagedLayoutManager(Context context) { - mContext = context; - } - - @Override - public RecyclerView.LayoutParams generateDefaultLayoutParams() { - return new RecyclerView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - } - - @Override - public boolean canScrollVertically() { - return true; - } - - /** - * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should: - * - *

      - *
    1. Check the current views to get the current state of affairs - *
    2. Detach all views from the window (a lightweight operation) so that rows not re-added - * will be removed after onLayoutChildren. - *
    3. Re-add rows as necessary. - *
    - * - * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) - */ - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - /* - * The anchor view is the first fully visible view on screen at the beginning of - * onLayoutChildren (or 0 if there is none). This row will be laid out first. After that, - * layoutNextRow will layout rows above and below it until the boundaries of what should be - * laid out have been reached. See shouldLayoutNextRow(View, int) for more info. - */ - int anchorPosition = 0; - int anchorTop = -1; - if (mPendingScrollPosition == -1) { - View anchor = getFirstFullyVisibleChild(); - if (anchor != null) { - anchorPosition = getPosition(anchor); - anchorTop = getDecoratedTop(anchor); - } - } else { - anchorPosition = mPendingScrollPosition; - mPendingScrollPosition = -1; - mAnchorPageBreakPosition = anchorPosition; - mUpperPageBreakPosition = -1; - mLowerPageBreakPosition = -1; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v( - TAG, - String.format( - ":: onLayoutChildren anchorPosition:%s, anchorTop:%s," - + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s," - + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s", - anchorPosition, - anchorTop, - mPendingScrollPosition, - mAnchorPageBreakPosition, - mUpperPageBreakPosition, - mLowerPageBreakPosition)); - } - - /* - * Detach all attached view for 2 reasons: - * - * 1) So that views are put in the scrap heap. This enables us to call {@link - * RecyclerView.Recycler#getViewForPosition(int)} which will either return one of these - * detached views if it is in the scrap heap, one from the recycled pool (will only call - * onBind in the adapter), or create an entirely new row if needed (will call onCreate - * and onBind in the adapter). - * 2) So that views are automatically removed if they are not manually re-added. - */ - detachAndScrapAttachedViews(recycler); - - /* - * Layout the views recursively. - * - * It's possible that this re-layout is triggered because an item gets removed. If the - * anchor view is at the end of the list, the anchor view position will be bigger than the - * number of available items. Correct that, and only start the layout if the anchor - * position is valid. - */ - anchorPosition = Math.min(anchorPosition, getItemCount() - 1); - if (anchorPosition >= 0) { - View anchor = layoutAnchor(recycler, anchorPosition, anchorTop); - View adjacentRow = anchor; - while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { - adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); - } - adjacentRow = anchor; - while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { - adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); - } - } - - updatePageBreakPositions(); - offsetRows(); - - if (Log.isLoggable(TAG, Log.VERBOSE) && getChildCount() > 1) { - Log.v(TAG, "Currently showing " - + getChildCount() - + " views " - + getPosition(getChildAt(0)) - + " to " - + getPosition(getChildAt(getChildCount() - 1)) - + " anchor " - + anchorPosition); - } - // Should be at least 1 - mViewsPerPage = - Math.max(getLastFullyVisibleChildIndex() + 1 - getFirstFullyVisibleChildIndex(), 1); - mCurrentPage = getFirstFullyVisibleChildPosition() / mViewsPerPage; - Log.v(TAG, "viewsPerPage " + mViewsPerPage); - } - - /** - * scrollVerticallyBy does the work of what should happen when the list scrolls in addition to - * handling cases where the list hits the end. It should be lighter weight than - * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list - * and removes views that have gone out of bounds and lays out new ones that scroll in. - * - * @param dy The amount that the list is supposed to scroll. > 0 means the list is scrolling - * down. < 0 means the list is scrolling up. - * @param recycler The recycler that enables views to be reused or created as they scroll in. - * @param state Various information about the current state of affairs. - * @return The amount the list actually scrolled. - * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State) - */ - @Override - public int scrollVerticallyBy( - int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { - // If the list is empty, we can prevent the overscroll glow from showing by just - // telling RecycerView that we scrolled. - if (getItemCount() == 0) { - return dy; - } - - // Prevent redundant computations if there is definitely nowhere to scroll to. - if (getChildCount() <= 1 || dy == 0) { - mReachedLimitOfDrag = true; - return 0; - } - - View firstChild = getChildAt(0); - if (firstChild == null) { - mReachedLimitOfDrag = true; - return 0; - } - int firstChildPosition = getPosition(firstChild); - RecyclerView.LayoutParams firstChildParams = getParams(firstChild); - int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin; - - View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex()); - if (lastFullyVisibleView == null) { - mReachedLimitOfDrag = true; - return 0; - } - boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1; - - View firstFullyVisibleChild = getFirstFullyVisibleChild(); - if (firstFullyVisibleChild == null) { - mReachedLimitOfDrag = true; - return 0; - } - int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild); - RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild); - int topRemainingSpace = - getDecoratedTop(firstFullyVisibleChild) - - firstFullyVisibleChildParams.topMargin - - getPaddingTop(); - - if (isLastViewVisible - && firstFullyVisiblePosition == mAnchorPageBreakPosition - && dy > topRemainingSpace - && dy > 0) { - // Prevent dragging down more than 1 page. As a side effect, this also prevents you - // from dragging past the bottom because if you are on the second to last page, it - // prevents you from dragging past the last page. - dy = topRemainingSpace; - mReachedLimitOfDrag = true; - } else if (dy < 0 - && firstChildPosition == 0 - && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) { - // Prevent scrolling past the beginning - dy = firstChildTopWithMargin - getPaddingTop(); - mReachedLimitOfDrag = true; - } else { - mReachedLimitOfDrag = false; - } - - boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING; - if (isDragging) { - mLastDragDistance += dy; - } - // We offset by -dy because the views translate in the opposite direction that the - // list scrolls (think about it.) - offsetChildrenVertical(-dy); - - // The last item in the layout should never scroll above the viewport - View view = getChildAt(getChildCount() - 1); - if (view.getTop() < 0) { - view.setTop(0); - } - - // This is the meat of this function. We remove views on the trailing edge of the scroll - // and add views at the leading edge as necessary. - View adjacentRow; - if (dy > 0) { - recycleChildrenFromStart(recycler); - adjacentRow = getChildAt(getChildCount() - 1); - while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { - adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); - } - } else { - recycleChildrenFromEnd(recycler); - adjacentRow = getChildAt(0); - while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { - adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); - } - } - // Now that the correct views are laid out, offset rows as necessary so we can do whatever - // fancy animation we want such as having the top view fly off the screen as the next one - // settles in to place. - updatePageBreakPositions(); - offsetRows(); - - if (getChildCount() > 1) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v( - TAG, - String.format( - "Currently showing %d views (%d to %d)", - getChildCount(), - getPosition(getChildAt(0)), - getPosition(getChildAt(getChildCount() - 1)))); - } - } - updatePagedState(); - return dy; - } - - private void updatePagedState() { - int page = getFirstFullyVisibleChildPosition() / mViewsPerPage; - if (mOnScrollListener != null) { - if (page > mCurrentPage) { - mOnScrollListener.onPageDown(); - } else if (page < mCurrentPage) { - mOnScrollListener.onPageUp(); - } - } - mCurrentPage = page; - } - - @Override - public void scrollToPosition(int position) { - mPendingScrollPosition = position; - requestLayout(); - } - - @Override - public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, - int position) { - /* - * startSmoothScroll will handle stopping the old one if there is one. We only keep a copy - * of it to handle the translation of rows as they slide off the screen in - * offsetRowsWithPageBreak(). - */ - mSmoothScroller = new CarSmoothScroller(mContext, position); - mSmoothScroller.setTargetPosition(position); - startSmoothScroll(mSmoothScroller); - } - - /** Miscellaneous bookkeeping. */ - @Override - public void onScrollStateChanged(int state) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, ":: onScrollStateChanged " + state); - } - if (state == RecyclerView.SCROLL_STATE_IDLE) { - // If the focused view is off screen, give focus to one that is. - // If the first fully visible view is first in the list, focus the first item. - // Otherwise, focus the second so that you have the first item as scrolling context. - View focusedChild = getFocusedChild(); - if (focusedChild != null - && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom() - || getDecoratedBottom(focusedChild) <= getPaddingTop())) { - focusedChild.clearFocus(); - requestLayout(); - } - - } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) { - mLastDragDistance = 0; - } - - if (state != RecyclerView.SCROLL_STATE_SETTLING) { - mSmoothScroller = null; - } - - mScrollState = state; - updatePageBreakPositions(); - } - - @Override - public void onItemsChanged(RecyclerView recyclerView) { - super.onItemsChanged(recyclerView); - // When item changed, our sample view height is no longer accurate, and need to be - // recomputed. - mSampleViewHeight = -1; - } - - /** - * Gives us the opportunity to override the order of the focused views. By default, it will just - * go from top to bottom. However, if there is no focused views, we take over the logic and - * start the focused views from the middle of what is visible and move from there until the - * end of the laid out views in the specified direction. - */ - @Override - public boolean onAddFocusables( - RecyclerView recyclerView, ArrayList views, int direction, int focusableMode) { - View focusedChild = getFocusedChild(); - if (focusedChild != null) { - // If there is a view that already has focus, we can just return false and the normal - // Android addFocusables will work fine. - return false; - } - - // Now we know that there isn't a focused view. We need to set up focusables such that - // instead of just focusing the first item that has been laid out, it focuses starting - // from a visible item. - - int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); - if (firstFullyVisibleChildIndex == -1) { - // Somehow there is a focused view but there is no fully visible view. There shouldn't - // be a way for this to happen but we'd better stop here and return instead of - // continuing on with -1. - Log.w(TAG, "There is a focused child but no first fully visible child."); - return false; - } - View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex); - int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild); - - int firstFocusableChildIndex = firstFullyVisibleChildIndex; - if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) { - // We are somewhere in the middle of the list. Instead of starting focus on the first - // item, start focus on the second item to give some context that we aren't at - // the beginning. - firstFocusableChildIndex++; - } - - if (direction == View.FOCUS_FORWARD) { - // Iterate from the first focusable view to the end. - for (int i = firstFocusableChildIndex; i < getChildCount(); i++) { - views.add(getChildAt(i)); - } - return true; - } else if (direction == View.FOCUS_BACKWARD) { - // Iterate from the first focusable view to the beginning. - for (int i = firstFocusableChildIndex; i >= 0; i--) { - views.add(getChildAt(i)); - } - return true; - } else if (direction == View.FOCUS_DOWN) { - // Framework calls onAddFocusables with FOCUS_DOWN direction when the focus is first - // gained. Thereafter, it calls onAddFocusables with FOCUS_FORWARD or FOCUS_BACKWARD. - // First we try to put the focus back on the last focused item, if it is visible - int lastFocusedVisibleChildIndex = getLastFocusedChildIndexIfVisible(); - if (lastFocusedVisibleChildIndex != -1) { - views.add(getChildAt(lastFocusedVisibleChildIndex)); - return true; - } - } - return false; - } - - @Override - public View onFocusSearchFailed( - View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { - // This doesn't seem to get called the way focus is handled in gearhead... - return null; - } - - /** - * This is the function that decides where to scroll to when a new view is focused. You can get - * the position of the currently focused child through the child parameter. Once you have that, - * determine where to smooth scroll to and scroll there. - * - * @param parent The RecyclerView hosting this LayoutManager - * @param state Current state of RecyclerView - * @param child Direct child of the RecyclerView containing the newly focused view - * @param focused The newly focused view. This may be the same view as child or it may be null - * @return {@code true} if the default scroll behavior should be suppressed - */ - @Override - public boolean onRequestChildFocus( - RecyclerView parent, RecyclerView.State state, View child, View focused) { - if (child == null) { - Log.w(TAG, "onRequestChildFocus with a null child!"); - return true; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child, - focused)); - } - - return onRequestChildFocusMarioStyle(parent, child); - } - - /** - * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar - * reaches the bottom of the screen when the last item is fully visible. This is because there - * are multiple points that could be considered the bottom since the last item can scroll past - * the bottom edge of the screen. - * - *

    To find the extent, we divide the number of items that can fit on screen by the number of - * items in total. - */ - @Override - public int computeVerticalScrollExtent(RecyclerView.State state) { - if (getChildCount() <= 1) { - return 0; - } - - int sampleViewHeight = getSampleViewHeight(); - int availableHeight = getAvailableHeight(); - int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; - - if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) { - return SCROLL_RANGE; - } else { - return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount(); - } - } - - /** - * The scrolling offset is calculated by determining what position is at the top of the list. - * However, instead of using fixed integer positions for each row, the scroll position is - * factored in and the position is recalculated as a float that takes in to account the - * current scroll state. This results in a smooth animation for the scrollbar when the user - * scrolls the list. - */ - @Override - public int computeVerticalScrollOffset(RecyclerView.State state) { - View firstChild = getFirstFullyVisibleChild(); - if (firstChild == null) { - return 0; - } - - RecyclerView.LayoutParams params = getParams(firstChild); - int firstChildPosition = getPosition(firstChild); - float previousChildHieght = (float) (getDecoratedMeasuredHeight(firstChild) - + params.topMargin + params.bottomMargin); - - // Assume the previous view is the same height as the current one. - float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin) - / previousChildHieght; - // If the previous view is actually larger than the current one then this the percent - // can be greater than 1. - percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1); - - float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing; - - int sampleViewHeight = getSampleViewHeight(); - int availableHeight = getAvailableHeight(); - int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; - int positionWhenLastItemIsVisible = - state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen; - - if (positionWhenLastItemIsVisible <= 0) { - return 0; - } - - if (currentPosition >= positionWhenLastItemIsVisible) { - return SCROLL_RANGE; - } - - return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible); - } - - /** - * The range of the scrollbar can be understood as the granularity of how we want the scrollbar - * to scroll. - */ - @Override - public int computeVerticalScrollRange(RecyclerView.State state) { - return SCROLL_RANGE; - } - - @Override - public void onAttachedToWindow(RecyclerView view) { - super.onAttachedToWindow(view); - // The purpose of calling this is so that any animation offsets are re-applied. These are - // cleared in View.onDetachedFromWindow(). - // This fixes b/27672379 - updatePageBreakPositions(); - offsetRows(); - } - - @Override - public void onDetachedFromWindow(RecyclerView recyclerView, Recycler recycler) { - super.onDetachedFromWindow(recyclerView, recycler); - } - - /** - * @return The first view that starts on screen. It assumes that it fully fits on the screen - * though. If the first fully visible child is also taller than the screen then it will - * still be returned. However, since the LayoutManager snaps to view starts, having a row - * that tall would lead to a broken experience anyways. - */ - public int getFirstFullyVisibleChildIndex() { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - RecyclerView.LayoutParams params = getParams(child); - if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) { - return i; - } - } - return -1; - } - - /** - * @return The position of first visible child in the list. -1 will be returned if there is no - * child. - */ - public int getFirstFullyVisibleChildPosition() { - View child = getFirstFullyVisibleChild(); - if (child == null) { - return -1; - } - return getPosition(child); - } - - /** - * @return The position of last visible child in the list. -1 will be returned if there is no - * child. - */ - public int getLastFullyVisibleChildPosition() { - View child = getLastFullyVisibleChild(); - if (child == null) { - return -1; - } - return getPosition(child); - } - - /** @return The first View that is completely visible on-screen. */ - public View getFirstFullyVisibleChild() { - int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); - View firstChild = null; - if (firstFullyVisibleChildIndex != -1) { - firstChild = getChildAt(firstFullyVisibleChildIndex); - } - return firstChild; - } - - /** @return The last View that is completely visible on-screen. */ - public View getLastFullyVisibleChild() { - int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex(); - View lastChild = null; - if (lastFullyVisibleChildIndex != -1) { - lastChild = getChildAt(lastFullyVisibleChildIndex); - } - return lastChild; - } - - /** - * @return The last view that ends on screen. It assumes that the start is also on screen - * though. If the last fully visible child is also taller than the screen then it will - * still be returned. However, since the LayoutManager snaps to view starts, having a row - * that tall would lead to a broken experience anyways. - */ - public int getLastFullyVisibleChildIndex() { - for (int i = getChildCount() - 1; i >= 0; i--) { - View child = getChildAt(i); - RecyclerView.LayoutParams params = getParams(child); - int childBottom = getDecoratedBottom(child) + params.bottomMargin; - int listBottom = getHeight() - getPaddingBottom(); - if (childBottom <= listBottom) { - return i; - } - } - return -1; - } - - /** - * Returns the index of the child in the list that was last focused and is currently visible to - * the user. If no child is found, returns -1. - */ - public int getLastFocusedChildIndexIfVisible() { - if (mLastChildPositionToRequestFocus == -1) { - return -1; - } - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (getPosition(child) == mLastChildPositionToRequestFocus) { - RecyclerView.LayoutParams params = getParams(child); - int childBottom = getDecoratedBottom(child) + params.bottomMargin; - int listBottom = getHeight() - getPaddingBottom(); - if (childBottom <= listBottom) { - return i; - } - break; - } - } - return -1; - } - - /** @return Whether or not the first view is fully visible. */ - public boolean isAtTop() { - // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views - // and also means that the list is at the top. - return getFirstFullyVisibleChildIndex() <= 0; - } - - /** @return Whether or not the last view is fully visible. */ - public boolean isAtBottom() { - int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex(); - if (lastFullyVisibleChildIndex == -1) { - return true; - } - View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex); - return getPosition(lastFullyVisibleChild) == getItemCount() - 1; - } - - /** - * Sets whether or not the rows have an offset animation when it scrolls off-screen. The type - * of offset is determined by {@link #setRowOffsetMode(int)}. - * - *

    A row being offset means that when they reach the top of the screen, the row is flung off - * respectively to the rest of the list. This creates a gap between the offset row(s) and the - * list. - * - * @param offsetRows {@code true} if the rows should be offset. - */ - public void setOffsetRows(boolean offsetRows) { - mOffsetRows = offsetRows; - if (offsetRows) { - // Card animation offsets are only needed when we use the flying off the screen effect - if (mFlyOffscreenAnimations == null) { - mFlyOffscreenAnimations = new LruCache<>(MAX_ANIMATIONS_IN_CACHE); - } - offsetRows(); - } else { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - setCardFlyingEffectOffset(getChildAt(i), 0); - } - mFlyOffscreenAnimations = null; - } - } - - /** - * Sets the manner of offsetting the rows when they are scrolled off-screen. The rows are either - * offset individually or the entire page being scrolled off is offset. - * - * @param mode One of {@link #ROW_OFFSET_MODE_INDIVIDUAL} or {@link #ROW_OFFSET_MODE_PAGE}. - */ - public void setRowOffsetMode(@RowOffsetMode int mode) { - if (mode == mRowOffsetMode) { - return; - } - - mRowOffsetMode = mode; - offsetRows(); - } - - /** - * Sets the listener that will be notified of various scroll events in the list. - * - * @param listener The on-scroll listener. - */ - public void setOnScrollListener(PagedListView.OnScrollListener listener) { - mOnScrollListener = listener; - } - - /** - * Finish the pagination taking into account where the gesture started (not where we are now). - * - * @return Whether the list was scrolled as a result of the fling. - */ - public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) { - if (getChildCount() == 0) { - return false; - } - - if (mReachedLimitOfDrag) { - return false; - } - - // If the fling was too slow or too short, settle on the first fully visible row instead. - if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE - || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) { - int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); - if (firstFullyVisibleChildIndex != -1) { - int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex)); - parent.smoothScrollToPosition(scrollPosition); - return true; - } - return false; - } - - // Finish the pagination taking into account where the gesture - // started (not where we are now). - boolean isDownGesture = flingVelocity > 0 || (flingVelocity == 0 && mLastDragDistance >= 0); - boolean isUpGesture = flingVelocity < 0 || (flingVelocity == 0 && mLastDragDistance < 0); - if (isDownGesture && mLowerPageBreakPosition != -1) { - // If the last view is fully visible then only settle on the first fully visible view - // instead of the original page down position. However, don't page down if the last - // item has come fully into view. - parent.smoothScrollToPosition(mAnchorPageBreakPosition); - if (mOnScrollListener != null) { - mOnScrollListener.onGestureDown(); - } - return true; - } else if (isUpGesture && mUpperPageBreakPosition != -1) { - parent.smoothScrollToPosition(mUpperPageBreakPosition); - if (mOnScrollListener != null) { - mOnScrollListener.onGestureUp(); - } - return true; - } else { - Log.e( - TAG, - "Error setting scroll for fling! flingVelocity: \t" - + flingVelocity - + "\tlastDragDistance: " - + mLastDragDistance - + "\tpageUpAtStartOfDrag: " - + mUpperPageBreakPosition - + "\tpageDownAtStartOfDrag: " - + mLowerPageBreakPosition); - // As a last resort, at the last smooth scroller target position if there is one. - if (mSmoothScroller != null) { - parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition()); - return true; - } - } - return false; - } - - /** @return The position that paging up from the current position would settle at. */ - public int getPageUpPosition() { - return mUpperPageBreakPosition; - } - - /** @return The position that paging down from the current position would settle at. */ - public int getPageDownPosition() { - return mLowerPageBreakPosition; - } - - @Override - public Parcelable onSaveInstanceState() { - SavedState savedState = new SavedState(); - savedState.mFirstChildPosition = getFirstFullyVisibleChildPosition(); - return savedState; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (state instanceof SavedState) { - scrollToPosition(((SavedState) state).mFirstChildPosition); - } - } - - /** The state that will be saved across configuration changes. */ - static class SavedState implements Parcelable { - /** The position of the first visible child view in the list. */ - int mFirstChildPosition; - - SavedState() {} - - private SavedState(Parcel in) { - mFirstChildPosition = in.readInt(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mFirstChildPosition); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - /** - * Layout the anchor row. The anchor row is the first fully visible row. - * - * @param anchorTop The decorated top of the anchor. If it is not known or should be reset to - * the top, pass -1. - */ - private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) { - View anchor = recycler.getViewForPosition(anchorPosition); - RecyclerView.LayoutParams params = getParams(anchor); - measureChildWithMargins(anchor, 0, 0); - int left = getPaddingLeft() + params.leftMargin; - int top = (anchorTop == -1) ? params.topMargin : anchorTop; - int right = left + getDecoratedMeasuredWidth(anchor); - int bottom = top + getDecoratedMeasuredHeight(anchor); - layoutDecorated(anchor, left, top, right, bottom); - addView(anchor); - return anchor; - } - - /** - * Lays out the next row in the specified direction next to the specified adjacent row. - * - * @param recycler The recycler from which a new view can be created. - * @param adjacentRow The View of the adjacent row which will be used to position the new one. - * @param layoutDirection The side of the adjacent row that the new row will be laid out on. - * @return The new row that was laid out. - */ - private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow, - @LayoutDirection int layoutDirection) { - int adjacentRowPosition = getPosition(adjacentRow); - int newRowPosition = adjacentRowPosition; - if (layoutDirection == BEFORE) { - newRowPosition = adjacentRowPosition - 1; - } else if (layoutDirection == AFTER) { - newRowPosition = adjacentRowPosition + 1; - } - - // Because we detach all rows in onLayoutChildren, this will often just return a view from - // the scrap heap. - View newRow = recycler.getViewForPosition(newRowPosition); - - measureChildWithMargins(newRow, 0, 0); - RecyclerView.LayoutParams newRowParams = - (RecyclerView.LayoutParams) newRow.getLayoutParams(); - RecyclerView.LayoutParams adjacentRowParams = - (RecyclerView.LayoutParams) adjacentRow.getLayoutParams(); - int left = getPaddingLeft() + newRowParams.leftMargin; - int right = left + getDecoratedMeasuredWidth(newRow); - int top; - int bottom; - if (layoutDirection == BEFORE) { - bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin; - top = bottom - getDecoratedMeasuredHeight(newRow); - } else { - top = getDecoratedBottom(adjacentRow) + adjacentRowParams.bottomMargin - + newRowParams.topMargin; - bottom = top + getDecoratedMeasuredHeight(newRow); - } - layoutDecorated(newRow, left, top, right, bottom); - - if (layoutDirection == BEFORE) { - addView(newRow, 0); - } else { - addView(newRow); - } - - return newRow; - } - - /** @return Whether another row should be laid out in the specified direction. */ - private boolean shouldLayoutNextRow( - RecyclerView.State state, View adjacentRow, @LayoutDirection int layoutDirection) { - int adjacentRowPosition = getPosition(adjacentRow); - - if (layoutDirection == BEFORE) { - if (adjacentRowPosition == 0) { - // We already laid out the first row. - return false; - } - } else if (layoutDirection == AFTER) { - if (adjacentRowPosition >= state.getItemCount() - 1) { - // We already laid out the last row. - return false; - } - } - - // If we are scrolling layout views until the target position. - if (mSmoothScroller != null) { - if (layoutDirection == BEFORE - && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) { - return true; - } else if (layoutDirection == AFTER - && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) { - return true; - } - } - - View focusedRow = getFocusedChild(); - if (focusedRow != null) { - int focusedRowPosition = getPosition(focusedRow); - if (layoutDirection == BEFORE && adjacentRowPosition - >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { - return true; - } else if (layoutDirection == AFTER && adjacentRowPosition - <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { - return true; - } - } - - RecyclerView.LayoutParams params = getParams(adjacentRow); - int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin; - int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin; - if (layoutDirection == BEFORE && adjacentRowTop < getPaddingTop() - getHeight()) { - // View is more than 1 page past the top of the screen and also past where the user has - // scrolled to. We want to keep one page past the top to make the scroll up calculation - // easier and scrolling smoother. - return false; - } else if (layoutDirection == AFTER - && adjacentRowBottom > getHeight() - getPaddingBottom()) { - // View is off of the bottom and also past where the user has scrolled to. - return false; - } - - return true; - } - - /** Remove and recycle views that are no longer needed. */ - private void recycleChildrenFromStart(RecyclerView.Recycler recycler) { - // Start laying out children one page before the top of the viewport. - int childrenStart = getPaddingTop() - getHeight(); - - int focusedChildPosition = Integer.MAX_VALUE; - View focusedChild = getFocusedChild(); - if (focusedChild != null) { - focusedChildPosition = getPosition(focusedChild); - } - - // Count the number of views that should be removed. - int detachedCount = 0; - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - int childEnd = getDecoratedBottom(child); - int childPosition = getPosition(child); - - if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) { - break; - } - - detachedCount++; - } - - // Remove the number of views counted above. Done by removing the first child n times. - while (--detachedCount >= 0) { - final View child = getChildAt(0); - removeAndRecycleView(child, recycler); - } - } - - /** Remove and recycle views that are no longer needed. */ - private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) { - // Layout views until the end of the viewport. - int childrenEnd = getHeight(); - - int focusedChildPosition = Integer.MIN_VALUE + 1; - View focusedChild = getFocusedChild(); - if (focusedChild != null) { - focusedChildPosition = getPosition(focusedChild); - } - - // Count the number of views that should be removed. - int firstDetachedPos = 0; - int detachedCount = 0; - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - int childStart = getDecoratedTop(child); - int childPosition = getPosition(child); - - if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) { - break; - } - - firstDetachedPos = i; - detachedCount++; - } - - while (--detachedCount >= 0) { - final View child = getChildAt(firstDetachedPos); - removeAndRecycleView(child, recycler); - } - } - - /** - * Offset rows to do fancy animations. If offset rows was not enabled with - * {@link #setOffsetRows}, this will do nothing. - * - * @see #offsetRowsIndividually - * @see #offsetRowsByPage - * @see #setOffsetRows - */ - public void offsetRows() { - if (!mOffsetRows) { - return; - } - - if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) { - offsetRowsByPage(); - } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) { - offsetRowsIndividually(); - } - } - - /** - * Offset the single row that is scrolling off the screen such that by the time the next row - * reaches the top, it will have accelerated completely off of the screen. - */ - private void offsetRowsIndividually() { - if (getChildCount() == 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, ":: offsetRowsIndividually getChildCount=0"); - } - return; - } - - // Identify the dangling row. It will be the first row that is at the top of the - // list or above. - int danglingChildIndex = -1; - for (int i = getChildCount() - 1; i >= 0; i--) { - View child = getChildAt(i); - if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) { - danglingChildIndex = i; - break; - } - } - - mAnchorPageBreakPosition = danglingChildIndex; - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex); - } - - // Calculate the total amount that the view will need to scroll in order to go completely - // off screen. - RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); - int[] locs = new int[2]; - rv.getLocationInWindow(locs); - int listTopInWindow = locs[1] + rv.getPaddingTop(); - int maxDanglingViewTranslation; - - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - RecyclerView.LayoutParams params = getParams(child); - - maxDanglingViewTranslation = listTopInWindow; - // If the child has a negative margin, we'll actually need to translate the view a - // little but further to get it completely off screen. - if (params.topMargin < 0) { - maxDanglingViewTranslation -= params.topMargin; - } - if (params.bottomMargin < 0) { - maxDanglingViewTranslation -= params.bottomMargin; - } - - if (i < danglingChildIndex) { - child.setAlpha(0f); - } else if (i > danglingChildIndex) { - child.setAlpha(1f); - setCardFlyingEffectOffset(child, 0); - } else { - int totalScrollDistance = - getDecoratedMeasuredHeight(child) + params.topMargin + params.bottomMargin; - - int distanceLeftInScroll = - getDecoratedBottom(child) + params.bottomMargin - getPaddingTop(); - float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance; - float interpolatedPercentage = - mDanglingRowInterpolator.getInterpolation(percentageIntoScroll); - - child.setAlpha(1f); - setCardFlyingEffectOffset(child, -(maxDanglingViewTranslation - * interpolatedPercentage)); - } - } - } - - /** - * When the list scrolls, the entire page of rows will offset in one contiguous block. This - * significantly reduces the amount of extra motion at the top of the screen. - */ - private void offsetRowsByPage() { - View anchorView = findViewByPosition(mAnchorPageBreakPosition); - if (anchorView == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, ":: offsetRowsByPage anchorView null"); - } - return; - } - int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin; - - View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); - int upperViewTop = - getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin; - - int scrollDistance = upperViewTop - anchorViewTop; - - int distanceLeft = anchorViewTop - getPaddingTop(); - float scrollPercentage = - (Math.abs(scrollDistance) - distanceLeft) / (float) Math.abs(scrollDistance); - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format(":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, " - + "scrollPercentage:%s", - scrollDistance, distanceLeft, scrollPercentage)); - } - - // Calculate the total amount that the view will need to scroll in order to go completely - // off screen. - RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); - int[] locs = new int[2]; - rv.getLocationInWindow(locs); - int listTopInWindow = locs[1] + rv.getPaddingTop(); - - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - int position = getPosition(child); - if (position < mUpperPageBreakPosition) { - child.setAlpha(0f); - setCardFlyingEffectOffset(child, -listTopInWindow); - } else if (position < mAnchorPageBreakPosition) { - // If the child has a negative margin, we need to offset the row by a little bit - // extra so that it moves completely off screen. - RecyclerView.LayoutParams params = getParams(child); - int extraTranslation = 0; - if (params.topMargin < 0) { - extraTranslation -= params.topMargin; - } - if (params.bottomMargin < 0) { - extraTranslation -= params.bottomMargin; - } - int translation = (int) ((listTopInWindow + extraTranslation) - * mDanglingRowInterpolator.getInterpolation(scrollPercentage)); - child.setAlpha(1f); - setCardFlyingEffectOffset(child, -translation); - } else { - child.setAlpha(1f); - setCardFlyingEffectOffset(child, 0); - } - } - } - - /** - * Apply an offset to this view. This offset is applied post-layout so it doesn't affect when - * views are recycled - * - * @param child The view to apply this to - * @param verticalOffset The offset for this child. - */ - private void setCardFlyingEffectOffset(View child, float verticalOffset) { - // Ideally instead of doing all this, we could use View.setTranslationY(). However, the - // default RecyclerView.ItemAnimator also uses this method which causes layout issues. - // See: http://b/25977087 - TranslateAnimation anim = mFlyOffscreenAnimations.get(child); - if (anim == null) { - anim = new TranslateAnimation(); - anim.setFillEnabled(true); - anim.setFillAfter(true); - anim.setDuration(0); - mFlyOffscreenAnimations.put(child, anim); - } else if (anim.verticalOffset == verticalOffset) { - return; - } - - anim.reset(); - anim.verticalOffset = verticalOffset; - anim.setStartTime(Animation.START_ON_FIRST_FRAME); - child.setAnimation(anim); - anim.startNow(); - } - - /** - * Update the page break positions based on the position of the views on screen. This should be - * called whenever view move or change such as during a scroll or layout. - */ - private void updatePageBreakPositions() { - if (getChildCount() == 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0"); - } - return; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions " - + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, " - + "mLowerPageBreakPosition:%s", - mAnchorPageBreakPosition, mUpperPageBreakPosition, - mLowerPageBreakPosition)); - } - - mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild()); - - if (mAnchorPageBreakPosition == -1) { - Log.w(TAG, "Unable to update anchor positions. There is no anchor position."); - return; - } - - View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition); - if (anchorPageBreakView == null) { - return; - } - int topMargin = getParams(anchorPageBreakView).topMargin; - int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin; - View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); - int upperPageBreakTop = upperPageBreakView == null - ? Integer.MIN_VALUE - : getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin; - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s" - + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s," - + " mLowerPageBreakPosition:%s", - topMargin, - anchorTop, - mAnchorPageBreakPosition, - mUpperPageBreakPosition, - mLowerPageBreakPosition)); - } - - if (anchorTop < getPaddingTop()) { - // The anchor has moved above the viewport. We are now on the next page. Shift the page - // break positions and calculate a new lower one. - mUpperPageBreakPosition = mAnchorPageBreakPosition; - mAnchorPageBreakPosition = mLowerPageBreakPosition; - mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); - } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) { - // The anchor has moved below the viewport. We are now on the previous page. Shift - // the page break positions and calculate a new upper one. - mLowerPageBreakPosition = mAnchorPageBreakPosition; - mAnchorPageBreakPosition = mUpperPageBreakPosition; - mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); - } else { - mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); - mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions" - + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s," - + " mLowerPageBreakPosition:%s", - mAnchorPageBreakPosition, mUpperPageBreakPosition, - mLowerPageBreakPosition)); - } - } - - /** - * @return The page break position of the page before the anchor page break position. However, - * if it reaches the end of the laid out children or position 0, it will just return that. - */ - @VisibleForTesting - int calculatePreviousPageBreakPosition(int position) { - if (position == -1) { - return -1; - } - View referenceView = findViewByPosition(position); - int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; - - int previousPagePosition = position; - while (previousPagePosition > 0) { - previousPagePosition--; - View child = findViewByPosition(previousPagePosition); - if (child == null) { - // View has not been laid out yet. - return previousPagePosition + 1; - } - - int childTop = getDecoratedTop(child) - getParams(child).topMargin; - if (childTop < referenceViewTop - getHeight()) { - return previousPagePosition + 1; - } - } - // Beginning of the list. - return 0; - } - - /** - * @return The page break position of the next page after the anchor page break position. - * However, if it reaches the end of the laid out children or end of the list, it will just - * return that. - */ - @VisibleForTesting - int calculateNextPageBreakPosition(int position) { - if (position == -1) { - return -1; - } - - View referenceView = findViewByPosition(position); - if (referenceView == null) { - return position; - } - int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; - - int nextPagePosition = position; - - // Search for the first child item after the referenceView that didn't fully fit on to the - // screen. The next page should start from the item before this child, so that users have - // a visual anchoring point of the page change. - while (nextPagePosition < getItemCount() - 1) { - nextPagePosition++; - View child = findViewByPosition(nextPagePosition); - if (child == null) { - // The next view has not been laid out yet. - return nextPagePosition - 1; - } - - int childTop = getDecoratedTop(child) - getParams(child).topMargin; - if (childTop > referenceViewTop + getHeight()) { - // If choosing the previous child causes the view to snap back to the referenceView - // position, then skip that and go directly to the child. This avoids the case - // where a tall card in the layout causes the view to constantly snap back to - // the top when scrolled. - return nextPagePosition - 1 == position ? nextPagePosition : nextPagePosition - 1; - } - } - // End of the list. - return nextPagePosition; - } - - /** - * In this style, the focus will scroll down to the middle of the screen and lock there so that - * moving in either direction will move the entire list by 1. - */ - private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) { - int focusedPosition = getPosition(child); - if (focusedPosition == mLastChildPositionToRequestFocus) { - return true; - } - mLastChildPositionToRequestFocus = focusedPosition; - - int availableHeight = getAvailableHeight(); - int focusedChildTop = getDecoratedTop(child); - int focusedChildBottom = getDecoratedBottom(child); - - int childIndex = parent.indexOfChild(child); - // Iterate through children starting at the focused child to find the child above it to - // smooth scroll to such that the focused child will be as close to the middle of the screen - // as possible. - for (int i = childIndex; i >= 0; i--) { - View childAtI = getChildAt(i); - if (childAtI == null) { - Log.e(TAG, "Child is null at index " + i); - continue; - } - // We haven't found a view that is more than half of the recycler view height above it - // but we've reached the top so we can't go any further. - if (i == 0) { - parent.smoothScrollToPosition(getPosition(childAtI)); - break; - } - - // Because we want to scroll to the first view that is less than half of the screen - // away from the focused view, we "look ahead" one view. When the look ahead view - // is more than availableHeight / 2 away, the current child at i is the one we want to - // scroll to. However, sometimes, that view can be null (ie, if the view is in - // transition). In that case, just skip that view. - - View childBefore = getChildAt(i - 1); - if (childBefore == null) { - continue; - } - int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore); - int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore); - - if (distanceToChildBeforeFromTop > availableHeight / 2 - || distanceToChildBeforeFromBottom > availableHeight) { - parent.smoothScrollToPosition(getPosition(childAtI)); - break; - } - } - return true; - } - - /** - * We don't actually know the size of every single view, only what is currently laid out. This - * makes it difficult to do accurate scrollbar calculations. However, lists in the car often - * consist of views with identical heights. Because of that, we can use a single sample view to - * do our calculations for. The main exceptions are in the first items of a list (hero card, - * last call card, etc) so if the first view is at position 0, we pick the next one. - * - * @return The decorated measured height of the sample view plus its margins. - */ - private int getSampleViewHeight() { - if (mSampleViewHeight != -1) { - return mSampleViewHeight; - } - int sampleViewIndex = getFirstFullyVisibleChildIndex(); - View sampleView = getChildAt(sampleViewIndex); - if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) { - sampleView = getChildAt(++sampleViewIndex); - } - RecyclerView.LayoutParams params = getParams(sampleView); - int height = getDecoratedMeasuredHeight(sampleView) + params.topMargin - + params.bottomMargin; - if (height == 0) { - // This can happen if the view isn't measured yet. - Log.w( - TAG, - "The sample view has a height of 0. Returning a dummy value for now " - + "that won't be cached."); - height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height); - } else { - mSampleViewHeight = height; - } - return height; - } - - /** @return The height of the RecyclerView excluding padding. */ - private int getAvailableHeight() { - return getHeight() - getPaddingTop() - getPaddingBottom(); - } - - /** - * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child of - * {@link RecyclerView}. - */ - private static RecyclerView.LayoutParams getParams(View view) { - return (RecyclerView.LayoutParams) view.getLayoutParams(); - } - - /** - * Custom {@link LinearSmoothScroller} that has: a) Custom control over the speed of scrolls. b) - * Scrolling snaps to start. All of our scrolling logic depends on that. c) Keeps track of some - * state of the current scroll so that can aid in things like the scrollbar calculations. - */ - private final class CarSmoothScroller extends LinearSmoothScroller { - /** This value (150) was hand tuned by UX for what felt right. * */ - private static final float MILLISECONDS_PER_INCH = 150f; - /** This value (0.45) was hand tuned by UX for what felt right. * */ - private static final float DECELERATION_TIME_DIVISOR = 0.45f; - - /** This value (1.8) was hand tuned by UX for what felt right. * */ - private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f); - - private final int mTargetPosition; - - CarSmoothScroller(Context context, int targetPosition) { - super(context); - mTargetPosition = targetPosition; - } - - @Override - public PointF computeScrollVectorForPosition(int i) { - if (getChildCount() == 0) { - return null; - } - final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex())); - final int direction = (mTargetPosition < firstChildPos) ? -1 : 1; - return new PointF(0, direction); - } - - @Override - protected int getVerticalSnapPreference() { - // This is key for most of the scrolling logic that guarantees that scrolling - // will settle with a view aligned to the top. - return LinearSmoothScroller.SNAP_TO_START; - } - - @Override - protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { - int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START); - if (dy == 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Scroll distance is 0"); - } - return; - } - - final int time = calculateTimeForDeceleration(dy); - if (time > 0) { - action.update(0, -dy, time, mInterpolator); - } - } - - @Override - protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { - return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; - } - - @Override - protected int calculateTimeForDeceleration(int dx) { - return (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR); - } - - @Override - public int getTargetPosition() { - return mTargetPosition; - } - } - - /** - * Animation that translates a view by the specified amount. Used for card flying off the screen - * effect. - */ - private static class TranslateAnimation extends Animation { - public float verticalOffset; - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - super.applyTransformation(interpolatedTime, t); - t.getMatrix().setTranslate(0, verticalOffset); - } - } -} diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java deleted file mode 100644 index 67a6247a..00000000 --- a/android/support/car/widget/PagedListView.java +++ /dev/null @@ -1,996 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.IdRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; -import android.support.annotation.UiThread; -import android.support.car.R; -import android.support.v7.widget.RecyclerView; -import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; - -/** - * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that - * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the - * left side. - */ -public class PagedListView extends FrameLayout { - /** Default maximum number of clicks allowed on a list */ - public static final int DEFAULT_MAX_CLICKS = 6; - - /** - * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the - * maximum number of pages to show. - */ - public static final int UNLIMITED_PAGES = -1; - - /** - * The amount of time after settling to wait before autoscrolling to the next page when the user - * holds down a pagination button. - */ - protected static final int PAGINATION_HOLD_DELAY_MS = 400; - - private static final String TAG = "PagedListView"; - private static final int INVALID_RESOURCE_ID = -1; - - protected final CarRecyclerView mRecyclerView; - protected final PagedLayoutManager mLayoutManager; - protected final Handler mHandler = new Handler(); - private final boolean mScrollBarEnabled; - private final PagedScrollBarView mScrollBarView; - - private int mRowsPerPage = -1; - protected RecyclerView.Adapter mAdapter; - - /** Maximum number of pages to show. */ - private int mMaxPages; - - protected OnScrollListener mOnScrollListener; - - /** Number of visible rows per page */ - private int mDefaultMaxPages = DEFAULT_MAX_CLICKS; - - /** Used to check if there are more items added to the list. */ - private int mLastItemCount = 0; - - private boolean mNeedsFocus; - - /** - * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of - * items. - * - *

    NOTE: it is still up to the adapter to use maxItems in {@link - * android.support.v7.widget.RecyclerView.Adapter#getItemCount()}. - * - *

    the recommended way would be with: - * - *

    {@code
    -     * {@literal@}Override
    -     * public int getItemCount() {
    -     *   return Math.min(super.getItemCount(), mMaxItems);
    -     * }
    -     * }
    - */ - public interface ItemCap { - /** - * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. - */ - int UNLIMITED = -1; - - /** - * Sets the maximum number of items available in the adapter. A value less than '0' means - * the list should not be capped. - */ - void setMaxItems(int maxItems); - } - - /** - * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position - * offset for the adapter to load the data. - * - *

    For example in the adapter, if the positionOffset is 20, then for position 0 it will show - * the item in position 20 instead, for position 1 it will show the item in position 21 instead - * and so on. - */ - public interface ItemPositionOffset { - /** Sets the position offset for the adapter. */ - void setPositionOffset(int positionOffset); - } - - public PagedListView(Context context, AttributeSet attrs) { - this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/); - } - - public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) { - this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/); - } - - public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { - this(context, attrs, defStyleAttrs, defStyleRes, 0); - } - - public PagedListView( - Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) { - super(context, attrs, defStyleAttrs, defStyleRes); - if (layoutId == 0) { - layoutId = R.layout.car_paged_recycler_view; - } - LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/); - - TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes); - mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view); - boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false); - mRecyclerView.setFadeLastItem(fadeLastItem); - boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false); - - mMaxPages = getDefaultMaxPages(); - - mLayoutManager = new PagedLayoutManager(context); - mLayoutManager.setOffsetRows(offsetRows); - mRecyclerView.setLayoutManager(mLayoutManager); - mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener); - mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12); - mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager)); - - boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false); - if (offsetScrollBar) { - MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); - params.setMarginStart(getResources().getDimensionPixelSize( - R.dimen.car_margin)); - params.setMarginEnd( - a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0)); - mRecyclerView.setLayoutParams(params); - } - - if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) { - int dividerStartMargin = a.getDimensionPixelSize( - R.styleable.PagedListView_dividerStartMargin, 0); - int dividerStartId = a.getResourceId( - R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID); - int dividerEndId = a.getResourceId( - R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID); - - mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin, - dividerStartId, dividerEndId)); - } - - int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0); - if (itemSpacing > 0) { - mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing)); - } - - // Set this to true so that this view consumes clicks events and views underneath - // don't receive this click event. Without this it's possible to click places in the - // view that don't capture the event, and as a result, elements visually hidden consume - // the event. - setClickable(true); - - // Set focusable false explicitly to handle the behavior change in Android O where - // clickable view becomes focusable by default. - setFocusable(false); - - mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true); - mScrollBarView = (PagedScrollBarView) findViewById(R.id.paged_scroll_view); - mScrollBarView.setPaginationListener( - new PagedScrollBarView.PaginationListener() { - @Override - public void onPaginate(int direction) { - if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) { - mRecyclerView.pageUp(); - if (mOnScrollListener != null) { - mOnScrollListener.onScrollUpButtonClicked(); - } - } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) { - mRecyclerView.pageDown(); - if (mOnScrollListener != null) { - mOnScrollListener.onScrollDownButtonClicked(); - } - } else { - Log.e(TAG, "Unknown pagination direction (" + direction + ")"); - } - } - }); - - Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon); - if (upButtonIcon != null) { - setUpButtonIcon(upButtonIcon); - } - - Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon); - if (downButtonIcon != null) { - setDownButtonIcon(downButtonIcon); - } - - mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE); - - // Modify the layout the Scroll Bar is not visible. - if (!mScrollBarEnabled) { - MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); - params.setMarginStart(0); - mRecyclerView.setLayoutParams(params); - } - - setDayNightStyle(DayNightStyle.AUTO); - a.recycle(); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mHandler.removeCallbacks(mUpdatePaginationRunnable); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_DOWN) { - // The user has interacted with the list using touch. All movements will now paginate - // the list. - mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_PAGE); - } - return super.onInterceptTouchEvent(e); - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - // The user has interacted with the list using the controller. Movements through the list - // will now be one row at a time. - mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL); - } - - /** - * Returns the position of the given View in the list. - * - * @param v The View to check for. - * @return The position or -1 if the given View is {@code null} or not in the list. - */ - public int positionOf(@Nullable View v) { - if (v == null || v.getParent() != mRecyclerView) { - return -1; - } - return mLayoutManager.getPosition(v); - } - - @NonNull - public CarRecyclerView getRecyclerView() { - return mRecyclerView; - } - - /** - * Scrolls to the given position in the PagedListView. - * - * @param position The position in the list to scroll to. - */ - public void scrollToPosition(int position) { - mLayoutManager.scrollToPosition(position); - - // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure - // the pagination arrows actually get updated. See b/http://b/15801119 - mHandler.post(mUpdatePaginationRunnable); - } - - /** Sets the icon to be used for the up button. */ - public void setUpButtonIcon(Drawable icon) { - mScrollBarView.setUpButtonIcon(icon); - } - - /** Sets the icon to be used for the down button. */ - public void setDownButtonIcon(Drawable icon) { - mScrollBarView.setDownButtonIcon(icon); - } - - /** - * Sets the adapter for the list. - * - *

    The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of - * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as - * {@link #setMaxPages(int)}, will do nothing. - */ - public void setAdapter( - @NonNull RecyclerView.Adapter adapter) { - mAdapter = adapter; - mRecyclerView.setAdapter(adapter); - updateMaxItems(); - } - - /** @hide */ - @RestrictTo(LIBRARY_GROUP) - @NonNull - public PagedLayoutManager getLayoutManager() { - return mLayoutManager; - } - - @Nullable - @SuppressWarnings("unchecked") - public RecyclerView.Adapter getAdapter() { - return mRecyclerView.getAdapter(); - } - - /** - * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a - * page is defined as the number of items that fit completely on the screen at once. - * - *

    Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number - * of pages. - * - *

    Note that for any restriction on maximum pages to work, the adapter passed to this - * PagedListView needs to implement {@link ItemCap}. - * - * @param maxPages The maximum number of pages that fit on the screen. Should be positive or - * {@link #UNLIMITED_PAGES}. - */ - public void setMaxPages(int maxPages) { - mMaxPages = Math.max(UNLIMITED_PAGES, maxPages); - updateMaxItems(); - } - - /** - * Returns the maximum number of pages allowed in the PagedListView. This number is set by - * {@link #setMaxPages(int)}. If that method has not been called, then this value should match - * the default value. - * - * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is - * no limit. - */ - public int getMaxPages() { - return mMaxPages; - } - - /** - * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of - * PagedLayoutManager is null or the height of the first child is 0, it will return 1. - */ - public int getRowsPerPage() { - return mRowsPerPage; - } - - /** Resets the maximum number of pages to be shown to be the default. */ - public void resetMaxPages() { - mMaxPages = getDefaultMaxPages(); - updateMaxItems(); - } - - /** - * @return The position of first visible child in the list. -1 will be returned if there is no - * child. - */ - public int getFirstFullyVisibleChildPosition() { - return mLayoutManager.getFirstFullyVisibleChildPosition(); - } - - /** - * @return The position of last visible child in the list. -1 will be returned if there is no - * child. - */ - public int getLastFullyVisibleChildPosition() { - return mLayoutManager.getLastFullyVisibleChildPosition(); - } - - /** - * Adds an {@link android.support.v7.widget.RecyclerView.ItemDecoration} to this PagedListView. - * - * @param decor The decoration to add. - * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration) - */ - public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) { - mRecyclerView.addItemDecoration(decor); - } - - /** - * Removes the given {@link android.support.v7.widget.RecyclerView.ItemDecoration} from this - * PagedListView. - * - *

    The decoration will function the same as the item decoration for a {@link RecyclerView}. - * - * @param decor The decoration to remove. - * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration) - */ - public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) { - mRecyclerView.removeItemDecoration(decor); - } - - /** - * Sets spacing between each item in the list. The spacing will not be added before the first - * item and after the last. - * - * @param itemSpacing the spacing between each item. - */ - public void setItemSpacing(int itemSpacing) { - ItemSpacingDecoration existing = null; - for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) { - RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i); - if (itemDecoration instanceof ItemSpacingDecoration) { - existing = (ItemSpacingDecoration) itemDecoration; - break; - } - } - - if (itemSpacing == 0 && existing != null) { - mRecyclerView.removeItemDecoration(existing); - } else if (existing == null) { - mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing)); - } else { - existing.setItemSpacing(itemSpacing); - } - mRecyclerView.invalidateItemDecorations(); - } - - /** - * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this - * PagedListView. - * - *

    The listener will function the same as the listener for a regular {@link RecyclerView}. - * - * @param touchListener The touch listener to add. - * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener) - */ - public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) { - mRecyclerView.addOnItemTouchListener(touchListener); - } - - /** - * Removes the given {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} from - * the PagedListView. - * - * @param touchListener The touch listener to remove. - * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener) - */ - public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) { - mRecyclerView.removeOnItemTouchListener(touchListener); - } - /** - * Sets how this {@link PagedListView} responds to day/night configuration changes. By - * default, the PagedListView is darker in the day and lighter at night. - * - * @param dayNightStyle A value from {@link DayNightStyle}. - * @see DayNightStyle - */ - public void setDayNightStyle(@DayNightStyle int dayNightStyle) { - // Update the scrollbar - mScrollBarView.setDayNightStyle(dayNightStyle); - - int decorCount = mRecyclerView.getItemDecorationCount(); - for (int i = 0; i < decorCount; i++) { - RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i); - if (decor instanceof DividerDecoration) { - ((DividerDecoration) decor).updateDividerColor(); - } - } - } - - /** - * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the - * last child in the PagedListView that is fully visible. - * - * @return The corresponding ViewHolder or {@code null} if none exists. - */ - @Nullable - public RecyclerView.ViewHolder getLastViewHolder() { - View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild(); - if (lastFullyVisible == null) { - return null; - } - int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible); - RecyclerView.ViewHolder lastViewHolder = getRecyclerView() - .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1); - // We want to get the very last ViewHolder in the list, even if it's only fully visible - // If it doesn't exist, return the last fully visible ViewHolder. - if (lastViewHolder == null) { - lastViewHolder = getRecyclerView() - .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition); - } - return lastViewHolder; - } - - /** - * Sets the {@link OnScrollListener} that will be notified of scroll events within the - * PagedListView. - * - * @param listener The scroll listener to set. - */ - public void setOnScrollListener(OnScrollListener listener) { - mOnScrollListener = listener; - mLayoutManager.setOnScrollListener(mOnScrollListener); - } - - /** Returns the page the given position is on, starting with page 0. */ - public int getPage(int position) { - if (mRowsPerPage == -1) { - return -1; - } - if (mRowsPerPage == 0) { - return 0; - } - return position / mRowsPerPage; - } - - /** - * Sets the default number of pages that this PagedListView is limited to. - * - * @param newDefault The default number of pages. Should be positive. - */ - public void setDefaultMaxPages(int newDefault) { - if (newDefault < 0) { - return; - } - mDefaultMaxPages = newDefault; - resetMaxPages(); - } - - /** Returns the default number of pages the list should have */ - protected int getDefaultMaxPages() { - // assume list shown in response to a click, so, reduce number of clicks by one - return mDefaultMaxPages - 1; - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - // if a late item is added to the top of the layout after the layout is stabilized, causing - // the former top item to be pushed to the 2nd page, the focus will still be on the former - // top item. Since our car layout manager tries to scroll the viewport so that the focused - // item is visible, the view port will be on the 2nd page. That means the newly added item - // will not be visible, on the first page. - - // what we want to do is: if the formerly focused item is the first one in the list, any - // item added above it will make the focus to move to the new first item. - // if the focus is not on the formerly first item, then we don't need to do anything. Let - // the layout manager do the job and scroll the viewport so the currently focused item - // is visible. - - // we need to calculate whether we want to request focus here, before the super call, - // because after the super call, the first born might be changed. - View focusedChild = mLayoutManager.getFocusedChild(); - View firstBorn = mLayoutManager.getChildAt(0); - - super.onLayout(changed, left, top, right, bottom); - - if (mAdapter != null) { - int itemCount = mAdapter.getItemCount(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, String.format( - "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, " - + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, " - + "mNeedsFocus: %s", - hasFocus(), - mLastItemCount, - itemCount, - focusedChild, - firstBorn, - isInTouchMode(), - mNeedsFocus)); - } - updateMaxItems(); - // This is a workaround for missing focus because isInTouchMode() is not always - // returning the right value. - // This is okay for the Engine release since focus is always showing. - // However, in Tala and Fender, we want to show focus only when the user uses - // hardware controllers, so we need to revisit this logic. b/22990605. - if (mNeedsFocus && itemCount > 0) { - if (focusedChild == null) { - requestFocus(); - } - mNeedsFocus = false; - } - if (itemCount > mLastItemCount && focusedChild == firstBorn) { - requestFocus(); - } - mLastItemCount = itemCount; - } - // We need to update the scroll buttons after layout has happened. - // Determining if a scrollbar is necessary requires looking at the layout of the child - // views. Therefore, this determination can only be done after layout has happened. - // Note: don't animate here to prevent b/26849677 - updatePaginationButtons(false /*animate*/); - } - - /** - * Returns the View at the given position within the list. - * - * @param position A position within the list. - * @return The View or {@code null} if no View exists at the given position. - */ - @Nullable - public View findViewByPosition(int position) { - return mLayoutManager.findViewByPosition(position); - } - - /** - * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is - * being called as a result of adapter changes, it should be called after the new layout has - * been calculated because the method of determining scrollbar visibility uses the current - * layout. If this is called after an adapter change but before the new layout, the visibility - * determination may not be correct. - * - * @param animate {@code true} if the scrollbar should animate to its new position. - * {@code false} if no animation is used - */ - protected void updatePaginationButtons(boolean animate) { - if (!mScrollBarEnabled) { - // Don't change the visibility of the ScrollBar unless it's enabled. - return; - } - - if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) - || mLayoutManager.getItemCount() == 0) { - mScrollBarView.setVisibility(View.INVISIBLE); - } else { - mScrollBarView.setVisibility(View.VISIBLE); - } - mScrollBarView.setUpEnabled(shouldEnablePageUpButton()); - mScrollBarView.setDownEnabled(shouldEnablePageDownButton()); - - mScrollBarView.setParameters( - mRecyclerView.computeVerticalScrollRange(), - mRecyclerView.computeVerticalScrollOffset(), - mRecyclerView.computeVerticalScrollExtent(), - animate); - invalidate(); - } - - protected boolean shouldEnablePageUpButton() { - return !mLayoutManager.isAtTop(); - } - - protected boolean shouldEnablePageDownButton() { - return !mLayoutManager.isAtBottom(); - } - - @UiThread - protected void updateMaxItems() { - if (mAdapter == null) { - return; - } - - // Ensure mRowsPerPage regardless of if the adapter implements ItemCap. - updateRowsPerPage(); - - // If the adapter does not implement ItemCap, then the max items on it cannot be updated. - if (!(mAdapter instanceof ItemCap)) { - return; - } - - final int originalCount = mAdapter.getItemCount(); - ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount()); - final int newCount = mAdapter.getItemCount(); - if (newCount == originalCount) { - return; - } - - if (newCount < originalCount) { - mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount); - } else { - mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount); - } - } - - protected int calculateMaxItemCount() { - final View firstChild = mLayoutManager.getChildAt(0); - if (firstChild == null || firstChild.getHeight() == 0) { - return -1; - } else { - return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages; - } - } - - /** - * Updates the rows number per current page, which is used for calculating how many items we - * want to show. - */ - protected void updateRowsPerPage() { - final View firstChild = mLayoutManager.getChildAt(0); - if (firstChild == null || firstChild.getHeight() == 0) { - mRowsPerPage = 1; - } else { - mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight()); - } - } - - @Override - protected Parcelable onSaveInstanceState() { - SavedState savedState = new SavedState(super.onSaveInstanceState()); - savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState(); - return savedState; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState savedState = (SavedState) state; - mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState); - super.onRestoreInstanceState(savedState.getSuperState()); - } - - @Override - protected void dispatchSaveInstanceState(SparseArray container) { - // There is the possibility of multiple PagedListViews on a page. This means that the ids - // of the child Views of PagedListView are no longer unique, and onSaveInstanceState() - // cannot be used. As a result, PagedListViews needs to manually dispatch the instance - // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState() - // called by the system. - dispatchFreezeSelfOnly(container); - } - - @Override - protected void dispatchRestoreInstanceState(SparseArray container) { - // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView - // will manually handle passing the state. See the comment in dispatchSaveInstanceState() - // for more information. - dispatchThawSelfOnly(container); - } - - /** The state that will be saved across configuration changes. */ - private static class SavedState extends BaseSavedState { - /** The state of the {@link #mLayoutManager} of this PagedListView. */ - Parcelable mLayoutManagerState; - - SavedState(Parcelable superState) { - super(superState); - } - - private SavedState(Parcel in) { - super(in); - mLayoutManagerState = - in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader()); - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeParcelable(mLayoutManagerState, flags); - } - - public static final ClassLoaderCreator CREATOR = - new ClassLoaderCreator() { - @Override - public SavedState createFromParcel(Parcel source, ClassLoader loader) { - return new SavedState(source); - } - - @Override - public SavedState createFromParcel(Parcel source) { - return createFromParcel(source, null /* loader */); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener = - new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - if (mOnScrollListener != null) { - mOnScrollListener.onScrolled(recyclerView, dx, dy); - if (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) { - mOnScrollListener.onReachBottom(); - } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) { - mOnScrollListener.onLeaveBottom(); - } - } - updatePaginationButtons(false); - } - - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - if (mOnScrollListener != null) { - mOnScrollListener.onScrollStateChanged(recyclerView, newState); - } - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS); - } - } - }; - - protected final Runnable mPaginationRunnable = - new Runnable() { - @Override - public void run() { - boolean upPressed = mScrollBarView.isUpPressed(); - boolean downPressed = mScrollBarView.isDownPressed(); - if (upPressed && downPressed) { - return; - } - if (upPressed) { - mRecyclerView.pageUp(); - } else if (downPressed) { - mRecyclerView.pageDown(); - } - } - }; - - private final Runnable mUpdatePaginationRunnable = - new Runnable() { - @Override - public void run() { - updatePaginationButtons(true /*animate*/); - } - }; - - /** Used to listen for {@code PagedListView} scroll events. */ - public abstract static class OnScrollListener { - /** Called when menu reaches the bottom */ - public void onReachBottom() {} - /** Called when menu leaves the bottom */ - public void onLeaveBottom() {} - /** Called when scroll up button is clicked */ - public void onScrollUpButtonClicked() {} - /** Called when scroll down button is clicked */ - public void onScrollDownButtonClicked() {} - /** Called when scrolling to the previous page via up gesture */ - public void onGestureUp() {} - /** Called when scrolling to the next page via down gesture */ - public void onGestureDown() {} - - /** - * Called when RecyclerView.OnScrollListener#onScrolled is called. See - * RecyclerView.OnScrollListener - */ - public void onScrolled(RecyclerView recyclerView, int dx, int dy) {} - - /** See RecyclerView.OnScrollListener */ - public void onScrollStateChanged(RecyclerView recyclerView, int newState) {} - - /** Called when the view scrolls up a page */ - public void onPageUp() {} - - /** Called when the view scrolls down a page */ - public void onPageDown() {} - } - - /** - * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will add spacing - * between each item in the RecyclerView that it is added to. - */ - private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { - - private int mHalfItemSpacing; - - private ItemSpacingDecoration(int itemSpacing) { - mHalfItemSpacing = itemSpacing / 2; - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - // Skip top offset for first item and bottom offset for last. - int position = parent.getChildAdapterPosition(view); - if (position > 0) { - outRect.top = mHalfItemSpacing; - } - if (position < state.getItemCount() - 1) { - outRect.bottom = mHalfItemSpacing; - } - } - - /** - * @param itemSpacing sets spacing between each item. - */ - public void setItemSpacing(int itemSpacing) { - mHalfItemSpacing = itemSpacing / 2; - } - } - - /** - * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing - * line between each item in the RecyclerView that it is added to. - */ - private static class DividerDecoration extends RecyclerView.ItemDecoration { - private final Context mContext; - private final Paint mPaint; - private final int mDividerHeight; - private final int mDividerStartMargin; - @IdRes private final int mDividerStartId; - @IdRes private final int mDividerEndId; - - /** - * @param dividerStartMargin The start offset of the dividing line. This offset will be - * relative to {@code dividerStartId} if that value is given. - * @param dividerStartId A child view id whose starting edge will be used as the starting - * edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top - * container of each child view will be used. - * @param dividerEndId A child view id whose ending edge will be used as the starting edge - * of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top - * container view of each child will be used. - */ - private DividerDecoration(Context context, int dividerStartMargin, - @IdRes int dividerStartId, @IdRes int dividerEndId) { - mContext = context; - mDividerStartMargin = dividerStartMargin; - mDividerStartId = dividerStartId; - mDividerEndId = dividerEndId; - - Resources res = context.getResources(); - mPaint = new Paint(); - mPaint.setColor(res.getColor(R.color.car_list_divider)); - mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height); - } - - /** Updates the list divider color which may have changed due to a day night transition. */ - public void updateDividerColor() { - mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider)); - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - // Draw a divider line between each item. No need to draw the line for the last item. - for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) { - View container = parent.getChildAt(i); - View nextContainer = parent.getChildAt(i + 1); - int spacing = nextContainer.getTop() - container.getBottom(); - - View startChild = - mDividerStartId != INVALID_RESOURCE_ID - ? container.findViewById(mDividerStartId) - : container; - - View endChild = - mDividerEndId != INVALID_RESOURCE_ID - ? container.findViewById(mDividerEndId) - : container; - - if (startChild == null || endChild == null) { - continue; - } - - int left = mDividerStartMargin + startChild.getLeft(); - int right = endChild.getRight(); - int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2; - int top = bottom - mDividerHeight; - - c.drawRect(left, top, right, bottom, mPaint); - } - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - // Skip top offset for first item and bottom offset for last. - int position = parent.getChildAdapterPosition(view); - if (position > 0) { - outRect.top = mDividerHeight / 2; - } - if (position < state.getItemCount() - 1) { - outRect.bottom = mDividerHeight / 2; - } - } - } -} diff --git a/android/support/car/widget/PagedScrollBarView.java b/android/support/car/widget/PagedScrollBarView.java deleted file mode 100644 index 1c46b5d4..00000000 --- a/android/support/car/widget/PagedScrollBarView.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.car.widget; - -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.support.car.R; -import android.support.v4.content.ContextCompat; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; -import android.widget.ImageView; - -/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */ -public class PagedScrollBarView extends FrameLayout - implements View.OnClickListener, View.OnLongClickListener { - private static final float BUTTON_DISABLED_ALPHA = 0.2f; - - @DayNightStyle private int mDayNightStyle; - - /** Listener for when the list should paginate. */ - public interface PaginationListener { - int PAGE_UP = 0; - int PAGE_DOWN = 1; - - /** Called when the linked view should be paged in the given direction */ - void onPaginate(int direction); - } - - private final ImageView mUpButton; - private final ImageView mDownButton; - private final ImageView mScrollThumb; - /** The "filler" view between the up and down buttons */ - private final View mFiller; - - private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator(); - private final int mMinThumbLength; - private final int mMaxThumbLength; - private PaginationListener mPaginationListener; - - public PagedScrollBarView(Context context, AttributeSet attrs) { - this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/); - } - - public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) { - this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/); - } - - public PagedScrollBarView( - Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { - super(context, attrs, defStyleAttrs, defStyleRes); - - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.car_paged_scrollbar_buttons, this /* root */, - true /* attachToRoot */); - - mUpButton = (ImageView) findViewById(R.id.page_up); - mUpButton.setOnClickListener(this); - mUpButton.setOnLongClickListener(this); - mDownButton = (ImageView) findViewById(R.id.page_down); - mDownButton.setOnClickListener(this); - mDownButton.setOnLongClickListener(this); - - mScrollThumb = (ImageView) findViewById(R.id.scrollbar_thumb); - mFiller = findViewById(R.id.filler); - - mMinThumbLength = getResources().getDimensionPixelSize(R.dimen.min_thumb_height); - mMaxThumbLength = getResources().getDimensionPixelSize(R.dimen.max_thumb_height); - } - - @Override - public void onClick(View v) { - dispatchPageClick(v); - } - - @Override - public boolean onLongClick(View v) { - dispatchPageClick(v); - return true; - } - - /** Sets the icon to be used for the up button. */ - public void setUpButtonIcon(Drawable icon) { - mUpButton.setImageDrawable(icon); - } - - /** Sets the icon to be used for the down button. */ - public void setDownButtonIcon(Drawable icon) { - mDownButton.setImageDrawable(icon); - } - - /** - * Sets the listener that will be notified when the up and down buttons have been pressed. - * - * @param listener The listener to set. - */ - public void setPaginationListener(PaginationListener listener) { - mPaginationListener = listener; - } - - /** Returns {@code true} if the "up" button is pressed */ - public boolean isUpPressed() { - return mUpButton.isPressed(); - } - - /** Returns {@code true} if the "down" button is pressed */ - public boolean isDownPressed() { - return mDownButton.isPressed(); - } - - /** Sets the range, offset and extent of the scroll bar. See {@link View}. */ - public void setParameters(int range, int offset, int extent, boolean animate) { - // This method is where we take the computed parameters from the PagedLayoutManager and - // render it within the specified constraints ({@link #mMaxThumbLength} and - // {@link #mMinThumbLength}). - final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom(); - - int thumbLength = extent * size / range; - thumbLength = Math.max(Math.min(thumbLength, mMaxThumbLength), mMinThumbLength); - - int thumbOffset = size - thumbLength; - if (isDownEnabled()) { - // We need to adjust the offset so that it fits into the possible space inside the - // filler with regarding to the constraints set by mMaxThumbLength and mMinThumbLength. - thumbOffset = (size - thumbLength) * offset / range; - } - - // Sets the size of the thumb and request a redraw if needed. - final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams(); - if (lp.height != thumbLength) { - lp.height = thumbLength; - mScrollThumb.requestLayout(); - } - - moveY(mScrollThumb, thumbOffset, animate); - } - - /** - * Sets how this {@link PagedScrollBarView} responds to day/night configuration changes. By - * default, the PagedScrollBarView is darker in the day and lighter at night. - * - * @param dayNightStyle A value from {@link DayNightStyle}. - * @see DayNightStyle - */ - public void setDayNightStyle(@DayNightStyle int dayNightStyle) { - mDayNightStyle = dayNightStyle; - reloadColors(); - } - - /** - * Sets whether or not the up button on the scroll bar is clickable. - * - * @param enabled {@code true} if the up button is enabled. - */ - public void setUpEnabled(boolean enabled) { - mUpButton.setEnabled(enabled); - mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); - } - - /** - * Sets whether or not the down button on the scroll bar is clickable. - * - * @param enabled {@code true} if the down button is enabled. - */ - public void setDownEnabled(boolean enabled) { - mDownButton.setEnabled(enabled); - mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); - } - - /** - * Returns whether or not the down button on the scroll bar is clickable. - * - * @return {@code true} if the down button is enabled. {@code false} otherwise. - */ - public boolean isDownEnabled() { - return mDownButton.isEnabled(); - } - - /** Reload the colors for the current {@link DayNightStyle}. */ - private void reloadColors() { - int tint; - int thumbBackground; - int upDownBackgroundResId; - - switch (mDayNightStyle) { - case DayNightStyle.AUTO: - tint = ContextCompat.getColor(getContext(), R.color.car_tint); - thumbBackground = ContextCompat.getColor(getContext(), - R.color.car_scrollbar_thumb); - upDownBackgroundResId = R.drawable.car_pagination_background; - break; - case DayNightStyle.AUTO_INVERSE: - tint = ContextCompat.getColor(getContext(), R.color.car_tint_inverse); - thumbBackground = ContextCompat.getColor(getContext(), - R.color.car_scrollbar_thumb_inverse); - upDownBackgroundResId = R.drawable.car_pagination_background_inverse; - break; - case DayNightStyle.FORCE_NIGHT: - tint = ContextCompat.getColor(getContext(), R.color.car_tint_light); - thumbBackground = ContextCompat.getColor(getContext(), - R.color.car_scrollbar_thumb_light); - upDownBackgroundResId = R.drawable.car_pagination_background_night; - break; - case DayNightStyle.FORCE_DAY: - tint = ContextCompat.getColor(getContext(), R.color.car_tint_dark); - thumbBackground = ContextCompat.getColor(getContext(), - R.color.car_scrollbar_thumb_dark); - upDownBackgroundResId = R.drawable.car_pagination_background_day; - break; - default: - throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle); - } - - mScrollThumb.setBackgroundColor(thumbBackground); - - mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); - mUpButton.setBackgroundResource(upDownBackgroundResId); - - mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); - mDownButton.setBackgroundResource(upDownBackgroundResId); - } - - private void dispatchPageClick(View v) { - final PaginationListener listener = mPaginationListener; - if (listener == null) { - return; - } - - int direction = v.getId() == R.id.page_up - ? PaginationListener.PAGE_UP - : PaginationListener.PAGE_DOWN; - listener.onPaginate(direction); - } - - /** Moves the given view to the specified 'y' position. */ - private void moveY(final View view, float newPosition, boolean animate) { - final int duration = animate ? 200 : 0; - view.animate() - .y(newPosition) - .setDuration(duration) - .setInterpolator(mPaginationInterpolator) - .start(); - } -} diff --git a/android/support/checkapi/UpdateApiTask.java b/android/support/checkapi/UpdateApiTask.java index de2db919..15e91040 100644 --- a/android/support/checkapi/UpdateApiTask.java +++ b/android/support/checkapi/UpdateApiTask.java @@ -29,7 +29,6 @@ import java.io.BufferedWriter; import java.io.File; import java.nio.charset.Charset; import java.util.HashSet; -import java.util.List; import java.util.Set; /** @@ -120,11 +119,6 @@ public class UpdateApiTask extends DefaultTask { } if (mWhitelistErrorsFile != null && !mWhitelistErrors.isEmpty()) { - if (mWhitelistErrorsFile.exists()) { - List lines = - Files.readLines(mWhitelistErrorsFile, Charset.defaultCharset()); - mWhitelistErrors.removeAll(lines); - } try (BufferedWriter writer = Files.newWriter( mWhitelistErrorsFile, Charset.defaultCharset())) { for (String error : mWhitelistErrors) { diff --git a/android/support/design/widget/CoordinatorLayout.java b/android/support/design/widget/CoordinatorLayout.java index c45810ef..03cce024 100644 --- a/android/support/design/widget/CoordinatorLayout.java +++ b/android/support/design/widget/CoordinatorLayout.java @@ -400,6 +400,7 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.resetTouchBehaviorTracking(); } + mBehaviorTouchView = null; mDisallowInterceptReset = false; } diff --git a/android/support/graphics/drawable/VectorDrawableCompat.java b/android/support/graphics/drawable/VectorDrawableCompat.java index a34fe2b8..943f1aa9 100644 --- a/android/support/graphics/drawable/VectorDrawableCompat.java +++ b/android/support/graphics/drawable/VectorDrawableCompat.java @@ -56,8 +56,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Stack; /** * For API 24 and above, this class is delegating to the framework's {@link VectorDrawable}. @@ -730,7 +730,7 @@ public class VectorDrawableCompat extends VectorDrawableCommon { // Use a stack to help to build the group tree. // The top of the stack is always the current group. - final Stack groupStack = new Stack(); + final ArrayDeque groupStack = new ArrayDeque<>(); groupStack.push(pathRenderer.mRootGroup); int eventType = parser.getEventType(); @@ -785,14 +785,7 @@ public class VectorDrawableCompat extends VectorDrawableCommon { } if (noPathTag) { - final StringBuffer tag = new StringBuffer(); - - if (tag.length() > 0) { - tag.append(" or "); - } - tag.append(SHAPE_PATH); - - throw new XmlPullParserException("no " + tag + " defined"); + throw new XmlPullParserException("no " + SHAPE_PATH + " defined"); } } diff --git a/android/support/media/ExifInterface.java b/android/support/media/ExifInterface.java index 72b61cb7..eea69ab1 100644 --- a/android/support/media/ExifInterface.java +++ b/android/support/media/ExifInterface.java @@ -4678,9 +4678,7 @@ public class ExifInterface { private int getMimeType(BufferedInputStream in) throws IOException { in.mark(SIGNATURE_CHECK_SIZE); byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE]; - if (in.read(signatureCheckBytes) != SIGNATURE_CHECK_SIZE) { - throw new EOFException(); - } + in.read(signatureCheckBytes); in.reset(); if (isJpegFormat(signatureCheckBytes)) { return IMAGE_TYPE_JPEG; @@ -5333,7 +5331,7 @@ public class ExifInterface { int dataFormat = dataInputStream.readUnsignedShort(); int numberOfComponents = dataInputStream.readInt(); // Next four bytes is for data offset or value. - long nextEntryOffset = dataInputStream.peek() + 4; + long nextEntryOffset = dataInputStream.peek() + 4L; // Look up a corresponding tag from tag number ExifTag tag = (ExifTag) sExifTagMapsForReading[ifdType].get(tagNumber); diff --git a/android/support/media/ExifInterfaceTest.java b/android/support/media/ExifInterfaceTest.java new file mode 100644 index 00000000..f811d1a7 --- /dev/null +++ b/android/support/media/ExifInterfaceTest.java @@ -0,0 +1,898 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.media; + +import static android.support.test.InstrumentationRegistry.getContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.location.Location; +import android.os.Environment; +import android.support.exifinterface.test.R; +import android.support.test.filters.LargeTest; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; +import android.util.Pair; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Test {@link ExifInterface}. + */ +@RunWith(AndroidJUnit4.class) +public class ExifInterfaceTest { + private static final String TAG = ExifInterface.class.getSimpleName(); + private static final boolean VERBOSE = false; // lots of logging + private static final double DIFFERENCE_TOLERANCE = .001; + + private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg"; + private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg"; + private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng"; + private static final int[] IMAGE_RESOURCES = new int[] { + R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800}; + private static final String[] IMAGE_FILENAMES = new String[] { + EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG}; + + private static final String TEST_TEMP_FILE_NAME = "testImage"; + private static final double DELTA = 1e-8; + // We translate double to rational in a 1/10000 precision. + private static final double RATIONAL_DELTA = 0.0001; + private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8; + private static final int TEST_NUMBER_OF_CORRUPTED_IMAGE_STREAMS = 30; + private static final double[] TEST_LATITUDE_VALID_VALUES = new double[] + {0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737}; + private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[] + {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352}; + private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[] + {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001, + 263.34763236326, -1e5, 347.32525, -176.346347754}; + private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[] + {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001, + 263.34763236326, -1e10, 347.325252623, -4000.346323236}; + private static final double[] TEST_ALTITUDE_VALUES = new double[] + {0, -2000, 10000, -355.99999999999, 18.02038}; + private static final int[][] TEST_ROTATION_STATE_MACHINE = { + {ExifInterface.ORIENTATION_UNDEFINED, -90, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_UNDEFINED, 0, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_UNDEFINED, 90, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_UNDEFINED, 180, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_UNDEFINED, 270, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_UNDEFINED, 540, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_NORMAL, -90, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_NORMAL, 0, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_NORMAL, 90, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_NORMAL, 180, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_NORMAL, 270, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_NORMAL, 540, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_ROTATE_90, -90, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_ROTATE_90, 0, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_ROTATE_90, 90, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_ROTATE_90, 180 , ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_ROTATE_90, 270, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_ROTATE_90, 540, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_ROTATE_180, -90, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_ROTATE_180, 0, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_ROTATE_180, 90, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_ROTATE_180, 180, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_ROTATE_180, 270, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_ROTATE_180, 540, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_ROTATE_270, -90, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_ROTATE_270, 0, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_ROTATE_270, 90, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_ROTATE_270, 180, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_ROTATE_270, 270, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_ROTATE_270, 540, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, -90, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, 0, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, 90, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, 180, + ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, 270, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, 540, + ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, -90, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 0, + ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 90, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 180, + ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 270, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 540, + ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_TRANSPOSE, -90, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_TRANSPOSE, 0, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_TRANSPOSE, 90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_TRANSPOSE, 180, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_TRANSPOSE, 270, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_TRANSPOSE, 540, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_TRANSVERSE, -90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_TRANSVERSE, 0, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_TRANSVERSE, 90, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_TRANSVERSE, 180, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_TRANSVERSE, 270, ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_TRANSVERSE, 540, ExifInterface.ORIENTATION_TRANSPOSE}, + }; + private static final int[][] TEST_FLIP_VERTICALLY_STATE_MACHINE = { + {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_270}, + {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_90} + }; + private static final int[][] TEST_FLIP_HORIZONTALLY_STATE_MACHINE = { + {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED}, + {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_HORIZONTAL}, + {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSPOSE}, + {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL}, + {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSVERSE}, + {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_ROTATE_180}, + {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_NORMAL}, + {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_90}, + {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_270} + }; + private static final HashMap FLIP_STATE_AND_ROTATION_DEGREES = new HashMap<>(); + static { + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_UNDEFINED, new Pair(false, 0)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_NORMAL, new Pair(false, 0)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_ROTATE_90, new Pair(false, 90)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_ROTATE_180, new Pair(false, 180)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_ROTATE_270, new Pair(false, 270)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_FLIP_HORIZONTAL, new Pair(true, 0)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_TRANSVERSE, new Pair(true, 90)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_FLIP_VERTICAL, new Pair(true, 180)); + FLIP_STATE_AND_ROTATION_DEGREES.put( + ExifInterface.ORIENTATION_TRANSPOSE, new Pair(true, 270)); + } + + private static final String[] EXIF_TAGS = { + ExifInterface.TAG_MAKE, + ExifInterface.TAG_MODEL, + ExifInterface.TAG_F_NUMBER, + ExifInterface.TAG_DATETIME_ORIGINAL, + ExifInterface.TAG_EXPOSURE_TIME, + ExifInterface.TAG_FLASH, + ExifInterface.TAG_FOCAL_LENGTH, + ExifInterface.TAG_GPS_ALTITUDE, + ExifInterface.TAG_GPS_ALTITUDE_REF, + ExifInterface.TAG_GPS_DATESTAMP, + ExifInterface.TAG_GPS_LATITUDE, + ExifInterface.TAG_GPS_LATITUDE_REF, + ExifInterface.TAG_GPS_LONGITUDE, + ExifInterface.TAG_GPS_LONGITUDE_REF, + ExifInterface.TAG_GPS_PROCESSING_METHOD, + ExifInterface.TAG_GPS_TIMESTAMP, + ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_IMAGE_WIDTH, + ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, + ExifInterface.TAG_ORIENTATION, + ExifInterface.TAG_WHITE_BALANCE + }; + + private static class ExpectedValue { + // Thumbnail information. + public final boolean hasThumbnail; + public final int thumbnailWidth; + public final int thumbnailHeight; + + // GPS information. + public final boolean hasLatLong; + public final float latitude; + public final float longitude; + public final float altitude; + + // Values. + public final String make; + public final String model; + public final float aperture; + public final String dateTimeOriginal; + public final float exposureTime; + public final float flash; + public final String focalLength; + public final String gpsAltitude; + public final String gpsAltitudeRef; + public final String gpsDatestamp; + public final String gpsLatitude; + public final String gpsLatitudeRef; + public final String gpsLongitude; + public final String gpsLongitudeRef; + public final String gpsProcessingMethod; + public final String gpsTimestamp; + public final int imageLength; + public final int imageWidth; + public final String iso; + public final int orientation; + public final int whiteBalance; + + private static String getString(TypedArray typedArray, int index) { + String stringValue = typedArray.getString(index); + if (stringValue == null || stringValue.equals("")) { + return null; + } + return stringValue.trim(); + } + + ExpectedValue(TypedArray typedArray) { + // Reads thumbnail information. + hasThumbnail = typedArray.getBoolean(0, false); + thumbnailWidth = typedArray.getInt(1, 0); + thumbnailHeight = typedArray.getInt(2, 0); + + // Reads GPS information. + hasLatLong = typedArray.getBoolean(3, false); + latitude = typedArray.getFloat(4, 0f); + longitude = typedArray.getFloat(5, 0f); + altitude = typedArray.getFloat(6, 0f); + + // Reads values. + make = getString(typedArray, 7); + model = getString(typedArray, 8); + aperture = typedArray.getFloat(9, 0f); + dateTimeOriginal = getString(typedArray, 10); + exposureTime = typedArray.getFloat(11, 0f); + flash = typedArray.getFloat(12, 0f); + focalLength = getString(typedArray, 13); + gpsAltitude = getString(typedArray, 14); + gpsAltitudeRef = getString(typedArray, 15); + gpsDatestamp = getString(typedArray, 16); + gpsLatitude = getString(typedArray, 17); + gpsLatitudeRef = getString(typedArray, 18); + gpsLongitude = getString(typedArray, 19); + gpsLongitudeRef = getString(typedArray, 20); + gpsProcessingMethod = getString(typedArray, 21); + gpsTimestamp = getString(typedArray, 22); + imageLength = typedArray.getInt(23, 0); + imageWidth = typedArray.getInt(24, 0); + iso = getString(typedArray, 25); + orientation = typedArray.getInt(26, 0); + whiteBalance = typedArray.getInt(27, 0); + + typedArray.recycle(); + } + } + + @Before + public void setUp() throws Exception { + for (int i = 0; i < IMAGE_RESOURCES.length; ++i) { + String outputPath = + new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i]) + .getAbsolutePath(); + + InputStream inputStream = null; + FileOutputStream outputStream = null; + try { + inputStream = getContext().getResources().openRawResource(IMAGE_RESOURCES[i]); + outputStream = new FileOutputStream(outputPath); + copy(inputStream, outputStream); + } finally { + closeQuietly(inputStream); + closeQuietly(outputStream); + } + } + } + + @After + public void tearDown() throws Exception { + for (int i = 0; i < IMAGE_RESOURCES.length; ++i) { + String imageFilePath = + new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i]) + .getAbsolutePath(); + File imageFile = new File(imageFilePath); + if (imageFile.exists()) { + imageFile.delete(); + } + } + } + + @Test + @LargeTest + public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable { + testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg); + } + + @Test + @LargeTest + public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable { + testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg); + } + + @Test + @LargeTest + public void testReadExifDataFromLgG4Iso800Dng() throws Throwable { + testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng); + } + + @Test + @LargeTest + public void testDoNotFailOnCorruptedImage() throws Throwable { + // ExifInterface shouldn't raise any exceptions except an IOException when unable to open + // a file, even with a corrupted image. Generates randomly corrupted image stream for + // testing. Uses Epoch date count as random seed so that we can reproduce a broken test. + long seed = System.currentTimeMillis() / (86400 * 1000); + Log.d(TAG, "testDoNotFailOnCorruptedImage random seed: " + seed); + Random random = new Random(seed); + byte[] bytes = new byte[8096]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + for (int i = 0; i < TEST_NUMBER_OF_CORRUPTED_IMAGE_STREAMS; i++) { + buffer.clear(); + random.nextBytes(bytes); + if (!randomlyCorrupted(random)) { + buffer.put(ExifInterface.JPEG_SIGNATURE); + } + if (!randomlyCorrupted(random)) { + buffer.put(ExifInterface.MARKER_APP1); + } + buffer.putShort((short) (random.nextInt(100) + 300)); + if (!randomlyCorrupted(random)) { + buffer.put(ExifInterface.IDENTIFIER_EXIF_APP1); + } + if (!randomlyCorrupted(random)) { + buffer.putShort(ExifInterface.BYTE_ALIGN_MM); + } + if (!randomlyCorrupted(random)) { + buffer.put((byte) 0); + buffer.put(ExifInterface.START_CODE); + } + buffer.putInt(8); + + // Primary Tags + int numberOfDirectory = random.nextInt(8) + 1; + if (!randomlyCorrupted(random)) { + buffer.putShort((short) numberOfDirectory); + } + for (int j = 0; j < numberOfDirectory; j++) { + generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_PRIMARY, random); + } + if (!randomlyCorrupted(random)) { + buffer.putInt(buffer.position() - 8); + } + + // Thumbnail Tags + numberOfDirectory = random.nextInt(8) + 1; + if (!randomlyCorrupted(random)) { + buffer.putShort((short) numberOfDirectory); + } + for (int j = 0; j < numberOfDirectory; j++) { + generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_THUMBNAIL, random); + } + if (!randomlyCorrupted(random)) { + buffer.putInt(buffer.position() - 8); + } + + // Preview Tags + numberOfDirectory = random.nextInt(8) + 1; + if (!randomlyCorrupted(random)) { + buffer.putShort((short) numberOfDirectory); + } + for (int j = 0; j < numberOfDirectory; j++) { + generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_PREVIEW, random); + } + if (!randomlyCorrupted(random)) { + buffer.putInt(buffer.position() - 8); + } + + if (!randomlyCorrupted(random)) { + buffer.put(ExifInterface.MARKER); + } + if (!randomlyCorrupted(random)) { + buffer.put(ExifInterface.MARKER_EOI); + } + + try { + new ExifInterface(new ByteArrayInputStream(bytes)); + // Always success + } catch (IOException e) { + fail("Should not reach here!"); + } + } + } + + @Test + @SmallTest + public void testSetGpsInfo() throws IOException { + final String provider = "ExifInterfaceTest"; + final long timestamp = System.currentTimeMillis(); + final float speedInMeterPerSec = 36.627533f; + Location location = new Location(provider); + location.setLatitude(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1]); + location.setLongitude(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1]); + location.setAltitude(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1]); + location.setSpeed(speedInMeterPerSec); + location.setTime(timestamp); + ExifInterface exif = createTestExifInterface(); + exif.setGpsInfo(location); + + double[] latLong = exif.getLatLong(); + assertNotNull(latLong); + assertEquals(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1], + latLong[0], DELTA); + assertEquals(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1], + latLong[1], DELTA); + assertEquals(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1], exif.getAltitude(0), + RATIONAL_DELTA); + assertEquals("K", exif.getAttribute(ExifInterface.TAG_GPS_SPEED_REF)); + assertEquals(speedInMeterPerSec, exif.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0.0) + * 1000 / TimeUnit.HOURS.toSeconds(1), RATIONAL_DELTA); + assertEquals(provider, exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD)); + // GPS time's precision is secs. + assertEquals(TimeUnit.MILLISECONDS.toSeconds(timestamp), + TimeUnit.MILLISECONDS.toSeconds(exif.getGpsDateTime())); + } + + @Test + @SmallTest + public void testSetLatLong_withValidValues() throws IOException { + for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { + ExifInterface exif = createTestExifInterface(); + exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]); + + double[] latLong = exif.getLatLong(); + assertNotNull(latLong); + assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA); + assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA); + } + } + + @Test + @SmallTest + public void testSetLatLong_withInvalidLatitude() throws IOException { + for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { + ExifInterface exif = createTestExifInterface(); + try { + exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + assertNull(exif.getLatLong()); + assertLatLongValuesAreNotSet(exif); + } + } + + @Test + @SmallTest + public void testSetLatLong_withInvalidLongitude() throws IOException { + for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) { + ExifInterface exif = createTestExifInterface(); + try { + exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + assertNull(exif.getLatLong()); + assertLatLongValuesAreNotSet(exif); + } + } + + @Test + @SmallTest + public void testSetAltitude() throws IOException { + for (int i = 0; i < TEST_ALTITUDE_VALUES.length; i++) { + ExifInterface exif = createTestExifInterface(); + exif.setAltitude(TEST_ALTITUDE_VALUES[i]); + assertEquals(TEST_ALTITUDE_VALUES[i], exif.getAltitude(Double.NaN), RATIONAL_DELTA); + } + } + + @Test + @SmallTest + public void testSetDateTime() throws IOException { + final String dateTimeValue = "2017:02:02 22:22:22"; + final String dateTimeOriginalValue = "2017:01:01 11:11:11"; + + File imageFile = new File( + Environment.getExternalStorageDirectory(), EXIF_BYTE_ORDER_II_JPEG); + ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath()); + exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue); + exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue); + exif.saveAttributes(); + + // Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value. + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME)); + assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); + + // Now remove the DATETIME value. + exif.setAttribute(ExifInterface.TAG_DATETIME, null); + exif.saveAttributes(); + + // When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value. + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME)); + + long currentTimeStamp = System.currentTimeMillis(); + exif.setDateTime(currentTimeStamp); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertEquals(currentTimeStamp, exif.getDateTime()); + } + + @Test + @SmallTest + public void testRotation() throws IOException { + File imageFile = new File( + Environment.getExternalStorageDirectory(), EXIF_BYTE_ORDER_II_JPEG); + ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath()); + + int num; + // Test flip vertically. + for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + Integer.toString(TEST_FLIP_VERTICALLY_STATE_MACHINE[num][0])); + exif.flipVertically(); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertIntTag(exif, ExifInterface.TAG_ORIENTATION, + TEST_FLIP_VERTICALLY_STATE_MACHINE[num][1]); + + } + + // Test flip horizontally. + for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + Integer.toString(TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][0])); + exif.flipHorizontally(); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertIntTag(exif, ExifInterface.TAG_ORIENTATION, + TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][1]); + + } + + // Test rotate by degrees + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + Integer.toString(ExifInterface.ORIENTATION_NORMAL)); + try { + exif.rotate(108); + fail("Rotate with 108 degree should throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Success + } + + for (num = 0; num < TEST_ROTATION_STATE_MACHINE.length; num++) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + Integer.toString(TEST_ROTATION_STATE_MACHINE[num][0])); + exif.rotate(TEST_ROTATION_STATE_MACHINE[num][1]); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertIntTag(exif, ExifInterface.TAG_ORIENTATION, TEST_ROTATION_STATE_MACHINE[num][2]); + } + + // Test get flip state and rotation degrees. + for (Integer key : FLIP_STATE_AND_ROTATION_DEGREES.keySet()) { + exif.setAttribute(ExifInterface.TAG_ORIENTATION, key.toString()); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertEquals(FLIP_STATE_AND_ROTATION_DEGREES.get(key).first, exif.isFlipped()); + assertEquals(FLIP_STATE_AND_ROTATION_DEGREES.get(key).second, + exif.getRotationDegrees()); + } + + // Test reset the rotation. + exif.setAttribute(ExifInterface.TAG_ORIENTATION, + Integer.toString(ExifInterface.ORIENTATION_FLIP_HORIZONTAL)); + exif.resetOrientation(); + exif.saveAttributes(); + exif = new ExifInterface(imageFile.getAbsolutePath()); + assertIntTag(exif, ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + + } + + @Test + @SmallTest + public void testInterchangeabilityBetweenTwoIsoSpeedTags() throws IOException { + // Tests that two tags TAG_ISO_SPEED_RATINGS and TAG_PHOTOGRAPHIC_SENSITIVITY can be used + // interchangeably. + final String oldTag = ExifInterface.TAG_ISO_SPEED_RATINGS; + final String newTag = ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY; + final String isoValue = "50"; + + ExifInterface exif = createTestExifInterface(); + exif.setAttribute(oldTag, isoValue); + assertEquals(isoValue, exif.getAttribute(oldTag)); + assertEquals(isoValue, exif.getAttribute(newTag)); + + exif = createTestExifInterface(); + exif.setAttribute(newTag, isoValue); + assertEquals(isoValue, exif.getAttribute(oldTag)); + assertEquals(isoValue, exif.getAttribute(newTag)); + } + + private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) { + // Prints thumbnail information. + if (exifInterface.hasThumbnail()) { + byte[] thumbnailBytes = exifInterface.getThumbnailBytes(); + if (thumbnailBytes != null) { + Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length); + Bitmap bitmap = exifInterface.getThumbnailBitmap(); + if (bitmap == null) { + Log.e(TAG, fileName + " Corrupted thumbnail!"); + } else { + Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", " + + bitmap.getHeight()); + } + } else { + Log.e(TAG, fileName + " Unexpected result: No thumbnails were found. " + + "A thumbnail is expected."); + } + } else { + if (exifInterface.getThumbnailBytes() != null) { + Log.e(TAG, fileName + " Unexpected result: A thumbnail was found. " + + "No thumbnail is expected."); + } else { + Log.v(TAG, fileName + " No thumbnail"); + } + } + + // Prints GPS information. + Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0)); + + double[] latLong = exifInterface.getLatLong(); + if (latLong != null) { + Log.v(TAG, fileName + " Latitude = " + latLong[0]); + Log.v(TAG, fileName + " Longitude = " + latLong[1]); + } else { + Log.v(TAG, fileName + " No latlong data"); + } + + // Prints values. + for (String tagKey : EXIF_TAGS) { + String tagValue = exifInterface.getAttribute(tagKey); + Log.v(TAG, fileName + " Key{" + tagKey + "} = '" + tagValue + "'"); + } + } + + private void assertIntTag(ExifInterface exifInterface, String tag, int expectedValue) { + int intValue = exifInterface.getAttributeInt(tag, 0); + assertEquals(expectedValue, intValue); + } + + private void assertFloatTag(ExifInterface exifInterface, String tag, float expectedValue) { + double doubleValue = exifInterface.getAttributeDouble(tag, 0.0); + assertEquals(expectedValue, doubleValue, DIFFERENCE_TOLERANCE); + } + + private void assertStringTag(ExifInterface exifInterface, String tag, String expectedValue) { + String stringValue = exifInterface.getAttribute(tag); + if (stringValue != null) { + stringValue = stringValue.trim(); + } + stringValue = ("".equals(stringValue)) ? null : stringValue; + + assertEquals(expectedValue, stringValue); + } + + private void compareWithExpectedValue(ExifInterface exifInterface, + ExpectedValue expectedValue, String verboseTag) { + if (VERBOSE) { + printExifTagsAndValues(verboseTag, exifInterface); + } + // Checks a thumbnail image. + assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail()); + if (expectedValue.hasThumbnail) { + byte[] thumbnailBytes = exifInterface.getThumbnailBytes(); + assertNotNull(thumbnailBytes); + Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap(); + assertNotNull(thumbnailBitmap); + assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth()); + assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight()); + } else { + assertNull(exifInterface.getThumbnail()); + } + + // Checks GPS information. + double[] latLong = exifInterface.getLatLong(); + assertEquals(expectedValue.hasLatLong, latLong != null); + if (expectedValue.hasLatLong) { + assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE); + assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE); + } + assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE); + + // Checks values. + assertStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make); + assertStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model); + assertFloatTag(exifInterface, ExifInterface.TAG_F_NUMBER, expectedValue.aperture); + assertStringTag(exifInterface, ExifInterface.TAG_DATETIME_ORIGINAL, + expectedValue.dateTimeOriginal); + assertFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime); + assertFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash); + assertStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF, + expectedValue.gpsAltitudeRef); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP, expectedValue.gpsDatestamp); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF, + expectedValue.gpsLatitudeRef); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE, expectedValue.gpsLongitude); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF, + expectedValue.gpsLongitudeRef); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD, + expectedValue.gpsProcessingMethod); + assertStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP, expectedValue.gpsTimestamp); + assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength); + assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth); + assertStringTag(exifInterface, ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY, + expectedValue.iso); + assertIntTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation); + assertIntTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE, expectedValue.whiteBalance); + } + + private void testExifInterfaceCommon(String fileName, ExpectedValue expectedValue) + throws IOException { + File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); + String verboseTag = imageFile.getName(); + + // Creates via path. + ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath()); + assertNotNull(exifInterface); + compareWithExpectedValue(exifInterface, expectedValue, verboseTag); + + InputStream in = null; + // Creates via InputStream. + try { + in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath())); + exifInterface = new ExifInterface(in); + compareWithExpectedValue(exifInterface, expectedValue, verboseTag); + } finally { + closeQuietly(in); + } + } + + private void testSaveAttributes_withFileName(String fileName, ExpectedValue expectedValue) + throws IOException { + File imageFile = new File(Environment.getExternalStorageDirectory(), fileName); + String verboseTag = imageFile.getName(); + + ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath()); + exifInterface.saveAttributes(); + exifInterface = new ExifInterface(imageFile.getAbsolutePath()); + compareWithExpectedValue(exifInterface, expectedValue, verboseTag); + + // Test for modifying one attribute. + String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE); + exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc"); + exifInterface.saveAttributes(); + exifInterface = new ExifInterface(imageFile.getAbsolutePath()); + assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE)); + // Restore the backup value. + exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue); + exifInterface.saveAttributes(); + exifInterface = new ExifInterface(imageFile.getAbsolutePath()); + compareWithExpectedValue(exifInterface, expectedValue, verboseTag); + } + + private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId) + throws IOException { + ExpectedValue expectedValue = new ExpectedValue( + getContext().getResources().obtainTypedArray(typedArrayResourceId)); + + // Test for reading from external data storage. + testExifInterfaceCommon(fileName, expectedValue); + + // Test for saving attributes. + testSaveAttributes_withFileName(fileName, expectedValue); + } + + private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId) + throws IOException { + ExpectedValue expectedValue = new ExpectedValue( + getContext().getResources().obtainTypedArray(typedArrayResourceId)); + + // Test for reading from external data storage. + testExifInterfaceCommon(fileName, expectedValue); + + // Since ExifInterface does not support for saving attributes for RAW files, do not test + // about writing back in here. + } + + private void generateRandomExifTag(ByteBuffer buffer, int ifdType, Random random) { + ExifInterface.ExifTag[] tagGroup = ExifInterface.EXIF_TAGS[ifdType]; + ExifInterface.ExifTag tag = tagGroup[random.nextInt(tagGroup.length)]; + if (!randomlyCorrupted(random)) { + buffer.putShort((short) tag.number); + } + int dataFormat = random.nextInt(ExifInterface.IFD_FORMAT_NAMES.length); + if (!randomlyCorrupted(random)) { + buffer.putShort((short) dataFormat); + } + buffer.putInt(1); + int dataLength = ExifInterface.IFD_FORMAT_BYTES_PER_FORMAT[dataFormat]; + if (dataLength > 4) { + buffer.putShort((short) random.nextInt(8096 - dataLength)); + buffer.position(buffer.position() + 2); + } else { + buffer.position(buffer.position() + 4); + } + } + + private boolean randomlyCorrupted(Random random) { + // Corrupts somewhere in a possibility of 1/500. + return random.nextInt(500) == 0; + } + + private void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + private int copy(InputStream in, OutputStream out) throws IOException { + int total = 0; + byte[] buffer = new byte[8192]; + int c; + while ((c = in.read(buffer)) != -1) { + total += c; + out.write(buffer, 0, c); + } + return total; + } + + private void assertLatLongValuesAreNotSet(ExifInterface exif) { + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)); + assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF)); + } + + private ExifInterface createTestExifInterface() throws IOException { + File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg"); + image.deleteOnExit(); + return new ExifInterface(image.getAbsolutePath()); + } +} diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java index 39c30140..eeaa5ea1 100644 --- a/android/support/media/tv/BasePreviewProgram.java +++ b/android/support/media/tv/BasePreviewProgram.java @@ -17,7 +17,6 @@ package android.support.media.tv; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Intent; import android.database.Cursor; @@ -40,7 +39,6 @@ import java.util.TimeZone; * * @hide */ -@TargetApi(26) public abstract class BasePreviewProgram extends BaseProgram { /** * @hide diff --git a/android/support/media/tv/Channel.java b/android/support/media/tv/Channel.java index 9b13e422..a24d948f 100644 --- a/android/support/media/tv/Channel.java +++ b/android/support/media/tv/Channel.java @@ -17,7 +17,6 @@ package android.support.media.tv; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Intent; import android.database.Cursor; @@ -76,7 +75,6 @@ import java.nio.charset.Charset; * TvContractCompat.buildChannelUri(existingChannel.getId()), null, null); *

  • */ -@TargetApi(21) public final class Channel { /** * @hide diff --git a/android/support/media/tv/ChannelLogoUtilsTest.java b/android/support/media/tv/ChannelLogoUtilsTest.java new file mode 100644 index 00000000..ea315ab3 --- /dev/null +++ b/android/support/media/tv/ChannelLogoUtilsTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.media.tv; + +import static android.support.test.InstrumentationRegistry.getContext; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.SystemClock; +import android.support.media.tv.test.R; +import android.support.test.filters.MediumTest; +import android.support.test.filters.Suppress; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Suppress // Test is failing b/70905391 +@MediumTest +@RunWith(AndroidJUnit4.class) +public class ChannelLogoUtilsTest { + private static final String FAKE_INPUT_ID = "ChannelLogoUtils.test"; + + private ContentResolver mContentResolver; + private Uri mChannelUri; + private long mChannelId; + + @Before + public void setUp() throws Exception { + mContentResolver = getContext().getContentResolver(); + ContentValues contentValues = new Channel.Builder() + .setInputId(FAKE_INPUT_ID) + .setType(TvContractCompat.Channels.TYPE_OTHER).build().toContentValues(); + mChannelUri = mContentResolver.insert(TvContract.Channels.CONTENT_URI, contentValues); + mChannelId = ContentUris.parseId(mChannelUri); + } + + @After + public void tearDown() throws Exception { + mContentResolver.delete(mChannelUri, null, null); + } + + @Test + public void testStoreChannelLogo_fromBitmap() { + assertNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId)); + Bitmap logo = BitmapFactory.decodeResource(getContext().getResources(), + R.drawable.test_icon); + assertNotNull(logo); + assertTrue(ChannelLogoUtils.storeChannelLogo(getContext(), mChannelId, logo)); + // Workaround: the file status is not consistent between openInputStream/openOutputStream, + // wait 10 secs to make sure that the logo file is written into the disk. + SystemClock.sleep(10000); + assertNotNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId)); + } + + @Test + public void testStoreChannelLogo_fromResUri() { + assertNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId)); + int resId = R.drawable.test_icon; + Resources res = getContext().getResources(); + Uri logoUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(res.getResourcePackageName(resId)) + .appendPath(res.getResourceTypeName(resId)) + .appendPath(res.getResourceEntryName(resId)) + .build(); + assertTrue(ChannelLogoUtils.storeChannelLogo(getContext(), mChannelId, logoUri)); + // Workaround: the file status is not consistent between openInputStream/openOutputStream, + // wait 10 secs to make sure that the logo file is written into the disk. + SystemClock.sleep(10000); + assertNotNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId)); + } +} diff --git a/android/support/media/tv/ChannelTest.java b/android/support/media/tv/ChannelTest.java new file mode 100644 index 00000000..979a20a4 --- /dev/null +++ b/android/support/media/tv/ChannelTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.media.tv; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Build; +import android.support.media.tv.TvContractCompat.Channels; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests that channels can be created using the Builder pattern and correctly obtain + * values from them + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) +public class ChannelTest { + @After + public void tearDown() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + resolver.delete(Channels.CONTENT_URI, null, null); + } + + @Test + public void testEmptyChannel() { + Channel emptyChannel = new Channel.Builder() + .build(); + ContentValues contentValues = emptyChannel.toContentValues(true); + compareChannel(emptyChannel, Channel.fromCursor(getChannelCursor(contentValues)), true); + } + + @Test + public void testSampleChannel() { + // Tests cloning and database I/O of a channel with some defined and some undefined + // values. + Channel sampleChannel = new Channel.Builder() + .setDisplayName("Google") + .setDisplayNumber("3") + .setDescription("This is a sample channel") + .setOriginalNetworkId(1) + .setAppLinkIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri( + Intent.URI_INTENT_SCHEME))) + .setOriginalNetworkId(0) + .build(); + ContentValues contentValues = sampleChannel.toContentValues(true); + compareChannel(sampleChannel, Channel.fromCursor(getChannelCursor(contentValues)), true); + + Channel clonedSampleChannel = new Channel.Builder(sampleChannel).build(); + compareChannel(sampleChannel, clonedSampleChannel, true); + } + + @Test + public void testFullyPopulatedChannel() { + Channel fullyPopulatedChannel = createFullyPopulatedChannel(); + ContentValues contentValues = fullyPopulatedChannel.toContentValues(true); + compareChannel(fullyPopulatedChannel, Channel.fromCursor(getChannelCursor(contentValues)), + true); + + Channel clonedFullyPopulatedChannel = new Channel.Builder(fullyPopulatedChannel).build(); + compareChannel(fullyPopulatedChannel, clonedFullyPopulatedChannel, true); + } + + @Test + public void testChannelWithSystemContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + Channel fullyPopulatedChannel = createFullyPopulatedChannel(); + ContentValues contentValues = fullyPopulatedChannel.toContentValues(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues); + assertNotNull(channelUri); + + Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri); + compareChannel(fullyPopulatedChannel, channelFromSystemDb, false); + } + + @Test + public void testChannelUpdateWithContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + + Channel fullyPopulatedChannel = createFullyPopulatedChannel(); + ContentValues contentValues = fullyPopulatedChannel.toContentValues(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues); + assertNotNull(channelUri); + + Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri); + compareChannel(fullyPopulatedChannel, channelFromSystemDb, false); + + // Update a field from a fully loaded channel. + Channel updatedChannel = new Channel.Builder(channelFromSystemDb) + .setDescription("new description").build(); + assertEquals(1, resolver.update(channelUri, updatedChannel.toContentValues(), null, null)); + channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri); + compareChannel(updatedChannel, channelFromSystemDb, false); + + // Update a field with null from a fully loaded channel. + updatedChannel = new Channel.Builder(updatedChannel) + .setAppLinkText(null).build(); + assertEquals(1, resolver.update( + channelUri, updatedChannel.toContentValues(), null, null)); + channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri); + compareChannel(updatedChannel, channelFromSystemDb, false); + + // Update a field without referencing fully channel. + ContentValues values = new Channel.Builder().setDisplayName("abc").build() + .toContentValues(); + assertEquals(1, values.size()); + assertEquals(1, resolver.update(channelUri, values, null, null)); + channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri); + Channel expectedChannel = new Channel.Builder(channelFromSystemDb) + .setDisplayName("abc").build(); + compareChannel(expectedChannel, channelFromSystemDb, false); + } + + @Test + public void testChannelEquals() { + assertEquals(createFullyPopulatedChannel(), createFullyPopulatedChannel()); + } + + + private static Channel loadChannelFromContentProvider( + ContentResolver resolver, Uri channelUri) { + try (Cursor cursor = resolver.query(channelUri, null, null, null, null)) { + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + return Channel.fromCursor(cursor); + } + } + + private static Channel createFullyPopulatedChannel() { + return new Channel.Builder() + .setAppLinkColor(0x00FF0000) + .setAppLinkIconUri(Uri.parse("http://example.com/icon.png")) + .setAppLinkIntent(new Intent()) + .setAppLinkPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setAppLinkText("Open an intent") + .setDescription("Channel description") + .setDisplayName("Display Name") + .setDisplayNumber("100") + .setInputId("TestInputService") + .setNetworkAffiliation("Network Affiliation") + .setOriginalNetworkId(2) + .setPackageName("android.support.media.tv.test") + .setSearchable(false) + .setServiceId(3) + .setTransportStreamId(4) + .setType(TvContractCompat.Channels.TYPE_PREVIEW) + .setServiceType(TvContractCompat.Channels.SERVICE_TYPE_AUDIO_VIDEO) + .setVideoFormat(TvContractCompat.Channels.VIDEO_FORMAT_240P) + .setInternalProviderFlag1(0x4) + .setInternalProviderFlag2(0x3) + .setInternalProviderFlag3(0x2) + .setInternalProviderFlag4(0x1) + .setInternalProviderId("Internal Provider") + .setTransient(true) + .setBrowsable(true) + .setLocked(true) + .setSystemApproved(true) + .build(); + } + + private static void compareChannel(Channel channelA, Channel channelB, + boolean includeIdAndProtectedFields) { + assertEquals(channelA.isSearchable(), channelB.isSearchable()); + assertEquals(channelA.getDescription(), channelB.getDescription()); + assertEquals(channelA.getDisplayName(), channelB.getDisplayName()); + assertEquals(channelA.getDisplayNumber(), channelB.getDisplayNumber()); + assertEquals(channelA.getInputId(), channelB.getInputId()); + assertEquals(channelA.getNetworkAffiliation(), channelB.getNetworkAffiliation()); + assertEquals(channelA.getOriginalNetworkId(), channelB.getOriginalNetworkId()); + assertEquals(channelA.getPackageName(), channelB.getPackageName()); + assertEquals(channelA.getServiceId(), channelB.getServiceId()); + assertEquals(channelA.getServiceType(), channelB.getServiceType()); + assertEquals(channelA.getTransportStreamId(), channelB.getTransportStreamId()); + assertEquals(channelA.getType(), channelB.getType()); + assertEquals(channelA.getVideoFormat(), channelB.getVideoFormat()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + assertEquals(channelA.getAppLinkColor(), channelB.getAppLinkColor()); + assertEquals(channelA.getAppLinkIconUri(), channelB.getAppLinkIconUri()); + assertEquals(channelA.getAppLinkIntentUri(), channelB.getAppLinkIntentUri()); + assertEquals(channelA.getAppLinkPosterArtUri(), channelB.getAppLinkPosterArtUri()); + assertEquals(channelA.getAppLinkText(), channelB.getAppLinkText()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + assertEquals(channelA.getInternalProviderId(), channelB.getInternalProviderId()); + assertEquals(channelA.isTransient(), channelB.isTransient()); + } + if (includeIdAndProtectedFields) { + // Skip row ID since the one from system DB has the valid ID while the other does not. + assertEquals(channelA.getId(), channelB.getId()); + // When we insert a channel using toContentValues() to the system, we drop some + // protected fields since they only can be modified by system apps. + assertEquals(channelA.isBrowsable(), channelB.isBrowsable()); + assertEquals(channelA.isLocked(), channelB.isLocked()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + assertEquals(channelA.isSystemApproved(), channelB.isSystemApproved()); + } + assertEquals(channelA.toContentValues(), channelB.toContentValues()); + } + } + + private static MatrixCursor getChannelCursor(ContentValues contentValues) { + String[] cols = Channel.PROJECTION; + MatrixCursor cursor = new MatrixCursor(cols); + MatrixCursor.RowBuilder builder = cursor.newRow(); + for (String col : cols) { + if (col != null) { + builder.add(col, contentValues.get(col)); + } + } + cursor.moveToFirst(); + return cursor; + } +} diff --git a/android/support/media/tv/PreviewProgram.java b/android/support/media/tv/PreviewProgram.java index 3df3a744..6d2fbaf4 100644 --- a/android/support/media/tv/PreviewProgram.java +++ b/android/support/media/tv/PreviewProgram.java @@ -17,7 +17,6 @@ package android.support.media.tv; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.ContentValues; import android.database.Cursor; import android.media.tv.TvContentRating; // For javadoc gen of super class @@ -74,7 +73,6 @@ import android.support.media.tv.TvContractCompat.Programs.Genres; // For javado * null, null); *
    */ -@TargetApi(26) public final class PreviewProgram extends BasePreviewProgram { /** * @hide diff --git a/android/support/media/tv/PreviewProgramTest.java b/android/support/media/tv/PreviewProgramTest.java new file mode 100644 index 00000000..d0baa5f6 --- /dev/null +++ b/android/support/media/tv/PreviewProgramTest.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.media.tv; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.media.tv.TvContentRating; +import android.net.Uri; +import android.support.media.tv.TvContractCompat.Channels; +import android.support.media.tv.TvContractCompat.PreviewPrograms; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Date; +import java.util.Objects; + +/** + * Tests that preview programs can be created using the Builder pattern and correctly obtain + * values from them. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = 26) +public class PreviewProgramTest { + + @After + public void tearDown() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + resolver.delete(Channels.CONTENT_URI, null, null); + } + + @Test + public void testEmptyPreviewProgram() { + PreviewProgram emptyProgram = new PreviewProgram.Builder().build(); + ContentValues contentValues = emptyProgram.toContentValues(); + compareProgram(emptyProgram, + PreviewProgram.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), + true); + } + + @Test + public void testSampleProgram() { + PreviewProgram sampleProgram = new PreviewProgram.Builder() + .setPackageName("My package") + .setTitle("Program Title") + .setDescription("This is a sample program") + .setEpisodeNumber(5) + .setSeasonNumber("The Final Season", 7) + .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png")) + .setChannelId(3) + .setWeight(70) + .build(); + ContentValues contentValues = sampleProgram.toContentValues(true); + compareProgram(sampleProgram, + PreviewProgram.fromCursor( + getProgramCursor(PreviewProgram.PROJECTION, contentValues)), true); + + PreviewProgram clonedSampleProgram = new PreviewProgram.Builder(sampleProgram).build(); + compareProgram(sampleProgram, clonedSampleProgram, true); + } + + @Test + public void testFullyPopulatedPreviewProgram() { + PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(3); + ContentValues contentValues = fullyPopulatedProgram.toContentValues(true); + compareProgram(fullyPopulatedProgram, + PreviewProgram.fromCursor( + getProgramCursor(PreviewProgram.PROJECTION, contentValues)), true); + + PreviewProgram clonedFullyPopulatedProgram = + new PreviewProgram.Builder(fullyPopulatedProgram).build(); + compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true); + } + + @Test + public void testPreviewProgramWithSystemContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + Channel channel = new Channel.Builder() + .setInputId("TestInputService") + .setType(TvContractCompat.Channels.TYPE_PREVIEW) + .build(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues()); + assertNotNull(channelUri); + + PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram( + ContentUris.parseId(channelUri)); + Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + PreviewProgram programFromSystemDb = + loadPreviewProgramFromContentProvider(resolver, previewProgramUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + } + + @Test + public void testPreviewProgramUpdateWithContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + Channel channel = new Channel.Builder() + .setInputId("TestInputService") + .setType(TvContractCompat.Channels.TYPE_PREVIEW) + .build(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues()); + assertNotNull(channelUri); + + PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram( + ContentUris.parseId(channelUri)); + Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + PreviewProgram programFromSystemDb = loadPreviewProgramFromContentProvider( + resolver, previewProgramUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + + // Update a field from a fully loaded preview program. + PreviewProgram updatedProgram = new PreviewProgram.Builder(programFromSystemDb) + .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build(); + assertEquals(1, resolver.update( + previewProgramUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field with null from a fully loaded preview program. + updatedProgram = new PreviewProgram.Builder(updatedProgram) + .setLongDescription(null).build(); + assertEquals(1, resolver.update( + previewProgramUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field without referencing fully loaded preview program. + ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build() + .toContentValues(); + assertEquals(1, values.size()); + assertEquals(1, resolver.update(previewProgramUri, values, null, null)); + programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri); + PreviewProgram expectedProgram = new PreviewProgram.Builder(programFromSystemDb) + .setInteractionCount(1).build(); + compareProgram(expectedProgram, programFromSystemDb, false); + } + + @Test + public void testPreviewProgramEquals() { + assertEquals(createFullyPopulatedPreviewProgram(1), createFullyPopulatedPreviewProgram(1)); + } + + private static PreviewProgram loadPreviewProgramFromContentProvider( + ContentResolver resolver, Uri previewProgramUri) { + try (Cursor cursor = resolver.query(previewProgramUri, null, null, null, null)) { + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + return PreviewProgram.fromCursor(cursor); + } + } + + @Test + public void testPreviewProgramWithPartialData() { + PreviewProgram previewProgram = new PreviewProgram.Builder() + .setChannelId(3) + .setWeight(100) + .setInternalProviderId("ID-4321") + .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg")) + .setLastPlaybackPositionMillis(0) + .setDurationMillis(60 * 1000) + .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri( + Intent.URI_INTENT_SCHEME))) + .setTransient(false) + .setType(PreviewPrograms.TYPE_TV_EPISODE) + .setPosterArtAspectRatio(PreviewPrograms.ASPECT_RATIO_3_2) + .setThumbnailAspectRatio(PreviewPrograms.ASPECT_RATIO_16_9) + .setLogoUri(Uri.parse("http://example.com/program-logo.mpg")) + .setAvailability(PreviewPrograms.AVAILABILITY_FREE_WITH_SUBSCRIPTION) + .setStartingPrice("9.99 USD") + .setOfferPrice("3.99 USD") + .setReleaseDate(new Date(Date.UTC(97, 2, 8, 9, 30, 59))) + .setLive(false) + .setInteractionType(PreviewPrograms.INTERACTION_TYPE_VIEWS) + .setInteractionCount(99200) + .setAuthor("author_name") + .setReviewRatingStyle(PreviewPrograms.REVIEW_RATING_STYLE_PERCENTAGE) + .setReviewRating("83.9") + .setId(10) + .setTitle("Recommended Video 1") + .setDescription("You should watch this!") + .setPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setInternalProviderFlag2(0x0010010084108410L) + .build(); + + String[] partialProjection = { + PreviewPrograms._ID, + PreviewPrograms.COLUMN_CHANNEL_ID, + PreviewPrograms.COLUMN_TITLE, + PreviewPrograms.COLUMN_SHORT_DESCRIPTION, + PreviewPrograms.COLUMN_POSTER_ART_URI, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI, + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + PreviewPrograms.COLUMN_DURATION_MILLIS, + PreviewPrograms.COLUMN_INTENT_URI, + PreviewPrograms.COLUMN_WEIGHT, + PreviewPrograms.COLUMN_TRANSIENT, + PreviewPrograms.COLUMN_TYPE, + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, + PreviewPrograms.COLUMN_LOGO_URI, + PreviewPrograms.COLUMN_AVAILABILITY, + PreviewPrograms.COLUMN_STARTING_PRICE, + PreviewPrograms.COLUMN_OFFER_PRICE, + PreviewPrograms.COLUMN_RELEASE_DATE, + PreviewPrograms.COLUMN_ITEM_COUNT, + PreviewPrograms.COLUMN_LIVE, + PreviewPrograms.COLUMN_INTERACTION_TYPE, + PreviewPrograms.COLUMN_INTERACTION_COUNT, + PreviewPrograms.COLUMN_AUTHOR, + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE, + PreviewPrograms.COLUMN_REVIEW_RATING, + }; + + ContentValues contentValues = previewProgram.toContentValues(true); + compareProgram(previewProgram, + PreviewProgram.fromCursor(getProgramCursor(partialProjection, contentValues)), + true); + + PreviewProgram clonedFullyPopulatedProgram = + new PreviewProgram.Builder(previewProgram).build(); + compareProgram(previewProgram, clonedFullyPopulatedProgram, true); + } + + private static PreviewProgram createFullyPopulatedPreviewProgram(long channelId) { + return new PreviewProgram.Builder() + .setTitle("Google") + .setInternalProviderId("ID-4321") + .setChannelId(channelId) + .setWeight(100) + .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg")) + .setLastPlaybackPositionMillis(0) + .setDurationMillis(60 * 1000) + .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri( + Intent.URI_INTENT_SCHEME))) + .setTransient(false) + .setType(PreviewPrograms.TYPE_MOVIE) + .setPosterArtAspectRatio(PreviewPrograms.ASPECT_RATIO_2_3) + .setThumbnailAspectRatio(PreviewPrograms.ASPECT_RATIO_16_9) + .setLogoUri(Uri.parse("http://example.com/program-logo.mpg")) + .setAvailability(PreviewPrograms.AVAILABILITY_AVAILABLE) + .setStartingPrice("12.99 USD") + .setOfferPrice("4.99 USD") + .setReleaseDate("1997") + .setItemCount(3) + .setLive(false) + .setInteractionType(PreviewPrograms.INTERACTION_TYPE_LIKES) + .setInteractionCount(10200) + .setAuthor("author_name") + .setReviewRatingStyle(PreviewPrograms.REVIEW_RATING_STYLE_STARS) + .setReviewRating("4.5") + .setSearchable(false) + .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png")) + .setAudioLanguages(new String [] {"eng", "kor"}) + .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES}) + .setContentRatings(new TvContentRating[] { + TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")}) + .setDescription("This is a sample program") + .setEpisodeNumber("Pilot", 0) + .setEpisodeTitle("Hello World") + .setLongDescription("This is a longer description than the previous description") + .setPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setSeasonNumber("The Final Season", 7) + .setSeasonTitle("The Final Season") + .setVideoHeight(1080) + .setVideoWidth(1920) + .setInternalProviderFlag1(0x4) + .setInternalProviderFlag2(0x3) + .setInternalProviderFlag3(0x2) + .setInternalProviderFlag4(0x1) + .setBrowsable(true) + .setContentId("CID-8642") + .build(); + } + + private static void compareProgram(PreviewProgram programA, PreviewProgram programB, + boolean includeIdAndProtectedFields) { + assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages())); + assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres())); + assertEquals(programA.getChannelId(), programB.getChannelId()); + assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings())); + assertEquals(programA.getDescription(), programB.getDescription()); + assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber()); + assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle()); + assertEquals(programA.getLongDescription(), programB.getLongDescription()); + assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri()); + assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber()); + assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri()); + assertEquals(programA.getTitle(), programB.getTitle()); + assertEquals(programA.getVideoHeight(), programB.getVideoHeight()); + assertEquals(programA.getVideoWidth(), programB.getVideoWidth()); + assertEquals(programA.isSearchable(), programB.isSearchable()); + assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1()); + assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2()); + assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3()); + assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4()); + assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle())); + assertEquals(programA.getInternalProviderId(), programB.getInternalProviderId()); + assertEquals(programA.getPreviewVideoUri(), programB.getPreviewVideoUri()); + assertEquals(programA.getLastPlaybackPositionMillis(), + programB.getLastPlaybackPositionMillis()); + assertEquals(programA.getDurationMillis(), programB.getDurationMillis()); + assertEquals(programA.getIntentUri(), programB.getIntentUri()); + assertEquals(programA.getWeight(), programB.getWeight()); + assertEquals(programA.isTransient(), programB.isTransient()); + assertEquals(programA.getType(), programB.getType()); + assertEquals(programA.getPosterArtAspectRatio(), programB.getPosterArtAspectRatio()); + assertEquals(programA.getThumbnailAspectRatio(), programB.getThumbnailAspectRatio()); + assertEquals(programA.getLogoUri(), programB.getLogoUri()); + assertEquals(programA.getAvailability(), programB.getAvailability()); + assertEquals(programA.getStartingPrice(), programB.getStartingPrice()); + assertEquals(programA.getOfferPrice(), programB.getOfferPrice()); + assertEquals(programA.getReleaseDate(), programB.getReleaseDate()); + assertEquals(programA.getItemCount(), programB.getItemCount()); + assertEquals(programA.isLive(), programB.isLive()); + assertEquals(programA.getInteractionType(), programB.getInteractionType()); + assertEquals(programA.getInteractionCount(), programB.getInteractionCount()); + assertEquals(programA.getAuthor(), programB.getAuthor()); + assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle()); + assertEquals(programA.getReviewRating(), programB.getReviewRating()); + assertEquals(programA.getContentId(), programB.getContentId()); + if (includeIdAndProtectedFields) { + // Skip row ID since the one from system DB has the valid ID while the other does not. + assertEquals(programA.getId(), programB.getId()); + assertEquals(programA.getPackageName(), programB.getPackageName()); + // When we insert a channel using toContentValues() to the system, we drop some + // protected fields since they only can be modified by system apps. + assertEquals(programA.isBrowsable(), programB.isBrowsable()); + assertEquals(programA.toContentValues(), programB.toContentValues()); + assertEquals(programA, programB); + } + } + + private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) { + MatrixCursor cursor = new MatrixCursor(projection); + MatrixCursor.RowBuilder builder = cursor.newRow(); + for (String col : projection) { + if (col != null) { + builder.add(col, contentValues.get(col)); + } + } + cursor.moveToFirst(); + return cursor; + } +} diff --git a/android/support/media/tv/Program.java b/android/support/media/tv/Program.java index 233f1bab..882916d3 100644 --- a/android/support/media/tv/Program.java +++ b/android/support/media/tv/Program.java @@ -17,7 +17,6 @@ package android.support.media.tv; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.ContentValues; import android.database.Cursor; import android.media.tv.TvContentRating; // For javadoc gen of super class @@ -72,7 +71,6 @@ import android.support.media.tv.TvContractCompat.Programs.Genres.Genre; * null, null); * */ -@TargetApi(21) public final class Program extends BaseProgram implements Comparable { /** * @hide diff --git a/android/support/media/tv/ProgramTest.java b/android/support/media/tv/ProgramTest.java new file mode 100644 index 00000000..62093832 --- /dev/null +++ b/android/support/media/tv/ProgramTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.media.tv; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.media.tv.TvContentRating; +import android.net.Uri; +import android.os.Build; +import android.support.media.tv.TvContractCompat.Channels; +import android.support.media.tv.TvContractCompat.Programs; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Tests that programs can be created using the Builder pattern and correctly obtain + * values from them. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) +public class ProgramTest { + @After + public void tearDown() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + resolver.delete(Channels.CONTENT_URI, null, null); + } + + @Test + public void testEmptyProgram() { + Program emptyProgram = new Program.Builder() + .build(); + ContentValues contentValues = emptyProgram.toContentValues(); + compareProgram(emptyProgram, + Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true); + } + + @Test + public void testSampleProgram() { + Program sampleProgram = new Program.Builder() + .setPackageName("My package") + .setTitle("Program Title") + .setDescription("This is a sample program") + .setEpisodeNumber(5) + .setSeasonNumber("The Final Season", 7) + .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png")) + .setChannelId(3) + .setStartTimeUtcMillis(0) + .setEndTimeUtcMillis(1000) + .build(); + ContentValues contentValues = sampleProgram.toContentValues(); + compareProgram(sampleProgram, + Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true); + + Program clonedSampleProgram = new Program.Builder(sampleProgram).build(); + compareProgram(sampleProgram, clonedSampleProgram, true); + } + + @Test + public void testFullyPopulatedProgram() { + Program fullyPopulatedProgram = createFullyPopulatedProgram(3); + + ContentValues contentValues = fullyPopulatedProgram.toContentValues(); + compareProgram(fullyPopulatedProgram, + Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true); + + Program clonedFullyPopulatedProgram = new Program.Builder(fullyPopulatedProgram).build(); + compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true); + } + + @Test + public void testChannelWithSystemContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + Channel channel = new Channel.Builder() + .setInputId("TestInputService") + .setType(TvContractCompat.Channels.TYPE_OTHER) + .build(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues()); + assertNotNull(channelUri); + + Program fullyPopulatedProgram = + createFullyPopulatedProgram(ContentUris.parseId(channelUri)); + Uri programUri = resolver.insert(Programs.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + } + + @Test + public void testProgramUpdateWithContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + Channel channel = new Channel.Builder() + .setInputId("TestInputService") + .setType(TvContractCompat.Channels.TYPE_OTHER) + .build(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues()); + assertNotNull(channelUri); + + Program fullyPopulatedProgram = + createFullyPopulatedProgram(ContentUris.parseId(channelUri)); + Uri programUri = resolver.insert(Programs.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + + // Update a field from a fully loaded program. + Program updatedProgram = new Program.Builder(programFromSystemDb) + .setDescription("description1").build(); + assertEquals(1, resolver.update( + programUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = loadProgramFromContentProvider(resolver, programUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field with null from a fully loaded program. + updatedProgram = new Program.Builder(updatedProgram) + .setLongDescription(null).build(); + assertEquals(1, resolver.update( + programUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = loadProgramFromContentProvider(resolver, programUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field without referencing fully loaded program. + ContentValues values = new Program.Builder().setDescription("description2").build() + .toContentValues(); + assertEquals(1, values.size()); + assertEquals(1, resolver.update(programUri, values, null, null)); + programFromSystemDb = loadProgramFromContentProvider(resolver, programUri); + Program expectedProgram = new Program.Builder(programFromSystemDb) + .setDescription("description2").build(); + compareProgram(expectedProgram, programFromSystemDb, false); + } + + @Test + public void testProgramEquals() { + assertEquals(createFullyPopulatedProgram(1), createFullyPopulatedProgram(1)); + } + + private static Program loadProgramFromContentProvider( + ContentResolver resolver, Uri programUri) { + try (Cursor cursor = resolver.query(programUri, null, null, null, null)) { + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + return Program.fromCursor(cursor); + } + } + + private static Program createFullyPopulatedProgram(long channelId) { + return new Program.Builder() + .setSearchable(false) + .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png")) + .setAudioLanguages(new String [] {"eng", "kor"}) + .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES}) + .setContentRatings(new TvContentRating[] { + TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")}) + .setDescription("This is a sample program") + .setEpisodeNumber("Pilot", 0) + .setEpisodeTitle("Hello World") + .setLongDescription("This is a longer description than the previous description") + .setPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setSeasonNumber("The Final Season", 7) + .setSeasonTitle("The Final Season") + .setTitle("Google") + .setVideoHeight(1080) + .setVideoWidth(1920) + .setInternalProviderFlag1(0x4) + .setInternalProviderFlag2(0x3) + .setInternalProviderFlag3(0x2) + .setInternalProviderFlag4(0x1) + .setReviewRatingStyle(Programs.REVIEW_RATING_STYLE_PERCENTAGE) + .setReviewRating("83.9") + .setChannelId(channelId) + .setStartTimeUtcMillis(0) + .setEndTimeUtcMillis(1000) + .setBroadcastGenres(new String[] {"Music", "Family"}) + .setRecordingProhibited(false) + .build(); + } + + private static void compareProgram(Program programA, Program programB, + boolean includeIdAndProtectedFields) { + assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages())); + assertTrue(Arrays.deepEquals(programA.getBroadcastGenres(), programB.getBroadcastGenres())); + assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres())); + assertEquals(programA.getChannelId(), programB.getChannelId()); + assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings())); + assertEquals(programA.getDescription(), programB.getDescription()); + assertEquals(programA.getEndTimeUtcMillis(), programB.getEndTimeUtcMillis()); + assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber()); + assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle()); + assertEquals(programA.getLongDescription(), programB.getLongDescription()); + assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri()); + assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber()); + assertEquals(programA.getStartTimeUtcMillis(), programB.getStartTimeUtcMillis()); + assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri()); + assertEquals(programA.getTitle(), programB.getTitle()); + assertEquals(programA.getVideoHeight(), programB.getVideoHeight()); + assertEquals(programA.getVideoWidth(), programB.getVideoWidth()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + assertEquals(programA.isSearchable(), programB.isSearchable()); + assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1()); + assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2()); + assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3()); + assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle())); + assertTrue(Objects.equals(programA.isRecordingProhibited(), + programB.isRecordingProhibited())); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle()); + assertEquals(programA.getReviewRating(), programB.getReviewRating()); + } + if (includeIdAndProtectedFields) { + // Skip row ID since the one from system DB has the valid ID while the other does not. + assertEquals(programA.getId(), programB.getId()); + assertEquals(programA.getPackageName(), programB.getPackageName()); + assertEquals(programA.toContentValues(), programB.toContentValues()); + } + } + + private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) { + MatrixCursor cursor = new MatrixCursor(projection); + MatrixCursor.RowBuilder builder = cursor.newRow(); + for (String row : projection) { + if (row != null) { + builder.add(row, contentValues.get(row)); + } + } + cursor.moveToFirst(); + return cursor; + } +} diff --git a/android/support/media/tv/TvContractUtilsTest.java b/android/support/media/tv/TvContractUtilsTest.java new file mode 100644 index 00000000..bdb739f1 --- /dev/null +++ b/android/support/media/tv/TvContractUtilsTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.media.tv; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.media.tv.TvContentRating; +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) +public class TvContractUtilsTest { + + @Test + public void testStringToContentRatings_nullInput() { + assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings(null)); + } + + @Test + public void testStringToContentRatings_emptyInput() { + assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings("")); + } + + @Test + public void testStringToContentRatings_singleRating() { + TvContentRating[] ratings = new TvContentRating[1]; + ratings[0] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_PG", + "US_TV_D", + "US_TV_L", + "US_TV_S", + "US_TV_V"); + assertArrayEquals(ratings, TvContractUtils.stringToContentRatings( + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V")); + } + + @Test + public void testStringToContentRatings_multipleRatings() { + TvContentRating[] ratings = new TvContentRating[3]; + ratings[0] = TvContentRating.createRating( + "com.android.tv", + "US_MV", + "US_MV_NC17"); + ratings[1] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_Y7"); + ratings[2] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_PG", + "US_TV_D", + "US_TV_L", + "US_TV_S", + "US_TV_V"); + assertArrayEquals(ratings, TvContractUtils.stringToContentRatings( + "com.android.tv/US_MV/US_MV_NC17," + + "com.android.tv/US_TV/US_TV_Y7," + + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V")); + } + + @Test + public void testStringToContentRatings_allRatingsInvalid() { + assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings( + "com.android.tv/US_MV," // Invalid + + "com.android.tv")); // Invalid + } + + @Test + public void testStringToContentRatings_someRatingsInvalid() { + TvContentRating[] ratings = new TvContentRating[1]; + ratings[0] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_PG", + "US_TV_D", + "US_TV_L", + "US_TV_S", + "US_TV_V"); + assertArrayEquals(ratings, TvContractUtils.stringToContentRatings( + "com.android.tv/US_MV," // Invalid + + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V," // Valid + + "com.android.tv")); // Invalid + } + + @Test + public void testContentRatingsToString_nullInput() { + assertEquals(null, TvContractUtils.contentRatingsToString(null)); + } + + @Test + public void testContentRatingsToString_emptyInput() { + assertEquals(null, TvContractUtils.contentRatingsToString(new TvContentRating[0])); + } + + @Test + public void testContentRatingsToString_singleRating() { + TvContentRating[] ratings = new TvContentRating[1]; + ratings[0] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_PG", + "US_TV_D", + "US_TV_L", + "US_TV_S", + "US_TV_V"); + assertEquals("com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V", + TvContractUtils.contentRatingsToString(ratings)); + } + + @Test + public void testContentRatingsToString_multipleRatings() { + TvContentRating[] ratings = new TvContentRating[3]; + ratings[0] = TvContentRating.createRating( + "com.android.tv", + "US_MV", + "US_MV_NC17"); + ratings[1] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_PG", + "US_TV_D", + "US_TV_L", + "US_TV_S", + "US_TV_V"); + ratings[2] = TvContentRating.createRating( + "com.android.tv", + "US_TV", + "US_TV_Y7"); + String ratingString = "com.android.tv/US_MV/US_MV_NC17," + + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V," + + "com.android.tv/US_TV/US_TV_Y7"; + assertEquals(ratingString, TvContractUtils.contentRatingsToString(ratings)); + } +} diff --git a/android/support/media/tv/Utils.java b/android/support/media/tv/Utils.java new file mode 100644 index 00000000..a6ff0ad9 --- /dev/null +++ b/android/support/media/tv/Utils.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.media.tv; + +import android.content.Context; +import android.content.pm.PackageManager; + +public class Utils { + private Utils() { } + + public static boolean hasTvInputFramework(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LIVE_TV); + } +} diff --git a/android/support/media/tv/WatchNextProgram.java b/android/support/media/tv/WatchNextProgram.java index c192745c..61082aac 100644 --- a/android/support/media/tv/WatchNextProgram.java +++ b/android/support/media/tv/WatchNextProgram.java @@ -17,7 +17,6 @@ package android.support.media.tv; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.ContentValues; import android.database.Cursor; import android.media.tv.TvContentRating; // For javadoc gen of super class @@ -79,7 +78,6 @@ import java.lang.annotation.RetentionPolicy; * null, null); * */ -@TargetApi(26) public final class WatchNextProgram extends BasePreviewProgram { /** * @hide diff --git a/android/support/media/tv/WatchNextProgramTest.java b/android/support/media/tv/WatchNextProgramTest.java new file mode 100644 index 00000000..ecce068f --- /dev/null +++ b/android/support/media/tv/WatchNextProgramTest.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.media.tv; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.media.tv.TvContentRating; +import android.net.Uri; +import android.support.media.tv.TvContractCompat.WatchNextPrograms; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Date; +import java.util.Objects; + +/** + * Tests that watch next programs can be created using the Builder pattern and correctly obtain + * values from them. + */ +@SmallTest +@SdkSuppress(minSdkVersion = 26) +@RunWith(AndroidJUnit4.class) +public class WatchNextProgramTest { + + @Before + public void tearDown() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + resolver.delete(WatchNextPrograms.CONTENT_URI, null, null); + } + + @Test + public void testEmptyPreviewProgram() { + WatchNextProgram emptyProgram = new WatchNextProgram.Builder().build(); + ContentValues contentValues = emptyProgram.toContentValues(true); + compareProgram(emptyProgram, + WatchNextProgram.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), + true); + } + + @Test + public void testSampleProgram() { + WatchNextProgram sampleProgram = new WatchNextProgram.Builder() + .setTitle("Program Title") + .setDescription("This is a sample program") + .setEpisodeNumber(5) + .setSeasonNumber("The Final Season", 7) + .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png")) + .build(); + ContentValues contentValues = sampleProgram.toContentValues(true); + compareProgram(sampleProgram, + WatchNextProgram.fromCursor( + getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true); + + WatchNextProgram clonedSampleProgram = new WatchNextProgram.Builder(sampleProgram).build(); + compareProgram(sampleProgram, clonedSampleProgram, true); + } + + @Test + public void testFullyPopulatedProgram() { + WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram(); + ContentValues contentValues = fullyPopulatedProgram.toContentValues(true); + compareProgram(fullyPopulatedProgram, + WatchNextProgram.fromCursor( + getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true); + + WatchNextProgram clonedFullyPopulatedProgram = + new WatchNextProgram.Builder(fullyPopulatedProgram).build(); + compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true); + } + + @Test + public void testChannelWithSystemContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + WatchNextProgram programFromSystemDb = + loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + } + + @Test + public void testWatchNextProgramUpdateWithContentProvider() { + if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) { + return; + } + + WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram(); + ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver(); + Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI, + fullyPopulatedProgram.toContentValues()); + + WatchNextProgram programFromSystemDb = + loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri); + compareProgram(fullyPopulatedProgram, programFromSystemDb, false); + + // Update a field from a fully loaded watch-next program. + WatchNextProgram updatedProgram = new WatchNextProgram.Builder(programFromSystemDb) + .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build(); + assertEquals(1, resolver.update( + watchNextProgramUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = + loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field with null from a fully loaded watch-next program. + updatedProgram = new WatchNextProgram.Builder(updatedProgram) + .setPreviewVideoUri(null).build(); + assertEquals(1, resolver.update( + watchNextProgramUri, updatedProgram.toContentValues(), null, null)); + programFromSystemDb = loadWatchNextProgramFromContentProvider( + resolver, watchNextProgramUri); + compareProgram(updatedProgram, programFromSystemDb, false); + + // Update a field without referencing fully watch-next program. + ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build() + .toContentValues(); + assertEquals(1, values.size()); + assertEquals(1, resolver.update(watchNextProgramUri, values, null, null)); + programFromSystemDb = loadWatchNextProgramFromContentProvider( + resolver, watchNextProgramUri); + WatchNextProgram expectedProgram = new WatchNextProgram.Builder(programFromSystemDb) + .setInteractionCount(1).build(); + compareProgram(expectedProgram, programFromSystemDb, false); + } + + @Test + public void testWatchNextProgramEquals() { + assertEquals(createFullyPopulatedWatchNextProgram(), + createFullyPopulatedWatchNextProgram()); + } + + private static WatchNextProgram loadWatchNextProgramFromContentProvider( + ContentResolver resolver, Uri watchNextProgramUri) { + try (Cursor cursor = resolver.query(watchNextProgramUri, null, null, null, null)) { + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToNext(); + return WatchNextProgram.fromCursor(cursor); + } + } + + @Test + public void testWatchNextProgramWithPartialData() { + WatchNextProgram previewProgram = new WatchNextProgram.Builder() + .setInternalProviderId("ID-4321") + .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg")) + .setLastPlaybackPositionMillis(0) + .setDurationMillis(60 * 1000) + .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri( + Intent.URI_INTENT_SCHEME))) + .setTransient(false) + .setType(WatchNextPrograms.TYPE_TV_EPISODE) + .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_3_2) + .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9) + .setLogoUri(Uri.parse("http://example.com/program-logo.mpg")) + .setAvailability(WatchNextPrograms.AVAILABILITY_FREE_WITH_SUBSCRIPTION) + .setStartingPrice("9.99 USD") + .setOfferPrice("3.99 USD") + .setReleaseDate(new Date(97, 2, 8)) + .setLive(false) + .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_VIEWS) + .setInteractionCount(99200) + .setAuthor("author_name") + .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_PERCENTAGE) + .setReviewRating("83.9") + .setId(10) + .setTitle("Recommended Video 1") + .setDescription("You should watch this!") + .setPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setInternalProviderFlag2(0x0010010084108410L) + .build(); + + String[] partialProjection = { + WatchNextPrograms._ID, + WatchNextPrograms.COLUMN_TITLE, + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION, + WatchNextPrograms.COLUMN_POSTER_ART_URI, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID, + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI, + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS, + WatchNextPrograms.COLUMN_DURATION_MILLIS, + WatchNextPrograms.COLUMN_INTENT_URI, + WatchNextPrograms.COLUMN_TRANSIENT, + WatchNextPrograms.COLUMN_TYPE, + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO, + WatchNextPrograms.COLUMN_LOGO_URI, + WatchNextPrograms.COLUMN_AVAILABILITY, + WatchNextPrograms.COLUMN_STARTING_PRICE, + WatchNextPrograms.COLUMN_OFFER_PRICE, + WatchNextPrograms.COLUMN_RELEASE_DATE, + WatchNextPrograms.COLUMN_ITEM_COUNT, + WatchNextPrograms.COLUMN_LIVE, + WatchNextPrograms.COLUMN_INTERACTION_TYPE, + WatchNextPrograms.COLUMN_INTERACTION_COUNT, + WatchNextPrograms.COLUMN_AUTHOR, + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE, + WatchNextPrograms.COLUMN_REVIEW_RATING, + }; + + ContentValues contentValues = previewProgram.toContentValues(true); + compareProgram(previewProgram, + WatchNextProgram.fromCursor(getProgramCursor(partialProjection, contentValues)), + true); + + WatchNextProgram clonedFullyPopulatedProgram = + new WatchNextProgram.Builder(previewProgram).build(); + compareProgram(previewProgram, clonedFullyPopulatedProgram, true); + } + + private static WatchNextProgram createFullyPopulatedWatchNextProgram() { + return new WatchNextProgram.Builder() + .setTitle("Google") + .setInternalProviderId("ID-4321") + .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg")) + .setLastPlaybackPositionMillis(0) + .setDurationMillis(60 * 1000) + .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri( + Intent.URI_INTENT_SCHEME))) + .setTransient(false) + .setType(WatchNextPrograms.TYPE_MOVIE) + .setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE) + .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_2_3) + .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9) + .setLogoUri(Uri.parse("http://example.com/program-logo.mpg")) + .setAvailability(WatchNextPrograms.AVAILABILITY_AVAILABLE) + .setStartingPrice("12.99 USD") + .setOfferPrice("4.99 USD") + .setReleaseDate("1997") + .setItemCount(3) + .setLive(false) + .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_LIKES) + .setInteractionCount(10200) + .setAuthor("author_name") + .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_STARS) + .setReviewRating("4.5") + .setSearchable(false) + .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png")) + .setAudioLanguages(new String [] {"eng", "kor"}) + .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES}) + .setContentRatings(new TvContentRating[] { + TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")}) + .setDescription("This is a sample program") + .setEpisodeNumber("Pilot", 0) + .setEpisodeTitle("Hello World") + .setLongDescription("This is a longer description than the previous description") + .setPosterArtUri(Uri.parse("http://example.com/poster.png")) + .setSeasonNumber("The Final Season", 7) + .setSeasonTitle("The Final Season") + .setVideoHeight(1080) + .setVideoWidth(1920) + .setInternalProviderFlag1(0x4) + .setInternalProviderFlag2(0x3) + .setInternalProviderFlag3(0x2) + .setInternalProviderFlag4(0x1) + .setBrowsable(true) + .setContentId("CID-8442") + .build(); + } + + private static void compareProgram(WatchNextProgram programA, WatchNextProgram programB, + boolean includeIdAndProtectedFields) { + assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages())); + assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres())); + assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings())); + assertEquals(programA.getDescription(), programB.getDescription()); + assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber()); + assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle()); + assertEquals(programA.getLongDescription(), programB.getLongDescription()); + assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri()); + assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber()); + assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri()); + assertEquals(programA.getTitle(), programB.getTitle()); + assertEquals(programA.getVideoHeight(), programB.getVideoHeight()); + assertEquals(programA.getVideoWidth(), programB.getVideoWidth()); + assertEquals(programA.isSearchable(), programB.isSearchable()); + assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1()); + assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2()); + assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3()); + assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4()); + assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle())); + assertEquals(programA.getInternalProviderId(), programB.getInternalProviderId()); + assertEquals(programA.getPreviewVideoUri(), programB.getPreviewVideoUri()); + assertEquals(programA.getLastPlaybackPositionMillis(), + programB.getLastPlaybackPositionMillis()); + assertEquals(programA.getDurationMillis(), programB.getDurationMillis()); + assertEquals(programA.getIntentUri(), programB.getIntentUri()); + assertEquals(programA.isTransient(), programB.isTransient()); + assertEquals(programA.getType(), programB.getType()); + assertEquals(programA.getWatchNextType(), programB.getWatchNextType()); + assertEquals(programA.getPosterArtAspectRatio(), programB.getPosterArtAspectRatio()); + assertEquals(programA.getThumbnailAspectRatio(), programB.getThumbnailAspectRatio()); + assertEquals(programA.getLogoUri(), programB.getLogoUri()); + assertEquals(programA.getAvailability(), programB.getAvailability()); + assertEquals(programA.getStartingPrice(), programB.getStartingPrice()); + assertEquals(programA.getOfferPrice(), programB.getOfferPrice()); + assertEquals(programA.getReleaseDate(), programB.getReleaseDate()); + assertEquals(programA.getItemCount(), programB.getItemCount()); + assertEquals(programA.isLive(), programB.isLive()); + assertEquals(programA.getInteractionType(), programB.getInteractionType()); + assertEquals(programA.getInteractionCount(), programB.getInteractionCount()); + assertEquals(programA.getAuthor(), programB.getAuthor()); + assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle()); + assertEquals(programA.getReviewRating(), programB.getReviewRating()); + assertEquals(programA.getContentId(), programB.getContentId()); + if (includeIdAndProtectedFields) { + // Skip row ID since the one from system DB has the valid ID while the other does not. + assertEquals(programA.getId(), programB.getId()); + // When we insert a channel using toContentValues() to the system, we drop some + // protected fields since they only can be modified by system apps. + assertEquals(programA.isBrowsable(), programB.isBrowsable()); + assertEquals(programA.toContentValues(), programB.toContentValues()); + assertEquals(programA, programB); + } + } + + private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) { + MatrixCursor cursor = new MatrixCursor(projection); + MatrixCursor.RowBuilder builder = cursor.newRow(); + for (String col : projection) { + if (col != null) { + builder.add(col, contentValues.get(col)); + } + } + cursor.moveToFirst(); + return cursor; + } +} diff --git a/android/support/mediacompat/testlib/util/PollingCheck.java b/android/support/mediacompat/testlib/util/PollingCheck.java index 3412da02..47344c03 100644 --- a/android/support/mediacompat/testlib/util/PollingCheck.java +++ b/android/support/mediacompat/testlib/util/PollingCheck.java @@ -16,7 +16,7 @@ package android.support.mediacompat.testlib.util; -import junit.framework.Assert; +import static org.junit.Assert.fail; /** * Utility used for testing that allows to poll for a certain condition to happen within a timeout. @@ -56,7 +56,7 @@ public abstract class PollingCheck { try { Thread.sleep(TIME_SLICE); } catch (InterruptedException e) { - Assert.fail("unexpected InterruptedException"); + fail("unexpected InterruptedException"); } if (check()) { @@ -66,7 +66,7 @@ public abstract class PollingCheck { timeout -= TIME_SLICE; } - Assert.fail("unexpected timeout"); + fail("unexpected timeout"); } /** diff --git a/android/support/mediacompat/testlib/util/TestUtil.java b/android/support/mediacompat/testlib/util/TestUtil.java index d105510c..21fd223e 100644 --- a/android/support/mediacompat/testlib/util/TestUtil.java +++ b/android/support/mediacompat/testlib/util/TestUtil.java @@ -16,8 +16,8 @@ package android.support.mediacompat.testlib.util; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertSame; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import android.os.Bundle; diff --git a/android/support/text/emoji/EmojiCompat.java b/android/support/text/emoji/EmojiCompat.java index 5436aa20..413a9dd4 100644 --- a/android/support/text/emoji/EmojiCompat.java +++ b/android/support/text/emoji/EmojiCompat.java @@ -183,6 +183,16 @@ public class EmojiCompat { */ private final boolean mReplaceAll; + /** + * @see Config#setUseEmojiAsDefaultStyle(boolean) + */ + private final boolean mUseEmojiAsDefaultStyle; + + /** + * @see Config#setUseEmojiAsDefaultStyle(boolean, List) + */ + private final int[] mEmojiAsDefaultStyleExceptions; + /** * @see Config#setEmojiSpanIndicatorEnabled(boolean) */ @@ -201,6 +211,8 @@ public class EmojiCompat { private EmojiCompat(@NonNull final Config config) { mInitLock = new ReentrantReadWriteLock(); mReplaceAll = config.mReplaceAll; + mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle; + mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions; mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled; mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor; mMetadataLoader = config.mMetadataLoader; @@ -787,6 +799,8 @@ public class EmojiCompat { public abstract static class Config { private final MetadataRepoLoader mMetadataLoader; private boolean mReplaceAll; + private boolean mUseEmojiAsDefaultStyle; + private int[] mEmojiAsDefaultStyleExceptions; private Set mInitCallbacks; private boolean mEmojiSpanIndicatorEnabled; private int mEmojiSpanIndicatorColor = Color.GREEN; @@ -848,6 +862,56 @@ public class EmojiCompat { return this; } + /** + * Determines whether EmojiCompat should use the emoji presentation style for emojis + * that have text style as default. By default, the text style would be used, unless these + * are followed by the U+FE0F variation selector. + * Details about emoji presentation and text presentation styles can be found here: + * http://unicode.org/reports/tr51/#Presentation_Style + * If useEmojiAsDefaultStyle is true, the emoji presentation style will be used for all + * emojis, including potentially unexpected ones (such as digits or other keycap emojis). If + * this is not the expected behaviour, method + * {@link #setUseEmojiAsDefaultStyle(boolean, List)} can be used to specify the + * exception emojis that should be still presented as text style. + * + * @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis + * that would be presented as text style by default + */ + public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) { + return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle, null); + } + + /** + * @see #setUseEmojiAsDefaultStyle(boolean) + * + * @param emojiAsDefaultStyleExceptions Contains the exception emojis which will be still + * presented as text style even if the + * useEmojiAsDefaultStyle flag is set to {@code true}. + * This list will be ignored if useEmojiAsDefaultStyle + * is {@code false}. Note that emojis with default + * emoji style presentation will remain emoji style + * regardless the value of useEmojiAsDefaultStyle or + * whether they are included in the exceptions list or + * not. When no exception is wanted, the method + * {@link #setUseEmojiAsDefaultStyle(boolean)} should + * be used instead. + */ + public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle, + @Nullable final List emojiAsDefaultStyleExceptions) { + mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle; + if (mUseEmojiAsDefaultStyle && emojiAsDefaultStyleExceptions != null) { + mEmojiAsDefaultStyleExceptions = new int[emojiAsDefaultStyleExceptions.size()]; + int i = 0; + for (Integer exception : emojiAsDefaultStyleExceptions) { + mEmojiAsDefaultStyleExceptions[i++] = exception; + } + Arrays.sort(mEmojiAsDefaultStyleExceptions); + } else { + mEmojiAsDefaultStyleExceptions = null; + } + return this; + } + /** * Determines whether a background will be drawn for the emojis that are found and * replaced by EmojiCompat. Should be used only for debugging purposes. The indicator color @@ -1020,7 +1084,9 @@ public class EmojiCompat { } mMetadataRepo = metadataRepo; - mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory()); + mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory(), + mEmojiCompat.mUseEmojiAsDefaultStyle, + mEmojiCompat.mEmojiAsDefaultStyleExceptions); mEmojiCompat.onMetadataLoadSuccess(); } diff --git a/android/support/text/emoji/EmojiMetadata.java b/android/support/text/emoji/EmojiMetadata.java index 488dcf9b..7d495ede 100644 --- a/android/support/text/emoji/EmojiMetadata.java +++ b/android/support/text/emoji/EmojiMetadata.java @@ -26,12 +26,13 @@ import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; -import android.support.text.emoji.flatbuffer.MetadataItem; -import android.support.text.emoji.flatbuffer.MetadataList; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import androidx.text.emoji.flatbuffer.MetadataItem; +import androidx.text.emoji.flatbuffer.MetadataList; + /** * Information about a single emoji. * diff --git a/android/support/text/emoji/EmojiProcessor.java b/android/support/text/emoji/EmojiProcessor.java index 3feb36d6..f711704e 100644 --- a/android/support/text/emoji/EmojiProcessor.java +++ b/android/support/text/emoji/EmojiProcessor.java @@ -22,6 +22,7 @@ import android.support.annotation.AnyThread; import android.support.annotation.IntDef; import android.support.annotation.IntRange; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.text.emoji.widget.SpannableBuilder; @@ -40,6 +41,8 @@ import android.view.inputmethod.InputConnection; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; /** * Processes the CharSequence and adds the emojis. @@ -90,14 +93,29 @@ final class EmojiProcessor { */ private GlyphChecker mGlyphChecker = new GlyphChecker(); + /** + * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean) + */ + private final boolean mUseEmojiAsDefaultStyle; + + /** + * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List) + */ + private final int[] mEmojiAsDefaultStyleExceptions; + EmojiProcessor(@NonNull final MetadataRepo metadataRepo, - @NonNull final EmojiCompat.SpanFactory spanFactory) { + @NonNull final EmojiCompat.SpanFactory spanFactory, + final boolean useEmojiAsDefaultStyle, + @Nullable final int[] emojiAsDefaultStyleExceptions) { mSpanFactory = spanFactory; mMetadataRepo = metadataRepo; + mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle; + mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions; } EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) { - final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode()); + final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(), + mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions); final int end = charSequence.length(); int currentOffset = 0; @@ -189,7 +207,8 @@ final class EmojiProcessor { } // add new ones int addedCount = 0; - final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode()); + final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(), + mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions); int currentOffset = start; int codePoint = Character.codePointAt(charSequence, currentOffset); @@ -483,9 +502,22 @@ final class EmojiProcessor { */ private int mCurrentDepth; - ProcessorSm(MetadataRepo.Node rootNode) { + /** + * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean) + */ + private final boolean mUseEmojiAsDefaultStyle; + + /** + * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List) + */ + private final int[] mEmojiAsDefaultStyleExceptions; + + ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle, + int[] emojiAsDefaultStyleExceptions) { mRootNode = rootNode; mCurrentNode = rootNode; + mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle; + mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions; } @Action @@ -505,8 +537,7 @@ final class EmojiProcessor { action = ACTION_ADVANCE_END; } else if (mCurrentNode.getData() != null) { if (mCurrentDepth == 1) { - if (mCurrentNode.getData().isDefaultEmoji() - || isEmojiStyle(mLastCodepoint)) { + if (shouldUseEmojiPresentationStyleForSingleCodepoint()) { mFlushNode = mCurrentNode; action = ACTION_FLUSH; reset(); @@ -571,9 +602,32 @@ final class EmojiProcessor { */ boolean isInFlushableState() { return mState == STATE_WALKING && mCurrentNode.getData() != null - && (mCurrentNode.getData().isDefaultEmoji() - || isEmojiStyle(mLastCodepoint) - || mCurrentDepth > 1); + && (mCurrentDepth > 1 || shouldUseEmojiPresentationStyleForSingleCodepoint()); + } + + private boolean shouldUseEmojiPresentationStyleForSingleCodepoint() { + if (mCurrentNode.getData().isDefaultEmoji()) { + // The codepoint is emoji style by default. + return true; + } + if (isEmojiStyle(mLastCodepoint)) { + // The codepoint was followed by the emoji style variation selector. + return true; + } + if (mUseEmojiAsDefaultStyle) { + // Emoji presentation style for text style default emojis is enabled. We have + // to check that the current codepoint is not an exception. + if (mEmojiAsDefaultStyleExceptions == null) { + return true; + } + final int codepoint = mCurrentNode.getData().getCodepointAt(0); + final int index = Arrays.binarySearch(mEmojiAsDefaultStyleExceptions, codepoint); + if (index < 0) { + // Index is negative, so the codepoint was not found in the array of exceptions. + return true; + } + } + return false; } /** diff --git a/android/support/text/emoji/MetadataListReader.java b/android/support/text/emoji/MetadataListReader.java index 1008c171..02856cb1 100644 --- a/android/support/text/emoji/MetadataListReader.java +++ b/android/support/text/emoji/MetadataListReader.java @@ -22,13 +22,14 @@ import android.support.annotation.AnyThread; import android.support.annotation.IntRange; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; -import android.support.text.emoji.flatbuffer.MetadataList; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import androidx.text.emoji.flatbuffer.MetadataList; + /** * Reads the emoji metadata from a given InputStream or ByteBuffer. * diff --git a/android/support/text/emoji/MetadataRepo.java b/android/support/text/emoji/MetadataRepo.java index e86277e5..f5afec86 100644 --- a/android/support/text/emoji/MetadataRepo.java +++ b/android/support/text/emoji/MetadataRepo.java @@ -24,7 +24,6 @@ import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; -import android.support.text.emoji.flatbuffer.MetadataList; import android.support.v4.util.Preconditions; import android.util.SparseArray; @@ -32,6 +31,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import androidx.text.emoji.flatbuffer.MetadataList; + /** * Class to hold the emoji metadata required to process and draw emojis. */ diff --git a/android/support/text/emoji/widget/EmojiButton.java b/android/support/text/emoji/widget/EmojiButton.java index 65afd9c7..752e0523 100644 --- a/android/support/text/emoji/widget/EmojiButton.java +++ b/android/support/text/emoji/widget/EmojiButton.java @@ -15,9 +15,9 @@ */ package android.support.text.emoji.widget; -import android.annotation.TargetApi; import android.content.Context; import android.os.Build; +import android.support.annotation.RequiresApi; import android.text.InputFilter; import android.util.AttributeSet; import android.widget.Button; @@ -50,7 +50,7 @@ public class EmojiButton extends Button { init(); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); diff --git a/android/support/text/emoji/widget/EmojiEditText.java b/android/support/text/emoji/widget/EmojiEditText.java index 70ca7a66..e1057e8f 100644 --- a/android/support/text/emoji/widget/EmojiEditText.java +++ b/android/support/text/emoji/widget/EmojiEditText.java @@ -15,11 +15,11 @@ */ package android.support.text.emoji.widget; -import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.support.annotation.IntRange; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.text.emoji.EmojiCompat; import android.text.method.KeyListener; import android.util.AttributeSet; @@ -57,7 +57,7 @@ public class EmojiEditText extends EditText { init(attrs, defStyleAttr, 0 /*defStyleRes*/); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(attrs, defStyleAttr, defStyleRes); diff --git a/android/support/text/emoji/widget/EmojiExtractEditText.java b/android/support/text/emoji/widget/EmojiExtractEditText.java index 2e4d3caa..1d5e2dd7 100644 --- a/android/support/text/emoji/widget/EmojiExtractEditText.java +++ b/android/support/text/emoji/widget/EmojiExtractEditText.java @@ -18,12 +18,12 @@ package android.support.text.emoji.widget; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.Context; import android.inputmethodservice.ExtractEditText; import android.os.Build; import android.support.annotation.IntRange; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.text.emoji.EmojiCompat; import android.support.text.emoji.EmojiSpan; @@ -64,7 +64,7 @@ public class EmojiExtractEditText extends ExtractEditText { init(attrs, defStyleAttr, 0 /*defStyleRes*/); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); diff --git a/android/support/transition/ArcMotionTest.java b/android/support/transition/ArcMotionTest.java new file mode 100644 index 00000000..75d61173 --- /dev/null +++ b/android/support/transition/ArcMotionTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; + +import android.graphics.Path; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ArcMotionTest extends PathMotionTest { + + @Test + public void test90Quadrants() { + ArcMotion arcMotion = new ArcMotion(); + arcMotion.setMaximumAngle(90); + + Path expected = arcWithPoint(0, 100, 100, 0, 100, 100); + Path path = arcMotion.getPath(0, 100, 100, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(100, 0, 0, -100, 0, 0); + path = arcMotion.getPath(100, 0, 0, -100); + assertPathMatches(expected, path); + + expected = arcWithPoint(0, -100, -100, 0, 0, 0); + path = arcMotion.getPath(0, -100, -100, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(-100, 0, 0, 100, -100, 100); + path = arcMotion.getPath(-100, 0, 0, 100); + assertPathMatches(expected, path); + } + + @Test + public void test345Triangles() { + // 3-4-5 triangles are easy to calculate the control points + ArcMotion arcMotion = new ArcMotion(); + arcMotion.setMaximumAngle(90); + Path expected; + Path path; + + expected = arcWithPoint(0, 120, 160, 0, 125, 120); + path = arcMotion.getPath(0, 120, 160, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(0, 160, 120, 0, 120, 125); + path = arcMotion.getPath(0, 160, 120, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(-120, 0, 0, 160, -120, 125); + path = arcMotion.getPath(-120, 0, 0, 160); + assertPathMatches(expected, path); + + expected = arcWithPoint(-160, 0, 0, 120, -125, 120); + path = arcMotion.getPath(-160, 0, 0, 120); + assertPathMatches(expected, path); + + expected = arcWithPoint(0, -120, -160, 0, -35, 0); + path = arcMotion.getPath(0, -120, -160, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(0, -160, -120, 0, 0, -35); + path = arcMotion.getPath(0, -160, -120, 0); + assertPathMatches(expected, path); + + expected = arcWithPoint(120, 0, 0, -160, 0, -35); + path = arcMotion.getPath(120, 0, 0, -160); + assertPathMatches(expected, path); + + expected = arcWithPoint(160, 0, 0, -120, 35, 0); + path = arcMotion.getPath(160, 0, 0, -120); + assertPathMatches(expected, path); + } + + private static Path arcWithPoint(float startX, float startY, float endX, float endY, + float eX, float eY) { + float c1x = (eX + startX) / 2; + float c1y = (eY + startY) / 2; + float c2x = (eX + endX) / 2; + float c2y = (eY + endY) / 2; + Path path = new Path(); + path.moveTo(startX, startY); + path.cubicTo(c1x, c1y, c2x, c2y, endX, endY); + return path; + } + + @Test + public void testMaximumAngle() { + ArcMotion arcMotion = new ArcMotion(); + arcMotion.setMaximumAngle(45f); + assertEquals(45f, arcMotion.getMaximumAngle(), 0.0f); + + float ratio = (float) Math.tan(Math.PI / 8); + float ex = 50 + (50 * ratio); + float ey = ex; + + Path expected = arcWithPoint(0, 100, 100, 0, ex, ey); + Path path = arcMotion.getPath(0, 100, 100, 0); + assertPathMatches(expected, path); + } + + @Test + public void testMinimumHorizontalAngle() { + ArcMotion arcMotion = new ArcMotion(); + arcMotion.setMinimumHorizontalAngle(45); + assertEquals(45, arcMotion.getMinimumHorizontalAngle(), 0.0f); + + float ex = 37.5f; + float ey = (float) (Math.tan(Math.PI / 4) * 50); + Path expected = arcWithPoint(0, 0, 100, 50, ex, ey); + Path path = arcMotion.getPath(0, 0, 100, 50); + assertPathMatches(expected, path); + + // Pretty much the same, but follows a different path. + expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey); + path = arcMotion.getPath(0, 0, 100.001f, 50); + assertPathMatches(expected, path); + + // Moving in the opposite direction. + expected = arcWithPoint(100, 50, 0, 0, ex, ey); + path = arcMotion.getPath(100, 50, 0, 0); + assertPathMatches(expected, path); + + // With x < y. + ex = 0; + ey = (float) (Math.tan(Math.PI / 4) * 62.5f); + expected = arcWithPoint(0, 0, 50, 100, ex, ey); + path = arcMotion.getPath(0, 0, 50, 100); + assertPathMatches(expected, path); + + // Pretty much the same, but follows a different path. + expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey); + path = arcMotion.getPath(0, 0, 50, 100.001f); + assertPathMatches(expected, path); + + // Moving in the opposite direction. + expected = arcWithPoint(50, 100, 0, 0, ex, ey); + path = arcMotion.getPath(50, 100, 0, 0); + assertPathMatches(expected, path); + } + + @Test + public void testMinimumVerticalAngle() { + ArcMotion arcMotion = new ArcMotion(); + arcMotion.setMinimumVerticalAngle(45); + assertEquals(45, arcMotion.getMinimumVerticalAngle(), 0.0f); + + float ex = 0; + float ey = 62.5f; + Path expected = arcWithPoint(0, 0, 50, 100, ex, ey); + Path path = arcMotion.getPath(0, 0, 50, 100); + assertPathMatches(expected, path); + + // Pretty much the same, but follows a different path. + expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey); + path = arcMotion.getPath(0, 0, 50, 100.001f); + assertPathMatches(expected, path); + + // Moving in opposite direction. + expected = arcWithPoint(50, 100, 0, 0, ex, ey); + path = arcMotion.getPath(50, 100, 0, 0); + assertPathMatches(expected, path); + + // With x > y. + ex = (float) (Math.tan(Math.PI / 4) * 37.5f); + ey = 50; + expected = arcWithPoint(0, 0, 100, 50, ex, ey); + path = arcMotion.getPath(0, 0, 100, 50); + assertPathMatches(expected, path); + + // Pretty much the same, but follows a different path. + expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey); + path = arcMotion.getPath(0, 0, 100.001f, 50); + assertPathMatches(expected, path); + + // Moving in opposite direction. + expected = arcWithPoint(100, 50, 0, 0, ex, ey); + path = arcMotion.getPath(100, 50, 0, 0); + assertPathMatches(expected, path); + + } + +} diff --git a/android/support/transition/AutoTransitionTest.java b/android/support/transition/AutoTransitionTest.java new file mode 100644 index 00000000..2c9a77f5 --- /dev/null +++ b/android/support/transition/AutoTransitionTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import android.graphics.Color; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.LargeTest; +import android.support.test.filters.MediumTest; +import android.view.View; +import android.widget.LinearLayout; + +import org.junit.Before; +import org.junit.Test; + +@MediumTest +public class AutoTransitionTest extends BaseTest { + + private LinearLayout mRoot; + private View mView0; + private View mView1; + + @UiThreadTest + @Before + public void setUp() { + mRoot = (LinearLayout) rule.getActivity().getRoot(); + mView0 = new View(rule.getActivity()); + mView0.setBackgroundColor(Color.RED); + mRoot.addView(mView0, new LinearLayout.LayoutParams(100, 100)); + mView1 = new View(rule.getActivity()); + mView1.setBackgroundColor(Color.BLUE); + mRoot.addView(mView1, new LinearLayout.LayoutParams(100, 100)); + } + + @LargeTest + @Test + public void testLayoutBetweenFadeAndChangeBounds() throws Throwable { + final LayoutCounter counter = new LayoutCounter(); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + assertThat(mView1.getY(), is(100.f)); + assertThat(mView0.getVisibility(), is(View.VISIBLE)); + mView1.addOnLayoutChangeListener(counter); + } + }); + final SyncTransitionListener listener = new SyncTransitionListener( + SyncTransitionListener.EVENT_END); + final Transition transition = new AutoTransition(); + transition.addListener(listener); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, transition); + // This makes view0 fade out and causes view1 to move upwards. + mView0.setVisibility(View.GONE); + } + }); + assertThat("Timed out waiting for the TransitionListener", + listener.await(), is(true)); + assertThat(mView1.getY(), is(0.f)); + assertThat(mView0.getVisibility(), is(View.GONE)); + counter.reset(); + listener.reset(); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, transition); + // Revert + mView0.setVisibility(View.VISIBLE); + } + }); + assertThat("Timed out waiting for the TransitionListener", + listener.await(), is(true)); + assertThat(mView1.getY(), is(100.f)); + assertThat(mView0.getVisibility(), is(View.VISIBLE)); + } + + private static class LayoutCounter implements View.OnLayoutChangeListener { + + private int mCalledCount; + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mCalledCount++; + // There should not be more than one layout request to view1. + if (mCalledCount > 1) { + fail("View layout happened too many times"); + } + } + + void reset() { + mCalledCount = 0; + } + + } + +} diff --git a/android/support/transition/BaseTest.java b/android/support/transition/BaseTest.java new file mode 100644 index 00000000..4ffb2f90 --- /dev/null +++ b/android/support/transition/BaseTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public abstract class BaseTest { + + @Rule + public final ActivityTestRule rule; + + BaseTest() { + rule = new ActivityTestRule<>(TransitionActivity.class); + } + +} diff --git a/android/support/transition/BaseTransitionTest.java b/android/support/transition/BaseTransitionTest.java new file mode 100644 index 00000000..5d39d943 --- /dev/null +++ b/android/support/transition/BaseTransitionTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.support.test.InstrumentationRegistry; +import android.support.transition.test.R; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import org.junit.Before; + +import java.util.ArrayList; + +public abstract class BaseTransitionTest extends BaseTest { + + ArrayList mTransitionTargets = new ArrayList<>(); + LinearLayout mRoot; + Transition mTransition; + Transition.TransitionListener mListener; + float mAnimatedValue; + + @Before + public void setUp() { + InstrumentationRegistry.getInstrumentation().setInTouchMode(false); + mRoot = (LinearLayout) rule.getActivity().findViewById(R.id.root); + mTransitionTargets.clear(); + mTransition = createTransition(); + mListener = mock(Transition.TransitionListener.class); + mTransition.addListener(mListener); + } + + Transition createTransition() { + return new TestTransition(); + } + + void waitForStart() { + verify(mListener, timeout(3000)).onTransitionStart(any(Transition.class)); + } + + void waitForEnd() { + verify(mListener, timeout(3000)).onTransitionEnd(any(Transition.class)); + } + + Scene loadScene(final int layoutId) throws Throwable { + final Scene[] scene = new Scene[1]; + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + scene[0] = Scene.getSceneForLayout(mRoot, layoutId, rule.getActivity()); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + return scene[0]; + } + + void startTransition(final int layoutId) throws Throwable { + startTransition(loadScene(layoutId)); + } + + private void startTransition(final Scene scene) throws Throwable { + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(scene, mTransition); + } + }); + waitForStart(); + } + + void enterScene(final int layoutId) throws Throwable { + enterScene(loadScene(layoutId)); + } + + void enterScene(final Scene scene) throws Throwable { + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + scene.enter(); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + void resetListener() { + mTransition.removeListener(mListener); + mListener = mock(Transition.TransitionListener.class); + mTransition.addListener(mListener); + } + + void setAnimatedValue(float animatedValue) { + mAnimatedValue = animatedValue; + } + + public class TestTransition extends Visibility { + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { + mTransitionTargets.add(endValues.view); + return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 0, 1); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { + mTransitionTargets.add(startValues.view); + return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 1, 0); + } + + } + +} diff --git a/android/support/transition/ChangeBoundsTest.java b/android/support/transition/ChangeBoundsTest.java new file mode 100644 index 00000000..186017cf --- /dev/null +++ b/android/support/transition/ChangeBoundsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; +import android.view.animation.LinearInterpolator; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Test; + +@MediumTest +public class ChangeBoundsTest extends BaseTransitionTest { + + @Override + Transition createTransition() { + final ChangeBounds changeBounds = new ChangeBounds(); + changeBounds.setDuration(400); + changeBounds.setInterpolator(new LinearInterpolator()); + return changeBounds; + } + + @Test + public void testResizeClip() { + ChangeBounds changeBounds = (ChangeBounds) mTransition; + assertThat(changeBounds.getResizeClip(), is(false)); + changeBounds.setResizeClip(true); + assertThat(changeBounds.getResizeClip(), is(true)); + } + + @Test + public void testBasic() throws Throwable { + enterScene(R.layout.scene1); + final ViewHolder startHolder = new ViewHolder(rule.getActivity()); + assertThat(startHolder.red, is(atTop())); + assertThat(startHolder.green, is(below(startHolder.red))); + startTransition(R.layout.scene6); + waitForEnd(); + final ViewHolder endHolder = new ViewHolder(rule.getActivity()); + assertThat(endHolder.green, is(atTop())); + assertThat(endHolder.red, is(below(endHolder.green))); + } + + private static TypeSafeMatcher atTop() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View view) { + return view.getTop() == 0; + } + + @Override + public void describeTo(Description description) { + description.appendText("is placed at the top of its parent"); + } + }; + } + + private static TypeSafeMatcher below(final View other) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View item) { + return other.getBottom() == item.getTop(); + } + + @Override + public void describeTo(Description description) { + description.appendText("is placed below the specified view"); + } + }; + } + + private static class ViewHolder { + + public final View red; + public final View green; + + ViewHolder(TransitionActivity activity) { + red = activity.findViewById(R.id.redSquare); + green = activity.findViewById(R.id.greenSquare); + } + } + +} diff --git a/android/support/transition/ChangeClipBoundsTest.java b/android/support/transition/ChangeClipBoundsTest.java new file mode 100644 index 00000000..c227bcac --- /dev/null +++ b/android/support/transition/ChangeClipBoundsTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import android.support.test.filters.MediumTest; +import android.support.test.filters.SdkSuppress; +import android.support.transition.test.R; +import android.support.v4.view.ViewCompat; +import android.view.View; + +import org.junit.Test; + +@MediumTest +public class ChangeClipBoundsTest extends BaseTransitionTest { + + @Override + Transition createTransition() { + return new ChangeClipBounds(); + } + + @SdkSuppress(minSdkVersion = 18) + @Test + public void testChangeClipBounds() throws Throwable { + enterScene(R.layout.scene1); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + final Rect newClip = new Rect(redSquare.getLeft() + 10, redSquare.getTop() + 10, + redSquare.getRight() - 10, redSquare.getBottom() - 10); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + assertNull(ViewCompat.getClipBounds(redSquare)); + TransitionManager.beginDelayedTransition(mRoot, mTransition); + ViewCompat.setClipBounds(redSquare, newClip); + } + }); + waitForStart(); + Thread.sleep(150); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + Rect midClip = ViewCompat.getClipBounds(redSquare); + assertNotNull(midClip); + assertTrue(midClip.left > 0 && midClip.left < newClip.left); + assertTrue(midClip.top > 0 && midClip.top < newClip.top); + assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right); + assertTrue(midClip.bottom < redSquare.getBottom() + && midClip.bottom > newClip.bottom); + } + }); + waitForEnd(); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + final Rect endRect = ViewCompat.getClipBounds(redSquare); + assertNotNull(endRect); + assertEquals(newClip, endRect); + } + }); + + resetListener(); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + ViewCompat.setClipBounds(redSquare, null); + } + }); + waitForStart(); + Thread.sleep(150); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + Rect midClip = ViewCompat.getClipBounds(redSquare); + assertNotNull(midClip); + assertTrue(midClip.left > 0 && midClip.left < newClip.left); + assertTrue(midClip.top > 0 && midClip.top < newClip.top); + assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right); + assertTrue(midClip.bottom < redSquare.getBottom() + && midClip.bottom > newClip.bottom); + } + }); + waitForEnd(); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + assertNull(ViewCompat.getClipBounds(redSquare)); + } + }); + + } + + @Test + public void dummy() { + // Avoid "No tests found" on older devices + } + +} diff --git a/android/support/transition/ChangeImageTransformTest.java b/android/support/transition/ChangeImageTransformTest.java new file mode 100644 index 00000000..907e01ef --- /dev/null +++ b/android/support/transition/ChangeImageTransformTest.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.support.v4.app.ActivityCompat; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ViewGroup; +import android.widget.ImageView; + +import org.junit.Test; + +@MediumTest +public class ChangeImageTransformTest extends BaseTransitionTest { + + private ChangeImageTransform mChangeImageTransform; + private Matrix mStartMatrix; + private Matrix mEndMatrix; + private Drawable mImage; + private ImageView mImageView; + + @Override + Transition createTransition() { + mChangeImageTransform = new CaptureMatrix(); + mChangeImageTransform.setDuration(100); + mTransition = mChangeImageTransform; + resetListener(); + return mChangeImageTransform; + } + + @Test + public void testCenterToFitXY() throws Throwable { + transformImage(ImageView.ScaleType.CENTER, ImageView.ScaleType.FIT_XY); + verifyMatrixMatches(centerMatrix(), mStartMatrix); + verifyMatrixMatches(fitXYMatrix(), mEndMatrix); + } + + @Test + public void testCenterCropToFitCenter() throws Throwable { + transformImage(ImageView.ScaleType.CENTER_CROP, ImageView.ScaleType.FIT_CENTER); + verifyMatrixMatches(centerCropMatrix(), mStartMatrix); + verifyMatrixMatches(fitCenterMatrix(), mEndMatrix); + } + + @Test + public void testCenterInsideToFitEnd() throws Throwable { + transformImage(ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_END); + // CENTER_INSIDE and CENTER are the same when the image is smaller than the View + verifyMatrixMatches(centerMatrix(), mStartMatrix); + verifyMatrixMatches(fitEndMatrix(), mEndMatrix); + } + + @Test + public void testFitStartToCenter() throws Throwable { + transformImage(ImageView.ScaleType.FIT_START, ImageView.ScaleType.CENTER); + verifyMatrixMatches(fitStartMatrix(), mStartMatrix); + verifyMatrixMatches(centerMatrix(), mEndMatrix); + } + + private Matrix centerMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float tx = Math.round((imageViewWidth - imageWidth) / 2f); + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float ty = Math.round((imageViewHeight - imageHeight) / 2f); + + Matrix matrix = new Matrix(); + matrix.postTranslate(tx, ty); + return matrix; + } + + private Matrix fitXYMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float scaleX = ((float) imageViewWidth) / imageWidth; + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float scaleY = ((float) imageViewHeight) / imageHeight; + + Matrix matrix = new Matrix(); + matrix.postScale(scaleX, scaleY); + return matrix; + } + + private Matrix centerCropMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float scaleX = ((float) imageViewWidth) / imageWidth; + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float scaleY = ((float) imageViewHeight) / imageHeight; + + float maxScale = Math.max(scaleX, scaleY); + + float width = imageWidth * maxScale; + float height = imageHeight * maxScale; + int tx = Math.round((imageViewWidth - width) / 2f); + int ty = Math.round((imageViewHeight - height) / 2f); + + Matrix matrix = new Matrix(); + matrix.postScale(maxScale, maxScale); + matrix.postTranslate(tx, ty); + return matrix; + } + + private Matrix fitCenterMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float scaleX = ((float) imageViewWidth) / imageWidth; + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float scaleY = ((float) imageViewHeight) / imageHeight; + + float minScale = Math.min(scaleX, scaleY); + + float width = imageWidth * minScale; + float height = imageHeight * minScale; + float tx = (imageViewWidth - width) / 2f; + float ty = (imageViewHeight - height) / 2f; + + Matrix matrix = new Matrix(); + matrix.postScale(minScale, minScale); + matrix.postTranslate(tx, ty); + return matrix; + } + + private Matrix fitStartMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float scaleX = ((float) imageViewWidth) / imageWidth; + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float scaleY = ((float) imageViewHeight) / imageHeight; + + float minScale = Math.min(scaleX, scaleY); + + Matrix matrix = new Matrix(); + matrix.postScale(minScale, minScale); + return matrix; + } + + private Matrix fitEndMatrix() { + int imageWidth = mImage.getIntrinsicWidth(); + int imageViewWidth = mImageView.getWidth(); + float scaleX = ((float) imageViewWidth) / imageWidth; + + int imageHeight = mImage.getIntrinsicHeight(); + int imageViewHeight = mImageView.getHeight(); + float scaleY = ((float) imageViewHeight) / imageHeight; + + float minScale = Math.min(scaleX, scaleY); + + float width = imageWidth * minScale; + float height = imageHeight * minScale; + float tx = imageViewWidth - width; + float ty = imageViewHeight - height; + + Matrix matrix = new Matrix(); + matrix.postScale(minScale, minScale); + matrix.postTranslate(tx, ty); + return matrix; + } + + private void verifyMatrixMatches(Matrix expected, Matrix matrix) { + if (expected == null) { + assertNull(matrix); + return; + } + assertNotNull(matrix); + float[] expectedValues = new float[9]; + expected.getValues(expectedValues); + + float[] values = new float[9]; + matrix.getValues(values); + + for (int i = 0; i < values.length; i++) { + final float expectedValue = expectedValues[i]; + final float value = values[i]; + assertEquals("Value [" + i + "]", expectedValue, value, 0.01f); + } + } + + private void transformImage(ImageView.ScaleType startScale, final ImageView.ScaleType endScale) + throws Throwable { + final ImageView imageView = enterImageViewScene(startScale); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mChangeImageTransform); + imageView.setScaleType(endScale); + } + }); + waitForStart(); + verify(mListener, (startScale == endScale) ? times(1) : never()) + .onTransitionEnd(any(Transition.class)); + waitForEnd(); + } + + private ImageView enterImageViewScene(final ImageView.ScaleType scaleType) throws Throwable { + enterScene(R.layout.scene4); + final ViewGroup container = (ViewGroup) rule.getActivity().findViewById(R.id.holder); + final ImageView[] imageViews = new ImageView[1]; + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + mImageView = new ImageView(rule.getActivity()); + mImage = ActivityCompat.getDrawable(rule.getActivity(), + android.R.drawable.ic_media_play); + mImageView.setImageDrawable(mImage); + mImageView.setScaleType(scaleType); + imageViews[0] = mImageView; + container.addView(mImageView); + ViewGroup.LayoutParams layoutParams = mImageView.getLayoutParams(); + DisplayMetrics metrics = rule.getActivity().getResources().getDisplayMetrics(); + float size = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, metrics); + layoutParams.width = Math.round(size); + layoutParams.height = Math.round(size * 2); + mImageView.setLayoutParams(layoutParams); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + return imageViews[0]; + } + + private class CaptureMatrix extends ChangeImageTransform { + + @Override + public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + Animator animator = super.createAnimator(sceneRoot, startValues, endValues); + assertNotNull(animator); + animator.addListener(new CaptureMatrixListener((ImageView) endValues.view)); + return animator; + } + + } + + private class CaptureMatrixListener extends AnimatorListenerAdapter { + + private final ImageView mImageView; + + CaptureMatrixListener(ImageView view) { + mImageView = view; + } + + @Override + public void onAnimationStart(Animator animation) { + mStartMatrix = copyMatrix(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mEndMatrix = copyMatrix(); + } + + private Matrix copyMatrix() { + Matrix matrix = mImageView.getImageMatrix(); + if (matrix != null) { + matrix = new Matrix(matrix); + } + return matrix; + } + + } + +} diff --git a/android/support/transition/ChangeScrollTest.java b/android/support/transition/ChangeScrollTest.java new file mode 100644 index 00000000..0f383d3f --- /dev/null +++ b/android/support/transition/ChangeScrollTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; + +import org.junit.Test; + +@MediumTest +public class ChangeScrollTest extends BaseTransitionTest { + + @Override + Transition createTransition() { + return new ChangeScroll(); + } + + @Test + public void testChangeScroll() throws Throwable { + enterScene(R.layout.scene5); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + final View view = rule.getActivity().findViewById(R.id.text); + assertEquals(0, view.getScrollX()); + assertEquals(0, view.getScrollY()); + TransitionManager.beginDelayedTransition(mRoot, mTransition); + view.scrollTo(150, 300); + } + }); + waitForStart(); + Thread.sleep(150); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + final View view = rule.getActivity().findViewById(R.id.text); + final int scrollX = view.getScrollX(); + final int scrollY = view.getScrollY(); + assertThat(scrollX, is(both(greaterThan(0)).and(lessThan(150)))); + assertThat(scrollY, is(both(greaterThan(0)).and(lessThan(300)))); + } + }); + waitForEnd(); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + final View view = rule.getActivity().findViewById(R.id.text); + assertEquals(150, view.getScrollX()); + assertEquals(300, view.getScrollY()); + } + }); + } + +} diff --git a/android/support/transition/ChangeTransformTest.java b/android/support/transition/ChangeTransformTest.java new file mode 100644 index 00000000..3e543aa2 --- /dev/null +++ b/android/support/transition/ChangeTransformTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; + +import org.junit.Test; + +@MediumTest +public class ChangeTransformTest extends BaseTransitionTest { + + @Override + Transition createTransition() { + return new ChangeTransform(); + } + + @Test + public void testTranslation() throws Throwable { + enterScene(R.layout.scene1); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + redSquare.setTranslationX(500); + redSquare.setTranslationY(600); + } + }); + waitForStart(); + + verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running + // There is no way to validate the intermediate matrix because it uses + // hidden properties of the View to execute. + waitForEnd(); + assertEquals(500f, redSquare.getTranslationX(), 0.0f); + assertEquals(600f, redSquare.getTranslationY(), 0.0f); + } + + @Test + public void testRotation() throws Throwable { + enterScene(R.layout.scene1); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + redSquare.setRotation(45); + } + }); + waitForStart(); + + verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running + // There is no way to validate the intermediate matrix because it uses + // hidden properties of the View to execute. + waitForEnd(); + assertEquals(45f, redSquare.getRotation(), 0.0f); + } + + @Test + public void testScale() throws Throwable { + enterScene(R.layout.scene1); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + redSquare.setScaleX(2f); + redSquare.setScaleY(3f); + } + }); + waitForStart(); + + verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running + // There is no way to validate the intermediate matrix because it uses + // hidden properties of the View to execute. + waitForEnd(); + assertEquals(2f, redSquare.getScaleX(), 0.0f); + assertEquals(3f, redSquare.getScaleY(), 0.0f); + } + + @Test + public void testReparent() throws Throwable { + final ChangeTransform changeTransform = (ChangeTransform) mTransition; + assertEquals(true, changeTransform.getReparent()); + enterScene(R.layout.scene5); + startTransition(R.layout.scene9); + verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running + waitForEnd(); + + resetListener(); + changeTransform.setReparent(false); + assertEquals(false, changeTransform.getReparent()); + startTransition(R.layout.scene5); + waitForEnd(); // no transition to run because reparent == false + } + +} diff --git a/android/support/transition/CheckCalledRunnable.java b/android/support/transition/CheckCalledRunnable.java new file mode 100644 index 00000000..9eea6080 --- /dev/null +++ b/android/support/transition/CheckCalledRunnable.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +class CheckCalledRunnable implements Runnable { + + private boolean mWasCalled = false; + + @Override + public void run() { + mWasCalled = true; + } + + /** + * @return {@code true} if {@link #run()} was called at least once. + */ + boolean wasCalled() { + return mWasCalled; + } + +} diff --git a/android/support/transition/ExplodeTest.java b/android/support/transition/ExplodeTest.java new file mode 100644 index 00000000..b4215373 --- /dev/null +++ b/android/support/transition/ExplodeTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.os.SystemClock; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; + +import org.junit.Test; + +@MediumTest +public class ExplodeTest extends BaseTransitionTest { + + @Override + Transition createTransition() { + return new Explode(); + } + + @Test + public void testExplode() throws Throwable { + enterScene(R.layout.scene10); + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare); + final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare); + final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + redSquare.setVisibility(View.INVISIBLE); + greenSquare.setVisibility(View.INVISIBLE); + blueSquare.setVisibility(View.INVISIBLE); + yellowSquare.setVisibility(View.INVISIBLE); + } + }); + waitForStart(); + verify(mListener, never()).onTransitionEnd(any(Transition.class)); + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, blueSquare.getVisibility()); + assertEquals(View.VISIBLE, yellowSquare.getVisibility()); + float redStartX = redSquare.getTranslationX(); + float redStartY = redSquare.getTranslationY(); + + SystemClock.sleep(100); + verifyTranslation(redSquare, true, true); + verifyTranslation(greenSquare, false, true); + verifyTranslation(blueSquare, false, false); + verifyTranslation(yellowSquare, true, false); + assertTrue(redStartX > redSquare.getTranslationX()); // moving left + assertTrue(redStartY > redSquare.getTranslationY()); // moving up + waitForEnd(); + + verifyNoTranslation(redSquare); + verifyNoTranslation(greenSquare); + verifyNoTranslation(blueSquare); + verifyNoTranslation(yellowSquare); + assertEquals(View.INVISIBLE, redSquare.getVisibility()); + assertEquals(View.INVISIBLE, greenSquare.getVisibility()); + assertEquals(View.INVISIBLE, blueSquare.getVisibility()); + assertEquals(View.INVISIBLE, yellowSquare.getVisibility()); + } + + @Test + public void testImplode() throws Throwable { + enterScene(R.layout.scene10); + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare); + final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare); + final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + redSquare.setVisibility(View.INVISIBLE); + greenSquare.setVisibility(View.INVISIBLE); + blueSquare.setVisibility(View.INVISIBLE); + yellowSquare.setVisibility(View.INVISIBLE); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(mRoot, mTransition); + redSquare.setVisibility(View.VISIBLE); + greenSquare.setVisibility(View.VISIBLE); + blueSquare.setVisibility(View.VISIBLE); + yellowSquare.setVisibility(View.VISIBLE); + } + }); + waitForStart(); + + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, blueSquare.getVisibility()); + assertEquals(View.VISIBLE, yellowSquare.getVisibility()); + float redStartX = redSquare.getTranslationX(); + float redStartY = redSquare.getTranslationY(); + + SystemClock.sleep(100); + verifyTranslation(redSquare, true, true); + verifyTranslation(greenSquare, false, true); + verifyTranslation(blueSquare, false, false); + verifyTranslation(yellowSquare, true, false); + assertTrue(redStartX < redSquare.getTranslationX()); // moving right + assertTrue(redStartY < redSquare.getTranslationY()); // moving down + waitForEnd(); + + verifyNoTranslation(redSquare); + verifyNoTranslation(greenSquare); + verifyNoTranslation(blueSquare); + verifyNoTranslation(yellowSquare); + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, blueSquare.getVisibility()); + assertEquals(View.VISIBLE, yellowSquare.getVisibility()); + } + + private void verifyTranslation(View view, boolean goLeft, boolean goUp) { + float translationX = view.getTranslationX(); + float translationY = view.getTranslationY(); + + if (goLeft) { + assertTrue(translationX < 0); + } else { + assertTrue(translationX > 0); + } + + if (goUp) { + assertTrue(translationY < 0); + } else { + assertTrue(translationY > 0); + } + } + + private void verifyNoTranslation(View view) { + assertEquals(0f, view.getTranslationX(), 0.0f); + assertEquals(0f, view.getTranslationY(), 0.0f); + } + +} diff --git a/android/support/transition/FadeTest.java b/android/support/transition/FadeTest.java new file mode 100644 index 00000000..3b171e2c --- /dev/null +++ b/android/support/transition/FadeTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.MediumTest; +import android.view.View; +import android.view.ViewGroup; + +import org.junit.Before; +import org.junit.Test; + +@MediumTest +public class FadeTest extends BaseTest { + + private View mView; + private ViewGroup mRoot; + + @UiThreadTest + @Before + public void setUp() { + mRoot = rule.getActivity().getRoot(); + mView = new View(rule.getActivity()); + mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100)); + } + + @Test + public void testMode() { + assertThat(Fade.IN, is(Visibility.MODE_IN)); + assertThat(Fade.OUT, is(Visibility.MODE_OUT)); + final Fade fade = new Fade(); + assertThat(fade.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT)); + fade.setMode(Visibility.MODE_IN); + assertThat(fade.getMode(), is(Visibility.MODE_IN)); + } + + @Test + @UiThreadTest + public void testDisappear() { + final Fade fade = new Fade(); + final TransitionValues startValues = new TransitionValues(); + startValues.view = mView; + fade.captureStartValues(startValues); + mView.setVisibility(View.INVISIBLE); + final TransitionValues endValues = new TransitionValues(); + endValues.view = mView; + fade.captureEndValues(endValues); + Animator animator = fade.createAnimator(mRoot, startValues, endValues); + assertThat(animator, is(notNullValue())); + } + + @Test + @UiThreadTest + public void testAppear() { + mView.setVisibility(View.INVISIBLE); + final Fade fade = new Fade(); + final TransitionValues startValues = new TransitionValues(); + startValues.view = mView; + fade.captureStartValues(startValues); + mView.setVisibility(View.VISIBLE); + final TransitionValues endValues = new TransitionValues(); + endValues.view = mView; + fade.captureEndValues(endValues); + Animator animator = fade.createAnimator(mRoot, startValues, endValues); + assertThat(animator, is(notNullValue())); + } + + @Test + @UiThreadTest + public void testNoChange() { + final Fade fade = new Fade(); + final TransitionValues startValues = new TransitionValues(); + startValues.view = mView; + fade.captureStartValues(startValues); + final TransitionValues endValues = new TransitionValues(); + endValues.view = mView; + fade.captureEndValues(endValues); + Animator animator = fade.createAnimator(mRoot, startValues, endValues); + // No visibility change; no animation should happen + assertThat(animator, is(nullValue())); + } + + @Test + public void testFadeOutThenIn() throws Throwable { + // Fade out + final Runnable interrupt = mock(Runnable.class); + float[] valuesOut = new float[2]; + final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt, + valuesOut); + final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class); + fadeOut.addListener(listenerOut); + changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE); + verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class)); + + // The view is in the middle of fading out + verify(interrupt, timeout(3000)).run(); + + // Fade in + float[] valuesIn = new float[2]; + final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn); + final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class); + fadeIn.addListener(listenerIn); + changeVisibility(fadeIn, mRoot, mView, View.VISIBLE); + verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class)); + verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class)); + assertThat(valuesOut[1], allOf(greaterThan(0f), lessThan(1f))); + if (Build.VERSION.SDK_INT >= 19) { + // These won't match on API levels 18 and below due to lack of Animator pause. + assertEquals(valuesOut[1], valuesIn[0], 0.01f); + } + + verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class)); + assertThat(mView.getVisibility(), is(View.VISIBLE)); + assertEquals(valuesIn[1], 1.f, 0.01f); + } + + @Test + public void testFadeInThenOut() throws Throwable { + changeVisibility(null, mRoot, mView, View.INVISIBLE); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // Fade in + final Runnable interrupt = mock(Runnable.class); + float[] valuesIn = new float[2]; + final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn); + final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class); + fadeIn.addListener(listenerIn); + changeVisibility(fadeIn, mRoot, mView, View.VISIBLE); + verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class)); + + // The view is in the middle of fading in + verify(interrupt, timeout(3000)).run(); + + // Fade out + float[] valuesOut = new float[2]; + final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut); + final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class); + fadeOut.addListener(listenerOut); + changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE); + verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class)); + verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class)); + assertThat(valuesIn[1], allOf(greaterThan(0f), lessThan(1f))); + if (Build.VERSION.SDK_INT >= 19) { + // These won't match on API levels 18 and below due to lack of Animator pause. + assertEquals(valuesIn[1], valuesOut[0], 0.01f); + } + + verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class)); + assertThat(mView.getVisibility(), is(View.INVISIBLE)); + } + + @Test + public void testFadeWithAlpha() throws Throwable { + // Set the view alpha to 0.5 + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + mView.setAlpha(0.5f); + } + }); + // Fade out + final Fade fadeOut = new Fade(Fade.OUT); + final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class); + fadeOut.addListener(listenerOut); + changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE); + verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class)); + verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class)); + // Fade in + final Fade fadeIn = new Fade(Fade.IN); + final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class); + fadeIn.addListener(listenerIn); + changeVisibility(fadeIn, mRoot, mView, View.VISIBLE); + verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class)); + verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class)); + // Confirm that the view still has the original alpha value + assertThat(mView.getVisibility(), is(View.VISIBLE)); + assertEquals(0.5f, mView.getAlpha(), 0.01f); + } + + private void changeVisibility(final Fade fade, final ViewGroup container, final View target, + final int visibility) throws Throwable { + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + if (fade != null) { + TransitionManager.beginDelayedTransition(container, fade); + } + target.setVisibility(visibility); + } + }); + } + + /** + * A special version of {@link Fade} that runs a specified {@link Runnable} soon after the + * target starts fading in or out. + */ + private static class InterruptibleFade extends Fade { + + static final float ALPHA_THRESHOLD = 0.2f; + + float mInitialAlpha = -1; + Runnable mMiddle; + final float[] mAlphaValues; + + InterruptibleFade(int mode, Runnable middle, float[] alphaValues) { + super(mode); + mMiddle = middle; + mAlphaValues = alphaValues; + } + + @Nullable + @Override + public Animator createAnimator(@NonNull ViewGroup sceneRoot, + @Nullable final TransitionValues startValues, + @Nullable final TransitionValues endValues) { + final Animator animator = super.createAnimator(sceneRoot, startValues, endValues); + if (animator instanceof ObjectAnimator) { + ((ObjectAnimator) animator).addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float alpha = (float) animation.getAnimatedValue(); + mAlphaValues[1] = alpha; + if (mInitialAlpha < 0) { + mInitialAlpha = alpha; + mAlphaValues[0] = mInitialAlpha; + } else if (Math.abs(alpha - mInitialAlpha) > ALPHA_THRESHOLD) { + if (mMiddle != null) { + mMiddle.run(); + mMiddle = null; + } + } + } + }); + } + return animator; + } + + } + +} diff --git a/android/support/transition/FragmentTransitionTest.java b/android/support/transition/FragmentTransitionTest.java new file mode 100644 index 00000000..893d4c66 --- /dev/null +++ b/android/support/transition/FragmentTransitionTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.app.Instrumentation; +import android.os.Bundle; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.util.Pair; +import android.support.v4.util.SparseArrayCompat; +import android.support.v4.view.ViewCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + + +@MediumTest +@RunWith(Parameterized.class) +public class FragmentTransitionTest extends BaseTest { + + @Parameterized.Parameters + public static Object[] data() { + return new Boolean[]{ + false, true + }; + } + + private final boolean mReorderingAllowed; + + public FragmentTransitionTest(boolean reorderingAllowed) { + mReorderingAllowed = reorderingAllowed; + } + + @Test + public void preconditions() { + final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2); + final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3); + showFragment(fragment1, false, null); + assertNull(fragment1.mRed); + assertNotNull(fragment1.mGreen); + assertNotNull(fragment1.mBlue); + showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green")); + assertNotNull(fragment2.mRed); + assertNotNull(fragment2.mGreen); + assertNotNull(fragment2.mBlue); + } + + @Test + public void nonSharedTransition() { + final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2); + final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3); + showFragment(fragment1, false, null); + showFragment(fragment2, true, null); + verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT)) + .onTransitionStart(any(Transition.class)); + verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT), timeout(3000)) + .onTransitionEnd(any(Transition.class)); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER)) + .onTransitionStart(any(Transition.class)); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER), timeout(3000)) + .onTransitionEnd(any(Transition.class)); + popBackStack(); + verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_REENTER)) + .onTransitionStart(any(Transition.class)); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_RETURN)) + .onTransitionStart(any(Transition.class)); + } + + @Test + public void sharedTransition() { + final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2); + final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3); + showFragment(fragment1, false, null); + showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green")); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER)) + .onTransitionStart(any(Transition.class)); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER), timeout(3000)) + .onTransitionEnd(any(Transition.class)); + popBackStack(); + verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_RETURN)) + .onTransitionStart(any(Transition.class)); + } + + private void showFragment(final Fragment fragment, final boolean addToBackStack, + final Pair sharedElement) { + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.runOnMainSync(new Runnable() { + @Override + public void run() { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.root, fragment); + transaction.setReorderingAllowed(mReorderingAllowed); + if (sharedElement != null) { + transaction.addSharedElement(sharedElement.first, sharedElement.second); + } + if (addToBackStack) { + transaction.addToBackStack(null); + transaction.commit(); + getFragmentManager().executePendingTransactions(); + } else { + transaction.commitNow(); + } + } + }); + instrumentation.waitForIdleSync(); + } + + private void popBackStack() { + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.runOnMainSync(new Runnable() { + @Override + public void run() { + getFragmentManager().popBackStackImmediate(); + } + }); + instrumentation.waitForIdleSync(); + } + + private FragmentManager getFragmentManager() { + return rule.getActivity().getSupportFragmentManager(); + } + + /** + * A {@link Fragment} with all kinds of {@link Transition} with tracking listeners. + */ + public static class TransitionFragment extends Fragment { + + static final int TRANSITION_ENTER = 1; + static final int TRANSITION_EXIT = 2; + static final int TRANSITION_REENTER = 3; + static final int TRANSITION_RETURN = 4; + static final int TRANSITION_SHARED_ENTER = 5; + static final int TRANSITION_SHARED_RETURN = 6; + + private static final String ARG_LAYOUT_ID = "layout_id"; + + View mRed; + View mGreen; + View mBlue; + + SparseArrayCompat mListeners = new SparseArrayCompat<>(); + + public static TransitionFragment newInstance(@LayoutRes int layout) { + final Bundle args = new Bundle(); + args.putInt(ARG_LAYOUT_ID, layout); + final TransitionFragment fragment = new TransitionFragment(); + fragment.setArguments(args); + return fragment; + } + + public TransitionFragment() { + setEnterTransition(createTransition(TRANSITION_ENTER)); + setExitTransition(createTransition(TRANSITION_EXIT)); + setReenterTransition(createTransition(TRANSITION_REENTER)); + setReturnTransition(createTransition(TRANSITION_RETURN)); + setSharedElementEnterTransition(createTransition(TRANSITION_SHARED_ENTER)); + setSharedElementReturnTransition(createTransition(TRANSITION_SHARED_RETURN)); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(getArguments().getInt(ARG_LAYOUT_ID), container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mRed = view.findViewById(R.id.redSquare); + mGreen = view.findViewById(R.id.greenSquare); + mBlue = view.findViewById(R.id.blueSquare); + if (mRed != null) { + ViewCompat.setTransitionName(mRed, "red"); + } + if (mGreen != null) { + ViewCompat.setTransitionName(mGreen, "green"); + } + if (mBlue != null) { + ViewCompat.setTransitionName(mBlue, "blue"); + } + } + + private Transition createTransition(int type) { + final Transition.TransitionListener listener = mock( + Transition.TransitionListener.class); + final AutoTransition transition = new AutoTransition(); + transition.addListener(listener); + transition.setDuration(10); + mListeners.put(type, listener); + return transition; + } + + } + +} diff --git a/android/support/transition/PathMotionTest.java b/android/support/transition/PathMotionTest.java new file mode 100644 index 00000000..8bf738e4 --- /dev/null +++ b/android/support/transition/PathMotionTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import android.graphics.Path; +import android.graphics.PathMeasure; + +public abstract class PathMotionTest { + + public static void assertPathMatches(Path expectedPath, Path path) { + PathMeasure expectedMeasure = new PathMeasure(expectedPath, false); + PathMeasure pathMeasure = new PathMeasure(path, false); + + boolean expectedNextContour; + boolean pathNextContour; + int contourIndex = 0; + do { + float expectedLength = expectedMeasure.getLength(); + assertEquals("Lengths differ", expectedLength, pathMeasure.getLength(), 0.01f); + + float minLength = Math.min(expectedLength, pathMeasure.getLength()); + + float[] pos = new float[2]; + + float increment = minLength / 5f; + for (float along = 0; along <= minLength; along += increment) { + expectedMeasure.getPosTan(along, pos, null); + float expectedX = pos[0]; + float expectedY = pos[1]; + + pathMeasure.getPosTan(along, pos, null); + assertEquals("Failed at " + increment + " in contour " + contourIndex, + expectedX, pos[0], 0.01f); + assertEquals("Failed at " + increment + " in contour " + contourIndex, + expectedY, pos[1], 0.01f); + } + expectedNextContour = expectedMeasure.nextContour(); + pathNextContour = pathMeasure.nextContour(); + contourIndex++; + } while (expectedNextContour && pathNextContour); + assertFalse(expectedNextContour); + assertFalse(pathNextContour); + } + +} diff --git a/android/support/transition/PatternPathMotionTest.java b/android/support/transition/PatternPathMotionTest.java new file mode 100644 index 00000000..b14ceaa1 --- /dev/null +++ b/android/support/transition/PatternPathMotionTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertSame; + +import android.graphics.Path; +import android.graphics.RectF; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PatternPathMotionTest extends PathMotionTest { + + @Test + public void testStraightPath() { + Path pattern = new Path(); + pattern.moveTo(100, 500); + pattern.lineTo(300, 1000); + + PatternPathMotion pathMotion = new PatternPathMotion(pattern); + assertPathMatches(pattern, pathMotion.getPatternPath()); + + Path expected = new Path(); + expected.moveTo(0, 0); + expected.lineTo(100, 100); + + assertPathMatches(expected, pathMotion.getPath(0, 0, 100, 100)); + } + + @Test + public void testCurve() { + RectF oval = new RectF(); + Path pattern = new Path(); + oval.set(0, 0, 100, 100); + pattern.addArc(oval, 0, 180); + + PatternPathMotion pathMotion = new PatternPathMotion(pattern); + assertPathMatches(pattern, pathMotion.getPatternPath()); + + Path expected = new Path(); + oval.set(-50, 0, 50, 100); + expected.addArc(oval, -90, 180); + + assertPathMatches(expected, pathMotion.getPath(0, 0, 0, 100)); + } + + @Test + public void testSetPatternPath() { + Path pattern = new Path(); + RectF oval = new RectF(0, 0, 100, 100); + pattern.addArc(oval, 0, 180); + + PatternPathMotion patternPathMotion = new PatternPathMotion(); + patternPathMotion.setPatternPath(pattern); + assertSame(pattern, patternPathMotion.getPatternPath()); + } + +} diff --git a/android/support/transition/PropagationTest.java b/android/support/transition/PropagationTest.java new file mode 100644 index 00000000..932c8d3d --- /dev/null +++ b/android/support/transition/PropagationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; + +import org.junit.Test; + +@MediumTest +public class PropagationTest extends BaseTransitionTest { + + @Test + public void testCircularPropagation() throws Throwable { + enterScene(R.layout.scene10); + CircularPropagation propagation = new CircularPropagation(); + mTransition.setPropagation(propagation); + final TransitionValues redValues = new TransitionValues(); + redValues.view = mRoot.findViewById(R.id.redSquare); + propagation.captureValues(redValues); + + // Only the reported propagation properties are set + for (String prop : propagation.getPropagationProperties()) { + assertTrue(redValues.values.keySet().contains(prop)); + } + assertEquals(propagation.getPropagationProperties().length, redValues.values.size()); + + // check the visibility + assertEquals(View.VISIBLE, propagation.getViewVisibility(redValues)); + assertEquals(View.GONE, propagation.getViewVisibility(null)); + + // Check the positions + int[] pos = new int[2]; + redValues.view.getLocationOnScreen(pos); + pos[0] += redValues.view.getWidth() / 2; + pos[1] += redValues.view.getHeight() / 2; + assertEquals(pos[0], propagation.getViewX(redValues)); + assertEquals(pos[1], propagation.getViewY(redValues)); + + mTransition.setEpicenterCallback(new Transition.EpicenterCallback() { + @Override + public Rect onGetEpicenter(@NonNull Transition transition) { + return new Rect(0, 0, redValues.view.getWidth(), redValues.view.getHeight()); + } + }); + + long redDelay = getDelay(R.id.redSquare); + // red square's delay should be roughly 0 since it is at the epicenter + assertEquals(0f, redDelay, 30f); + + // The green square is on the upper-right + long greenDelay = getDelay(R.id.greenSquare); + assertTrue(greenDelay < redDelay); + + // The blue square is on the lower-right + long blueDelay = getDelay(R.id.blueSquare); + assertTrue(blueDelay < greenDelay); + + // Test propagation speed + propagation.setPropagationSpeed(1000000000f); + assertEquals(0, getDelay(R.id.blueSquare)); + } + + private TransitionValues capturePropagationValues(int viewId) { + TransitionValues transitionValues = new TransitionValues(); + transitionValues.view = mRoot.findViewById(viewId); + TransitionPropagation propagation = mTransition.getPropagation(); + assertNotNull(propagation); + propagation.captureValues(transitionValues); + return transitionValues; + } + + private long getDelay(int viewId) { + TransitionValues transitionValues = capturePropagationValues(viewId); + TransitionPropagation propagation = mTransition.getPropagation(); + assertNotNull(propagation); + return propagation.getStartDelay(mRoot, mTransition, transitionValues, null); + } + +} diff --git a/android/support/transition/SceneTest.java b/android/support/transition/SceneTest.java new file mode 100644 index 00000000..129a3ebe --- /dev/null +++ b/android/support/transition/SceneTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.sameInstance; + +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.junit.Test; + +@MediumTest +public class SceneTest extends BaseTest { + + @Test + public void testGetSceneRoot() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + Scene scene = new Scene(root); + assertThat(scene.getSceneRoot(), is(sameInstance(root))); + } + + @Test + @UiThreadTest + public void testSceneWithViewGroup() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + FrameLayout layout = new FrameLayout(activity); + Scene scene = new Scene(root, layout); + CheckCalledRunnable enterAction = new CheckCalledRunnable(); + CheckCalledRunnable exitAction = new CheckCalledRunnable(); + scene.setEnterAction(enterAction); + scene.setExitAction(exitAction); + scene.enter(); + assertThat(enterAction.wasCalled(), is(true)); + assertThat(exitAction.wasCalled(), is(false)); + assertThat(root.getChildCount(), is(1)); + assertThat(root.getChildAt(0), is((View) layout)); + scene.exit(); + assertThat(exitAction.wasCalled(), is(true)); + } + + @Test + @UiThreadTest + public void testSceneWithView() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + View view = new View(activity); + Scene scene = new Scene(root, view); + CheckCalledRunnable enterAction = new CheckCalledRunnable(); + CheckCalledRunnable exitAction = new CheckCalledRunnable(); + scene.setEnterAction(enterAction); + scene.setExitAction(exitAction); + scene.enter(); + assertThat(enterAction.wasCalled(), is(true)); + assertThat(exitAction.wasCalled(), is(false)); + assertThat(root.getChildCount(), is(1)); + assertThat(root.getChildAt(0), is(view)); + scene.exit(); + assertThat(exitAction.wasCalled(), is(true)); + } + + @Test + public void testEnterAction() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + Scene scene = new Scene(root); + CheckCalledRunnable runnable = new CheckCalledRunnable(); + scene.setEnterAction(runnable); + scene.enter(); + assertThat(runnable.wasCalled(), is(true)); + } + + @Test + public void testExitAction() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + Scene scene = new Scene(root); + scene.enter(); + CheckCalledRunnable runnable = new CheckCalledRunnable(); + scene.setExitAction(runnable); + scene.exit(); + assertThat(runnable.wasCalled(), is(true)); + } + + @Test + public void testExitAction_withoutEnter() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + Scene scene = new Scene(root); + CheckCalledRunnable runnable = new CheckCalledRunnable(); + scene.setExitAction(runnable); + scene.exit(); + assertThat(runnable.wasCalled(), is(false)); + } + + @Test + public void testGetSceneForLayout_cache() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + Scene scene = Scene.getSceneForLayout(root, R.layout.support_scene0, activity); + assertThat("getSceneForLayout should return the same instance for subsequent calls", + Scene.getSceneForLayout(root, R.layout.support_scene0, activity), + is(sameInstance(scene))); + } + +} diff --git a/android/support/transition/SlideBadEdgeTest.java b/android/support/transition/SlideBadEdgeTest.java new file mode 100644 index 00000000..e43d4f32 --- /dev/null +++ b/android/support/transition/SlideBadEdgeTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.fail; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.Gravity; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SlideBadEdgeTest { + + private static final Object[][] sBadGravity = { + {Gravity.AXIS_CLIP, "AXIS_CLIP"}, + {Gravity.AXIS_PULL_AFTER, "AXIS_PULL_AFTER"}, + {Gravity.AXIS_PULL_BEFORE, "AXIS_PULL_BEFORE"}, + {Gravity.AXIS_SPECIFIED, "AXIS_SPECIFIED"}, + {Gravity.AXIS_Y_SHIFT, "AXIS_Y_SHIFT"}, + {Gravity.AXIS_X_SHIFT, "AXIS_X_SHIFT"}, + {Gravity.CENTER, "CENTER"}, + {Gravity.CLIP_VERTICAL, "CLIP_VERTICAL"}, + {Gravity.CLIP_HORIZONTAL, "CLIP_HORIZONTAL"}, + {Gravity.CENTER_VERTICAL, "CENTER_VERTICAL"}, + {Gravity.CENTER_HORIZONTAL, "CENTER_HORIZONTAL"}, + {Gravity.DISPLAY_CLIP_VERTICAL, "DISPLAY_CLIP_VERTICAL"}, + {Gravity.DISPLAY_CLIP_HORIZONTAL, "DISPLAY_CLIP_HORIZONTAL"}, + {Gravity.FILL_VERTICAL, "FILL_VERTICAL"}, + {Gravity.FILL, "FILL"}, + {Gravity.FILL_HORIZONTAL, "FILL_HORIZONTAL"}, + {Gravity.HORIZONTAL_GRAVITY_MASK, "HORIZONTAL_GRAVITY_MASK"}, + {Gravity.NO_GRAVITY, "NO_GRAVITY"}, + {Gravity.RELATIVE_LAYOUT_DIRECTION, "RELATIVE_LAYOUT_DIRECTION"}, + {Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, "RELATIVE_HORIZONTAL_GRAVITY_MASK"}, + {Gravity.VERTICAL_GRAVITY_MASK, "VERTICAL_GRAVITY_MASK"}, + }; + + @Test + public void testBadSide() { + for (int i = 0; i < sBadGravity.length; i++) { + int badEdge = (Integer) sBadGravity[i][0]; + String edgeName = (String) sBadGravity[i][1]; + try { + new Slide(badEdge); + fail("Should not be able to set slide edge to " + edgeName); + } catch (IllegalArgumentException e) { + // expected + } + + try { + Slide slide = new Slide(); + slide.setSlideEdge(badEdge); + fail("Should not be able to set slide edge to " + edgeName); + } catch (IllegalArgumentException e) { + // expected + } + } + } + +} diff --git a/android/support/transition/SlideDefaultEdgeTest.java b/android/support/transition/SlideDefaultEdgeTest.java new file mode 100644 index 00000000..a0e7eabb --- /dev/null +++ b/android/support/transition/SlideDefaultEdgeTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.Gravity; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SlideDefaultEdgeTest { + + @Test + public void testDefaultSide() { + // default to bottom + Slide slide = new Slide(); + assertEquals(Gravity.BOTTOM, slide.getSlideEdge()); + } + +} diff --git a/android/support/transition/SlideEdgeTest.java b/android/support/transition/SlideEdgeTest.java new file mode 100644 index 00000000..af8d0b62 --- /dev/null +++ b/android/support/transition/SlideEdgeTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; + +import org.junit.Test; + +@MediumTest +public class SlideEdgeTest extends BaseTransitionTest { + + private static final Object[][] sSlideEdgeArray = { + {Gravity.START, "START"}, + {Gravity.END, "END"}, + {Gravity.LEFT, "LEFT"}, + {Gravity.TOP, "TOP"}, + {Gravity.RIGHT, "RIGHT"}, + {Gravity.BOTTOM, "BOTTOM"}, + }; + + @Test + public void testSetSide() throws Throwable { + for (int i = 0; i < sSlideEdgeArray.length; i++) { + int slideEdge = (Integer) (sSlideEdgeArray[i][0]); + String edgeName = (String) (sSlideEdgeArray[i][1]); + Slide slide = new Slide(slideEdge); + assertEquals("Edge not set properly in constructor " + edgeName, + slideEdge, slide.getSlideEdge()); + + slide = new Slide(); + slide.setSlideEdge(slideEdge); + assertEquals("Edge not set properly with setter " + edgeName, + slideEdge, slide.getSlideEdge()); + } + } + + @LargeTest + @Test + public void testSlideOut() throws Throwable { + for (int i = 0; i < sSlideEdgeArray.length; i++) { + final int slideEdge = (Integer) (sSlideEdgeArray[i][0]); + final Slide slide = new Slide(slideEdge); + final Transition.TransitionListener listener = + mock(Transition.TransitionListener.class); + slide.addListener(listener); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + rule.getActivity().setContentView(R.layout.scene1); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare); + final View hello = rule.getActivity().findViewById(R.id.hello); + final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(sceneRoot, slide); + redSquare.setVisibility(View.INVISIBLE); + greenSquare.setVisibility(View.INVISIBLE); + hello.setVisibility(View.INVISIBLE); + } + }); + verify(listener, timeout(1000)).onTransitionStart(any(Transition.class)); + verify(listener, never()).onTransitionEnd(any(Transition.class)); + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, hello.getVisibility()); + + float redStartX = redSquare.getTranslationX(); + float redStartY = redSquare.getTranslationY(); + + Thread.sleep(200); + verifyTranslation(slideEdge, redSquare); + verifyTranslation(slideEdge, greenSquare); + verifyTranslation(slideEdge, hello); + + final float redMidX = redSquare.getTranslationX(); + final float redMidY = redSquare.getTranslationY(); + + switch (slideEdge) { + case Gravity.LEFT: + case Gravity.START: + assertTrue( + "isn't sliding out to left. Expecting " + redStartX + " > " + redMidX, + redStartX > redMidX); + break; + case Gravity.RIGHT: + case Gravity.END: + assertTrue( + "isn't sliding out to right. Expecting " + redStartX + " < " + redMidX, + redStartX < redMidX); + break; + case Gravity.TOP: + assertTrue("isn't sliding out to top. Expecting " + redStartY + " > " + redMidY, + redStartY > redSquare.getTranslationY()); + break; + case Gravity.BOTTOM: + assertTrue( + "isn't sliding out to bottom. Expecting " + redStartY + " < " + redMidY, + redStartY < redSquare.getTranslationY()); + break; + } + verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class)); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + verifyNoTranslation(redSquare); + verifyNoTranslation(greenSquare); + verifyNoTranslation(hello); + assertEquals(View.INVISIBLE, redSquare.getVisibility()); + assertEquals(View.INVISIBLE, greenSquare.getVisibility()); + assertEquals(View.INVISIBLE, hello.getVisibility()); + } + } + + @LargeTest + @Test + public void testSlideIn() throws Throwable { + for (int i = 0; i < sSlideEdgeArray.length; i++) { + final int slideEdge = (Integer) (sSlideEdgeArray[i][0]); + final Slide slide = new Slide(slideEdge); + final Transition.TransitionListener listener = + mock(Transition.TransitionListener.class); + slide.addListener(listener); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + rule.getActivity().setContentView(R.layout.scene1); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + final View redSquare = rule.getActivity().findViewById(R.id.redSquare); + final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare); + final View hello = rule.getActivity().findViewById(R.id.hello); + final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder); + + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + redSquare.setVisibility(View.INVISIBLE); + greenSquare.setVisibility(View.INVISIBLE); + hello.setVisibility(View.INVISIBLE); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + // now slide in + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(sceneRoot, slide); + redSquare.setVisibility(View.VISIBLE); + greenSquare.setVisibility(View.VISIBLE); + hello.setVisibility(View.VISIBLE); + } + }); + verify(listener, timeout(1000)).onTransitionStart(any(Transition.class)); + + verify(listener, never()).onTransitionEnd(any(Transition.class)); + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, hello.getVisibility()); + + final float redStartX = redSquare.getTranslationX(); + final float redStartY = redSquare.getTranslationY(); + + Thread.sleep(200); + verifyTranslation(slideEdge, redSquare); + verifyTranslation(slideEdge, greenSquare); + verifyTranslation(slideEdge, hello); + final float redMidX = redSquare.getTranslationX(); + final float redMidY = redSquare.getTranslationY(); + + switch (slideEdge) { + case Gravity.LEFT: + case Gravity.START: + assertTrue( + "isn't sliding in from left. Expecting " + redStartX + " < " + redMidX, + redStartX < redMidX); + break; + case Gravity.RIGHT: + case Gravity.END: + assertTrue( + "isn't sliding in from right. Expecting " + redStartX + " > " + redMidX, + redStartX > redMidX); + break; + case Gravity.TOP: + assertTrue( + "isn't sliding in from top. Expecting " + redStartY + " < " + redMidY, + redStartY < redSquare.getTranslationY()); + break; + case Gravity.BOTTOM: + assertTrue("isn't sliding in from bottom. Expecting " + redStartY + " > " + + redMidY, + redStartY > redSquare.getTranslationY()); + break; + } + verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class)); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + verifyNoTranslation(redSquare); + verifyNoTranslation(greenSquare); + verifyNoTranslation(hello); + assertEquals(View.VISIBLE, redSquare.getVisibility()); + assertEquals(View.VISIBLE, greenSquare.getVisibility()); + assertEquals(View.VISIBLE, hello.getVisibility()); + } + } + + private void verifyTranslation(int slideEdge, View view) { + switch (slideEdge) { + case Gravity.LEFT: + case Gravity.START: + assertTrue(view.getTranslationX() < 0); + assertEquals(0f, view.getTranslationY(), 0.01f); + break; + case Gravity.RIGHT: + case Gravity.END: + assertTrue(view.getTranslationX() > 0); + assertEquals(0f, view.getTranslationY(), 0.01f); + break; + case Gravity.TOP: + assertTrue(view.getTranslationY() < 0); + assertEquals(0f, view.getTranslationX(), 0.01f); + break; + case Gravity.BOTTOM: + assertTrue(view.getTranslationY() > 0); + assertEquals(0f, view.getTranslationX(), 0.01f); + break; + } + } + + private void verifyNoTranslation(View view) { + assertEquals(0f, view.getTranslationX(), 0.01f); + assertEquals(0f, view.getTranslationY(), 0.01f); + } + +} diff --git a/android/support/transition/SyncRunnable.java b/android/support/transition/SyncRunnable.java new file mode 100644 index 00000000..2e8a2e14 --- /dev/null +++ b/android/support/transition/SyncRunnable.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +class SyncRunnable implements Runnable { + + private final CountDownLatch mLatch = new CountDownLatch(1); + + @Override + public void run() { + mLatch.countDown(); + } + + boolean await() { + try { + return mLatch.await(3000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + } + +} diff --git a/android/support/transition/SyncTransitionListener.java b/android/support/transition/SyncTransitionListener.java new file mode 100644 index 00000000..4d7e02e8 --- /dev/null +++ b/android/support/transition/SyncTransitionListener.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.transition; + +import android.support.annotation.NonNull; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * This {@link Transition.TransitionListener} synchronously waits for the specified callback. + */ +class SyncTransitionListener implements Transition.TransitionListener { + + static final int EVENT_START = 1; + static final int EVENT_END = 2; + static final int EVENT_CANCEL = 3; + static final int EVENT_PAUSE = 4; + static final int EVENT_RESUME = 5; + + private final int mTargetEvent; + private CountDownLatch mLatch = new CountDownLatch(1); + + SyncTransitionListener(int event) { + mTargetEvent = event; + } + + boolean await() { + try { + return mLatch.await(3000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + void reset() { + mLatch = new CountDownLatch(1); + } + + @Override + public void onTransitionStart(@NonNull Transition transition) { + if (mTargetEvent == EVENT_START) { + mLatch.countDown(); + } + } + + @Override + public void onTransitionEnd(@NonNull Transition transition) { + if (mTargetEvent == EVENT_END) { + mLatch.countDown(); + } + } + + @Override + public void onTransitionCancel(@NonNull Transition transition) { + if (mTargetEvent == EVENT_CANCEL) { + mLatch.countDown(); + } + } + + @Override + public void onTransitionPause(@NonNull Transition transition) { + if (mTargetEvent == EVENT_PAUSE) { + mLatch.countDown(); + } + } + + @Override + public void onTransitionResume(@NonNull Transition transition) { + if (mTargetEvent == EVENT_RESUME) { + mLatch.countDown(); + } + } +} diff --git a/android/support/transition/TransitionActivity.java b/android/support/transition/TransitionActivity.java new file mode 100644 index 00000000..ecb9355f --- /dev/null +++ b/android/support/transition/TransitionActivity.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import android.os.Bundle; +import android.support.transition.test.R; +import android.support.v4.app.FragmentActivity; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +public class TransitionActivity extends FragmentActivity { + + private LinearLayout mRoot; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_transition); + mRoot = findViewById(R.id.root); + } + + ViewGroup getRoot() { + return mRoot; + } + +} diff --git a/android/support/transition/TransitionInflaterTest.java b/android/support/transition/TransitionInflaterTest.java new file mode 100644 index 00000000..f9bd23fa --- /dev/null +++ b/android/support/transition/TransitionInflaterTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.support.annotation.NonNull; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.util.AttributeSet; +import android.view.Gravity; +import android.widget.ImageView; +import android.widget.TextView; + +import org.junit.Test; + +import java.util.List; + +@MediumTest +public class TransitionInflaterTest extends BaseTest { + + @Test + public void testInflationConstructors() throws Throwable { + TransitionInflater inflater = TransitionInflater.from(rule.getActivity()); + Transition transition = inflater.inflateTransition(R.transition.transition_constructors); + assertTrue(transition instanceof TransitionSet); + TransitionSet set = (TransitionSet) transition; + assertEquals(10, set.getTransitionCount()); + } + + @Test + public void testInflation() { + TransitionInflater inflater = TransitionInflater.from(rule.getActivity()); + verifyFadeProperties(inflater.inflateTransition(R.transition.fade)); + verifyChangeBoundsProperties(inflater.inflateTransition(R.transition.change_bounds)); + verifySlideProperties(inflater.inflateTransition(R.transition.slide)); + verifyExplodeProperties(inflater.inflateTransition(R.transition.explode)); + verifyChangeImageTransformProperties( + inflater.inflateTransition(R.transition.change_image_transform)); + verifyChangeTransformProperties(inflater.inflateTransition(R.transition.change_transform)); + verifyChangeClipBoundsProperties( + inflater.inflateTransition(R.transition.change_clip_bounds)); + verifyAutoTransitionProperties(inflater.inflateTransition(R.transition.auto_transition)); + verifyChangeScrollProperties(inflater.inflateTransition(R.transition.change_scroll)); + verifyTransitionSetProperties(inflater.inflateTransition(R.transition.transition_set)); + verifyCustomTransitionProperties( + inflater.inflateTransition(R.transition.custom_transition)); + verifyTargetIds(inflater.inflateTransition(R.transition.target_ids)); + verifyTargetNames(inflater.inflateTransition(R.transition.target_names)); + verifyTargetClass(inflater.inflateTransition(R.transition.target_classes)); + verifyArcMotion(inflater.inflateTransition(R.transition.arc_motion)); + verifyCustomPathMotion(inflater.inflateTransition(R.transition.custom_path_motion)); + verifyPatternPathMotion(inflater.inflateTransition(R.transition.pattern_path_motion)); + } + + // TODO: Add test for TransitionManager + + private void verifyFadeProperties(Transition transition) { + assertTrue(transition instanceof Fade); + Fade fade = (Fade) transition; + assertEquals(Fade.OUT, fade.getMode()); + } + + private void verifyChangeBoundsProperties(Transition transition) { + assertTrue(transition instanceof ChangeBounds); + ChangeBounds changeBounds = (ChangeBounds) transition; + assertTrue(changeBounds.getResizeClip()); + } + + private void verifySlideProperties(Transition transition) { + assertTrue(transition instanceof Slide); + Slide slide = (Slide) transition; + assertEquals(Gravity.TOP, slide.getSlideEdge()); + } + + private void verifyExplodeProperties(Transition transition) { + assertTrue(transition instanceof Explode); + Visibility visibility = (Visibility) transition; + assertEquals(Visibility.MODE_IN, visibility.getMode()); + } + + private void verifyChangeImageTransformProperties(Transition transition) { + assertTrue(transition instanceof ChangeImageTransform); + } + + private void verifyChangeTransformProperties(Transition transition) { + assertTrue(transition instanceof ChangeTransform); + ChangeTransform changeTransform = (ChangeTransform) transition; + assertFalse(changeTransform.getReparent()); + assertFalse(changeTransform.getReparentWithOverlay()); + } + + private void verifyChangeClipBoundsProperties(Transition transition) { + assertTrue(transition instanceof ChangeClipBounds); + } + + private void verifyAutoTransitionProperties(Transition transition) { + assertTrue(transition instanceof AutoTransition); + } + + private void verifyChangeScrollProperties(Transition transition) { + assertTrue(transition instanceof ChangeScroll); + } + + private void verifyTransitionSetProperties(Transition transition) { + assertTrue(transition instanceof TransitionSet); + TransitionSet set = (TransitionSet) transition; + assertEquals(TransitionSet.ORDERING_SEQUENTIAL, set.getOrdering()); + assertEquals(2, set.getTransitionCount()); + assertTrue(set.getTransitionAt(0) instanceof ChangeBounds); + assertTrue(set.getTransitionAt(1) instanceof Fade); + } + + private void verifyCustomTransitionProperties(Transition transition) { + assertTrue(transition instanceof CustomTransition); + } + + private void verifyTargetIds(Transition transition) { + List targets = transition.getTargetIds(); + assertNotNull(targets); + assertEquals(2, targets.size()); + assertEquals(R.id.hello, (int) targets.get(0)); + assertEquals(R.id.world, (int) targets.get(1)); + } + + private void verifyTargetNames(Transition transition) { + List targets = transition.getTargetNames(); + assertNotNull(targets); + assertEquals(2, targets.size()); + assertEquals("hello", targets.get(0)); + assertEquals("world", targets.get(1)); + } + + private void verifyTargetClass(Transition transition) { + List targets = transition.getTargetTypes(); + assertNotNull(targets); + assertEquals(2, targets.size()); + assertEquals(TextView.class, targets.get(0)); + assertEquals(ImageView.class, targets.get(1)); + } + + private void verifyArcMotion(Transition transition) { + assertNotNull(transition); + PathMotion motion = transition.getPathMotion(); + assertNotNull(motion); + assertTrue(motion instanceof ArcMotion); + ArcMotion arcMotion = (ArcMotion) motion; + assertEquals(1f, arcMotion.getMinimumVerticalAngle(), 0.01f); + assertEquals(2f, arcMotion.getMinimumHorizontalAngle(), 0.01f); + assertEquals(53f, arcMotion.getMaximumAngle(), 0.01f); + } + + private void verifyCustomPathMotion(Transition transition) { + assertNotNull(transition); + PathMotion motion = transition.getPathMotion(); + assertNotNull(motion); + assertTrue(motion instanceof CustomPathMotion); + } + + private void verifyPatternPathMotion(Transition transition) { + assertNotNull(transition); + PathMotion motion = transition.getPathMotion(); + assertNotNull(motion); + assertTrue(motion instanceof PatternPathMotion); + PatternPathMotion pattern = (PatternPathMotion) motion; + Path path = pattern.getPatternPath(); + PathMeasure measure = new PathMeasure(path, false); + assertEquals(200f, measure.getLength(), 0.1f); + } + + public static class CustomTransition extends Transition { + public CustomTransition() { + fail("Default constructor was not expected"); + } + + @SuppressWarnings("unused") // This constructor is used in XML + public CustomTransition(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(@NonNull TransitionValues transitionValues) { + } + + @Override + public void captureEndValues(@NonNull TransitionValues transitionValues) { + } + } + + public static class CustomPathMotion extends PathMotion { + public CustomPathMotion() { + fail("default constructor shouldn't be called."); + } + + public CustomPathMotion(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public Path getPath(float startX, float startY, float endX, float endY) { + return null; + } + } + + public static class InflationFade extends Fade { + public InflationFade(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationChangeBounds extends ChangeBounds { + public InflationChangeBounds(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationSlide extends Slide { + public InflationSlide(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationTransitionSet extends TransitionSet { + public InflationTransitionSet(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationChangeImageTransform extends ChangeImageTransform { + public InflationChangeImageTransform(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationChangeTransform extends ChangeTransform { + public InflationChangeTransform(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationAutoTransition extends AutoTransition { + public InflationAutoTransition(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationChangeClipBounds extends ChangeClipBounds { + public InflationChangeClipBounds(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationChangeScroll extends ChangeScroll { + public InflationChangeScroll(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + + public static class InflationExplode extends Explode { + public InflationExplode(Context context, AttributeSet attrs) { + super(context, attrs); + } + } + +} diff --git a/android/support/transition/TransitionManagerTest.java b/android/support/transition/TransitionManagerTest.java new file mode 100644 index 00000000..dc4f9832 --- /dev/null +++ b/android/support/transition/TransitionManagerTest.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.ViewGroup; + +import org.junit.Before; +import org.junit.Test; + +@MediumTest +public class TransitionManagerTest extends BaseTest { + + private Scene[] mScenes = new Scene[2]; + + @Before + public void prepareScenes() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity); + mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity); + } + + @Test + public void testSetup() { + assertThat(mScenes[0], is(notNullValue())); + assertThat(mScenes[1], is(notNullValue())); + } + + @Test + @UiThreadTest + public void testGo_enterAction() { + CheckCalledRunnable runnable = new CheckCalledRunnable(); + mScenes[0].setEnterAction(runnable); + assertThat(runnable.wasCalled(), is(false)); + TransitionManager.go(mScenes[0]); + assertThat(runnable.wasCalled(), is(true)); + } + + @Test + public void testGo_exitAction() throws Throwable { + final CheckCalledRunnable enter = new CheckCalledRunnable(); + final CheckCalledRunnable exit = new CheckCalledRunnable(); + mScenes[0].setEnterAction(enter); + mScenes[0].setExitAction(exit); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + assertThat(enter.wasCalled(), is(false)); + assertThat(exit.wasCalled(), is(false)); + TransitionManager.go(mScenes[0]); + assertThat(enter.wasCalled(), is(true)); + assertThat(exit.wasCalled(), is(false)); + } + }); + // Let the main thread catch up with the scene change + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(mScenes[1]); + assertThat(exit.wasCalled(), is(true)); + } + }); + } + + @Test + public void testGo_transitionListenerStart() throws Throwable { + final SyncTransitionListener listener = + new SyncTransitionListener(SyncTransitionListener.EVENT_START); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + Transition transition = new AutoTransition(); + transition.setDuration(0); + assertThat(transition.addListener(listener), is(sameInstance(transition))); + TransitionManager.go(mScenes[0], transition); + } + }); + assertThat("Timed out waiting for the TransitionListener", + listener.await(), is(true)); + } + + @Test + public void testGo_transitionListenerEnd() throws Throwable { + final SyncTransitionListener listener = + new SyncTransitionListener(SyncTransitionListener.EVENT_END); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + Transition transition = new AutoTransition(); + transition.setDuration(0); + assertThat(transition.addListener(listener), is(sameInstance(transition))); + TransitionManager.go(mScenes[0], transition); + } + }); + assertThat("Timed out waiting for the TransitionListener", + listener.await(), is(true)); + } + + @Test + public void testGo_nullParameter() throws Throwable { + final ViewGroup root = rule.getActivity().getRoot(); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(mScenes[0], null); + assertThat(Scene.getCurrentScene(root), is(mScenes[0])); + TransitionManager.go(mScenes[1], null); + assertThat(Scene.getCurrentScene(root), is(mScenes[1])); + } + }); + } + + @Test + public void testEndTransitions() throws Throwable { + final ViewGroup root = rule.getActivity().getRoot(); + final Transition transition = new AutoTransition(); + // This transition is very long, but will be forced to end as soon as it starts + transition.setDuration(30000); + final Transition.TransitionListener listener = mock(Transition.TransitionListener.class); + transition.addListener(listener); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(mScenes[0], transition); + } + }); + verify(listener, timeout(3000)).onTransitionStart(any(Transition.class)); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.endTransitions(root); + } + }); + verify(listener, timeout(3000)).onTransitionEnd(any(Transition.class)); + } + + @Test + public void testEndTransitionsBeforeStarted() throws Throwable { + final ViewGroup root = rule.getActivity().getRoot(); + final Transition transition = new AutoTransition(); + transition.setDuration(0); + final Transition.TransitionListener listener = mock(Transition.TransitionListener.class); + transition.addListener(listener); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(mScenes[0], transition); + // This terminates the transition before it starts + TransitionManager.endTransitions(root); + } + }); + verify(listener, never()).onTransitionStart(any(Transition.class)); + verify(listener, never()).onTransitionEnd(any(Transition.class)); + } + +} diff --git a/android/support/transition/TransitionSetTest.java b/android/support/transition/TransitionSetTest.java new file mode 100644 index 00000000..aec9ecb2 --- /dev/null +++ b/android/support/transition/TransitionSetTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.view.View; + +import org.junit.Before; +import org.junit.Test; + +@MediumTest +public class TransitionSetTest extends BaseTest { + + private final TransitionSet mTransitionSet = new TransitionSet(); + private final Transition mTransition = new TransitionTest.EmptyTransition(); + + @Before + public void setUp() { + // mTransitionSet has 1 item from the start + mTransitionSet.addTransition(mTransition); + } + + @Test + public void testOrdering() { + assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_TOGETHER)); + assertThat(mTransitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL), + is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_SEQUENTIAL)); + } + + @Test + public void testAddAndRemoveTransition() { + assertThat(mTransitionSet.getTransitionCount(), is(1)); + assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition))); + Transition anotherTransition = new TransitionTest.EmptyTransition(); + assertThat(mTransitionSet.addTransition(anotherTransition), + is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTransitionCount(), is(2)); + assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition))); + assertThat(mTransitionSet.getTransitionAt(1), is(sameInstance(anotherTransition))); + assertThat(mTransitionSet.removeTransition(mTransition), + is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTransitionCount(), is(1)); + } + + @Test + public void testSetDuration() { + assertThat(mTransitionSet.setDuration(123), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getDuration(), is(123L)); + assertThat(mTransition.getDuration(), is(123L)); + } + + @Test + public void testTargetId() { + assertThat(mTransitionSet.addTarget(R.id.view0), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetIds(), hasItem(R.id.view0)); + assertThat(mTransitionSet.getTargetIds(), hasSize(1)); + assertThat(mTransition.getTargetIds(), hasItem(R.id.view0)); + assertThat(mTransition.getTargetIds(), hasSize(1)); + assertThat(mTransitionSet.removeTarget(R.id.view0), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetIds(), hasSize(0)); + assertThat(mTransition.getTargetIds(), hasSize(0)); + } + + @Test + public void testTargetView() { + final View view = new View(rule.getActivity()); + assertThat(mTransitionSet.addTarget(view), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargets(), hasItem(view)); + assertThat(mTransitionSet.getTargets(), hasSize(1)); + assertThat(mTransition.getTargets(), hasItem(view)); + assertThat(mTransition.getTargets(), hasSize(1)); + assertThat(mTransitionSet.removeTarget(view), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargets(), hasSize(0)); + assertThat(mTransition.getTargets(), hasSize(0)); + } + + @Test + public void testTargetName() { + assertThat(mTransitionSet.addTarget("abc"), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetNames(), hasItem("abc")); + assertThat(mTransitionSet.getTargetNames(), hasSize(1)); + assertThat(mTransition.getTargetNames(), hasItem("abc")); + assertThat(mTransition.getTargetNames(), hasSize(1)); + assertThat(mTransitionSet.removeTarget("abc"), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetNames(), hasSize(0)); + assertThat(mTransition.getTargetNames(), hasSize(0)); + } + + @Test + public void testTargetClass() { + assertThat(mTransitionSet.addTarget(View.class), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetTypes(), hasItem(View.class)); + assertThat(mTransitionSet.getTargetTypes(), hasSize(1)); + assertThat(mTransition.getTargetTypes(), hasItem(View.class)); + assertThat(mTransition.getTargetTypes(), hasSize(1)); + assertThat(mTransitionSet.removeTarget(View.class), is(sameInstance(mTransitionSet))); + assertThat(mTransitionSet.getTargetTypes(), hasSize(0)); + assertThat(mTransition.getTargetTypes(), hasSize(0)); + } + +} diff --git a/android/support/transition/TransitionTest.java b/android/support/transition/TransitionTest.java new file mode 100644 index 00000000..72f6dae9 --- /dev/null +++ b/android/support/transition/TransitionTest.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.MediumTest; +import android.support.transition.test.R; +import android.support.v4.view.ViewCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.LinearInterpolator; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +@MediumTest +public class TransitionTest extends BaseTest { + + private Scene[] mScenes = new Scene[2]; + private View[] mViews = new View[3]; + + @Before + public void prepareScenes() { + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity); + mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity); + } + + @Test + public void testName() { + Transition transition = new EmptyTransition(); + assertThat(transition.getName(), + is(equalTo("android.support.transition.TransitionTest$EmptyTransition"))); + } + + @Test + public void testDuration() { + Transition transition = new EmptyTransition(); + long duration = 12345; + assertThat(transition.setDuration(duration), is(sameInstance(transition))); + assertThat(transition.getDuration(), is(duration)); + } + + @Test + public void testInterpolator() { + Transition transition = new EmptyTransition(); + TimeInterpolator interpolator = new LinearInterpolator(); + assertThat(transition.setInterpolator(interpolator), is(sameInstance(transition))); + assertThat(transition.getInterpolator(), is(interpolator)); + } + + @Test + public void testStartDelay() { + Transition transition = new EmptyTransition(); + long startDelay = 12345; + assertThat(transition.setStartDelay(startDelay), is(sameInstance(transition))); + assertThat(transition.getStartDelay(), is(startDelay)); + } + + @Test + public void testTargetIds() { + Transition transition = new EmptyTransition(); + assertThat(transition.addTarget(R.id.view0), is(sameInstance(transition))); + assertThat(transition.addTarget(R.id.view1), is(sameInstance(transition))); + List targetIds = transition.getTargetIds(); + assertThat(targetIds.size(), is(2)); + assertThat(targetIds, hasItem(R.id.view0)); + assertThat(targetIds, hasItem(R.id.view1)); + assertThat(transition.removeTarget(R.id.view0), is(sameInstance(transition))); + targetIds = transition.getTargetIds(); + assertThat(targetIds.size(), is(1)); + assertThat(targetIds, not(hasItem(R.id.view0))); + assertThat(targetIds, hasItem(R.id.view1)); + } + + @Test + @UiThreadTest + public void testTargetView() { + // Set up views + TransitionActivity activity = rule.getActivity(); + ViewGroup root = activity.getRoot(); + View container = LayoutInflater.from(activity) + .inflate(R.layout.support_scene0, root, false); + root.addView(container); + View view0 = container.findViewById(R.id.view0); + View view1 = container.findViewById(R.id.view1); + // Test transition targets + Transition transition = new EmptyTransition(); + assertThat(transition.addTarget(view0), is(sameInstance(transition))); + assertThat(transition.addTarget(view1), is(sameInstance(transition))); + List targets = transition.getTargets(); + assertThat(targets.size(), is(2)); + assertThat(targets, hasItem(sameInstance(view0))); + assertThat(targets, hasItem(sameInstance(view1))); + assertThat(transition.removeTarget(view0), is(sameInstance(transition))); + targets = transition.getTargets(); + assertThat(targets.size(), is(1)); + assertThat(targets, not(hasItem(sameInstance(view0)))); + assertThat(targets, hasItem(sameInstance(view1))); + } + + @Test + public void testTargetName() { + Transition transition = new EmptyTransition(); + assertThat(transition.addTarget("a"), is(sameInstance(transition))); + assertThat(transition.addTarget("b"), is(sameInstance(transition))); + List targetNames = transition.getTargetNames(); + assertNotNull(targetNames); + assertThat(targetNames.size(), is(2)); + assertThat(targetNames, hasItem("a")); + assertThat(targetNames, hasItem("b")); + transition.removeTarget("a"); + assertThat(targetNames.size(), is(1)); + assertThat(targetNames, not(hasItem("a"))); + assertThat(targetNames, hasItem("b")); + } + + @Test + public void testTargetType() { + Transition transition = new EmptyTransition(); + assertThat(transition.addTarget(Button.class), is(sameInstance(transition))); + assertThat(transition.addTarget(ImageView.class), is(sameInstance(transition))); + List targetTypes = transition.getTargetTypes(); + assertNotNull(targetTypes); + assertThat(targetTypes.size(), is(2)); + assertThat(targetTypes, hasItem(Button.class)); + assertThat(targetTypes, hasItem(ImageView.class)); + transition.removeTarget(Button.class); + assertThat(targetTypes.size(), is(1)); + assertThat(targetTypes, not(hasItem(Button.class))); + assertThat(targetTypes, hasItem(ImageView.class)); + } + + @Test + public void testExcludeTargetId() throws Throwable { + showInitialScene(); + Transition transition = new EmptyTransition(); + transition.addTarget(R.id.view0); + transition.addTarget(R.id.view1); + View view0 = rule.getActivity().findViewById(R.id.view0); + View view1 = rule.getActivity().findViewById(R.id.view1); + assertThat(transition.isValidTarget(view0), is(true)); + assertThat(transition.isValidTarget(view1), is(true)); + transition.excludeTarget(R.id.view0, true); + assertThat(transition.isValidTarget(view0), is(false)); + assertThat(transition.isValidTarget(view1), is(true)); + } + + @Test + public void testExcludeTargetView() throws Throwable { + showInitialScene(); + Transition transition = new EmptyTransition(); + View view0 = rule.getActivity().findViewById(R.id.view0); + View view1 = rule.getActivity().findViewById(R.id.view1); + transition.addTarget(view0); + transition.addTarget(view1); + assertThat(transition.isValidTarget(view0), is(true)); + assertThat(transition.isValidTarget(view1), is(true)); + transition.excludeTarget(view0, true); + assertThat(transition.isValidTarget(view0), is(false)); + assertThat(transition.isValidTarget(view1), is(true)); + } + + @Test + public void testExcludeTargetName() throws Throwable { + showInitialScene(); + Transition transition = new EmptyTransition(); + View view0 = rule.getActivity().findViewById(R.id.view0); + View view1 = rule.getActivity().findViewById(R.id.view1); + ViewCompat.setTransitionName(view0, "zero"); + ViewCompat.setTransitionName(view1, "one"); + transition.addTarget("zero"); + transition.addTarget("one"); + assertThat(transition.isValidTarget(view0), is(true)); + assertThat(transition.isValidTarget(view1), is(true)); + transition.excludeTarget("zero", true); + assertThat(transition.isValidTarget(view0), is(false)); + assertThat(transition.isValidTarget(view1), is(true)); + } + + @Test + public void testExcludeTargetType() throws Throwable { + showInitialScene(); + Transition transition = new EmptyTransition(); + FrameLayout container = (FrameLayout) rule.getActivity().findViewById(R.id.container); + View view0 = rule.getActivity().findViewById(R.id.view0); + transition.addTarget(View.class); + assertThat(transition.isValidTarget(container), is(true)); + assertThat(transition.isValidTarget(view0), is(true)); + transition.excludeTarget(FrameLayout.class, true); + assertThat(transition.isValidTarget(container), is(false)); + assertThat(transition.isValidTarget(view0), is(true)); + } + + @Test + public void testListener() { + Transition transition = new EmptyTransition(); + Transition.TransitionListener listener = new EmptyTransitionListener(); + assertThat(transition.addListener(listener), is(sameInstance(transition))); + assertThat(transition.removeListener(listener), is(sameInstance(transition))); + } + + @Test + public void testMatchOrder() throws Throwable { + showInitialScene(); + final Transition transition = new ChangeBounds() { + @Nullable + @Override + public Animator createAnimator(@NonNull ViewGroup sceneRoot, + @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) { + if (startValues != null && endValues != null) { + fail("Match by View ID should be prevented"); + } + return super.createAnimator(sceneRoot, startValues, endValues); + } + }; + transition.setDuration(0); + // This prevents matches between start and end scenes because they have different set of + // View instances. They will be regarded as independent views even though they share the + // same View IDs. + transition.setMatchOrder(Transition.MATCH_INSTANCE); + SyncRunnable enter1 = new SyncRunnable(); + mScenes[1].setEnterAction(enter1); + goToScene(mScenes[1], transition); + if (!enter1.await()) { + fail("Timed out while waiting for scene change"); + } + } + + @Test + public void testExcludedTransitionAnimator() throws Throwable { + showInitialScene(); + final Animator.AnimatorListener animatorListener = mock(Animator.AnimatorListener.class); + final DummyTransition transition = new DummyTransition(animatorListener); + final SyncTransitionListener transitionListener = new SyncTransitionListener( + SyncTransitionListener.EVENT_END); + transition.addListener(transitionListener); + transition.addTarget(mViews[0]); + transition.excludeTarget(mViews[0], true); + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.beginDelayedTransition(rule.getActivity().getRoot(), transition); + mViews[0].setTranslationX(3.f); + } + }); + if (!transitionListener.await()) { + fail("Timed out waiting for the TransitionListener"); + } + verify(animatorListener, never()).onAnimationStart(any(Animator.class)); + } + + @Test + public void testEpicenter() throws Throwable { + final Transition transition = new EmptyTransition(); + final Transition.EpicenterCallback epicenterCallback = new Transition.EpicenterCallback() { + private Rect mRect = new Rect(); + + @Override + public Rect onGetEpicenter(@NonNull Transition t) { + assertThat(t, is(sameInstance(transition))); + mRect.set(1, 2, 3, 4); + return mRect; + } + }; + transition.setEpicenterCallback(epicenterCallback); + assertThat(transition.getEpicenterCallback(), + is(sameInstance(transition.getEpicenterCallback()))); + Rect rect = transition.getEpicenter(); + assertNotNull(rect); + assertThat(rect.left, is(1)); + assertThat(rect.top, is(2)); + assertThat(rect.right, is(3)); + assertThat(rect.bottom, is(4)); + } + + @Test + public void testSetPropagation() throws Throwable { + final Transition transition = new EmptyTransition(); + assertThat(transition.getPropagation(), is(nullValue())); + final TransitionPropagation propagation = new CircularPropagation(); + transition.setPropagation(propagation); + assertThat(propagation, is(sameInstance(propagation))); + } + + @Test + public void testIsTransitionRequired() throws Throwable { + final EmptyTransition transition = new EmptyTransition(); + assertThat(transition.isTransitionRequired(null, null), is(false)); + final TransitionValues start = new TransitionValues(); + final String propname = "android:transition:dummy"; + start.values.put(propname, 1); + final TransitionValues end = new TransitionValues(); + end.values.put(propname, 1); + assertThat(transition.isTransitionRequired(start, end), is(false)); + end.values.put(propname, 2); + assertThat(transition.isTransitionRequired(start, end), is(true)); + } + + private void showInitialScene() throws Throwable { + SyncRunnable enter0 = new SyncRunnable(); + mScenes[0].setEnterAction(enter0); + AutoTransition transition1 = new AutoTransition(); + transition1.setDuration(0); + goToScene(mScenes[0], transition1); + if (!enter0.await()) { + fail("Timed out while waiting for scene change"); + } + mViews[0] = rule.getActivity().findViewById(R.id.view0); + mViews[1] = rule.getActivity().findViewById(R.id.view1); + mViews[2] = rule.getActivity().findViewById(R.id.view2); + } + + private void goToScene(final Scene scene, final Transition transition) throws Throwable { + rule.runOnUiThread(new Runnable() { + @Override + public void run() { + TransitionManager.go(scene, transition); + } + }); + } + + public static class EmptyTransition extends Transition { + + @Override + public void captureEndValues(@NonNull TransitionValues transitionValues) { + } + + @Override + public void captureStartValues(@NonNull TransitionValues transitionValues) { + } + + @Override + public Animator createAnimator(@NonNull ViewGroup sceneRoot, + @Nullable TransitionValues startValues, + @Nullable TransitionValues endValues) { + return null; + } + + } + + public static class EmptyTransitionListener implements Transition.TransitionListener { + + @Override + public void onTransitionStart(@NonNull Transition transition) { + } + + @Override + public void onTransitionEnd(@NonNull Transition transition) { + } + + @Override + public void onTransitionCancel(@NonNull Transition transition) { + } + + @Override + public void onTransitionPause(@NonNull Transition transition) { + } + + @Override + public void onTransitionResume(@NonNull Transition transition) { + } + + } + + /** + * A dummy transition for monitoring use of its animator by the Transition framework. + */ + private static class DummyTransition extends Transition { + + private final Animator.AnimatorListener mListener; + + DummyTransition(Animator.AnimatorListener listener) { + mListener = listener; + } + + @Override + public void captureStartValues(@NonNull TransitionValues transitionValues) { + transitionValues.values.put("state", 1); + } + + @Override + public void captureEndValues(@NonNull TransitionValues transitionValues) { + transitionValues.values.put("state", 2); + } + + @Override + public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + final ObjectAnimator animator = ObjectAnimator + .ofFloat(startValues.view, "translationX", 1.f, 2.f); + animator.addListener(mListener); + return animator; + } + + } +} diff --git a/android/support/transition/VisibilityTest.java b/android/support/transition/VisibilityTest.java new file mode 100644 index 00000000..dcfcbdc3 --- /dev/null +++ b/android/support/transition/VisibilityTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.transition; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.MediumTest; +import android.view.View; +import android.view.ViewGroup; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +@MediumTest +public class VisibilityTest extends BaseTest { + + private View mView; + private ViewGroup mRoot; + + @UiThreadTest + @Before + public void setUp() { + mRoot = rule.getActivity().getRoot(); + mView = new View(rule.getActivity()); + mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100)); + } + + @Test + public void testMode() { + final CustomVisibility visibility = new CustomVisibility(); + assertThat(visibility.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT)); + visibility.setMode(Visibility.MODE_IN); + assertThat(visibility.getMode(), is(Visibility.MODE_IN)); + } + + @Test + @UiThreadTest + public void testCustomVisibility() { + final CustomVisibility visibility = new CustomVisibility(); + assertThat(visibility.getName(), is(equalTo(CustomVisibility.class.getName()))); + assertNotNull(visibility.getTransitionProperties()); + + // Capture start values + mView.setScaleX(0.5f); + final TransitionValues startValues = new TransitionValues(); + startValues.view = mView; + visibility.captureStartValues(startValues); + assertThat((float) startValues.values.get(CustomVisibility.PROPNAME_SCALE_X), is(0.5f)); + + // Hide the view and capture end values + mView.setVisibility(View.GONE); + final TransitionValues endValues = new TransitionValues(); + endValues.view = mView; + visibility.captureEndValues(endValues); + + // This should invoke onDisappear, not onAppear + ObjectAnimator animator = (ObjectAnimator) visibility + .createAnimator(mRoot, startValues, endValues); + assertNotNull(animator); + assertThat(animator.getPropertyName(), is(equalTo("scaleX"))); + + // Jump to the end of the animation + animator.end(); + + // This value confirms that onDisappear, not onAppear, was called + assertThat((float) animator.getAnimatedValue(), is(0.25f)); + } + + @Test + @UiThreadTest + public void testCustomVisibility2() { + final CustomVisibility2 visibility = new CustomVisibility2(); + final TransitionValues startValues = new TransitionValues(); + startValues.view = mView; + visibility.captureStartValues(startValues); + mView.setVisibility(View.GONE); + final TransitionValues endValues = new TransitionValues(); + endValues.view = mView; + visibility.captureEndValues(endValues); + ObjectAnimator animator = (ObjectAnimator) visibility + .createAnimator(mRoot, startValues, endValues); + assertNotNull(animator); + + // Jump to the end of the animation + animator.end(); + + // This value confirms that onDisappear, not onAppear, was called + assertThat((float) animator.getAnimatedValue(), is(0.25f)); + } + + /** + * A custom {@link Visibility} with 5-arg onAppear/Disappear + */ + public static class CustomVisibility extends Visibility { + + static final String PROPNAME_SCALE_X = "customVisibility:scaleX"; + + private static String[] sTransitionProperties; + + @Nullable + @Override + public String[] getTransitionProperties() { + if (sTransitionProperties == null) { + String[] properties = super.getTransitionProperties(); + if (properties != null) { + sTransitionProperties = Arrays.copyOf(properties, properties.length + 1); + } else { + sTransitionProperties = new String[1]; + } + sTransitionProperties[sTransitionProperties.length - 1] = PROPNAME_SCALE_X; + } + return sTransitionProperties; + } + + @Override + public void captureStartValues(@NonNull TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX()); + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, + int startVisibility, TransitionValues endValues, int endVisibility) { + if (startValues == null) { + return null; + } + float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X); + return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.75f); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues, + int startVisibility, TransitionValues endValues, int endVisibility) { + if (startValues == null) { + return null; + } + float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X); + return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.25f); + } + + } + + /** + * A custom {@link Visibility} with 4-arg onAppear/Disappear + */ + public static class CustomVisibility2 extends Visibility { + + static final String PROPNAME_SCALE_X = "customVisibility:scaleX"; + + @Override + public void captureStartValues(@NonNull TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX()); + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { + float startScaleX = startValues == null ? 0.25f : + (float) startValues.values.get(PROPNAME_SCALE_X); + return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.75f); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null) { + return null; + } + float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X); + return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.25f); + } + + } + +} diff --git a/android/support/v13/app/ActivityCompat.java b/android/support/v13/app/ActivityCompat.java index b0c3c30c..7c1546a5 100644 --- a/android/support/v13/app/ActivityCompat.java +++ b/android/support/v13/app/ActivityCompat.java @@ -16,32 +16,22 @@ package android.support.v13.app; -import android.app.Activity; -import android.support.v13.view.DragAndDropPermissionsCompat; -import android.view.DragEvent; - /** * Helper for accessing features in {@link android.app.Activity} in a backwards compatible fashion. + * + * @deprecated Use {@link android.support.v4.app.ActivityCompat + * android.support.v4.app.ActivityCompat}. */ +@Deprecated public class ActivityCompat extends android.support.v4.app.ActivityCompat { - - /** - * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling - * the access permissions for content URIs associated with the {@link android.view.DragEvent}. - * @param dragEvent Drag event to request permission for - * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content - * URIs. {@code null} if no content URIs are associated with the event or if permissions could - * not be granted. - */ - public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity, - DragEvent dragEvent) { - return DragAndDropPermissionsCompat.request(activity, dragEvent); - } - /** * This class should not be instantiated, but the constructor must be * visible for the class to be extended. + * + * @deprecated Use {@link android.support.v4.app.ActivityCompat + * android.support.v4.app.ActivityCompat}. */ + @Deprecated protected ActivityCompat() { // Not publicly instantiable, but may be extended. } diff --git a/android/support/v13/app/FragmentCompat.java b/android/support/v13/app/FragmentCompat.java index 31c2343e..e8915fb8 100644 --- a/android/support/v13/app/FragmentCompat.java +++ b/android/support/v13/app/FragmentCompat.java @@ -30,8 +30,19 @@ import java.util.Arrays; /** * Helper for accessing features in {@link Fragment} in a backwards compatible fashion. + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework fragment. */ +@Deprecated public class FragmentCompat { + + /** + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework fragment. + */ + @Deprecated + public FragmentCompat() { + } + interface FragmentCompatImpl { void setUserVisibleHint(Fragment f, boolean deferStart); void requestPermissions(Fragment fragment, String[] permissions, int requestCode); @@ -48,7 +59,11 @@ public class FragmentCompat { * to the compatibility methods in this class will first check whether the delegate can * handle the method call, and invoke the corresponding method if it can. *

    + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public interface PermissionCompatDelegate { /** @@ -66,7 +81,11 @@ public class FragmentCompat { * * @return Whether the delegate has handled the permission request. * @see FragmentCompat#requestPermissions(Fragment, String[], int) + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated boolean requestPermissions(Fragment fragment, String[] permissions, int requestCode); } @@ -157,22 +176,34 @@ public class FragmentCompat { * delegate. * * @param delegate The delegate to be set. {@code null} to clear the set delegate. + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public static void setPermissionCompatDelegate(PermissionCompatDelegate delegate) { sDelegate = delegate; } /** * @hide + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Deprecated public static PermissionCompatDelegate getPermissionCompatDelegate() { return sDelegate; } /** * This interface is the contract for receiving the results for permission requests. + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public interface OnRequestPermissionsResultCallback { /** @@ -188,7 +219,11 @@ public class FragmentCompat { * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null. * * @see #requestPermissions(android.app.Fragment, String[], int) + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults); } @@ -197,7 +232,8 @@ public class FragmentCompat { * Call {@link Fragment#setMenuVisibility(boolean) Fragment.setMenuVisibility(boolean)} * if running on an appropriate version of the platform. * - * @deprecated Use {@link Fragment#setMenuVisibility(boolean)} directly. + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ @Deprecated public static void setMenuVisibility(Fragment f, boolean visible) { @@ -207,7 +243,11 @@ public class FragmentCompat { /** * Call {@link Fragment#setUserVisibleHint(boolean) setUserVisibleHint(boolean)} * if running on an appropriate version of the platform. + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public static void setUserVisibleHint(Fragment f, boolean deferStart) { IMPL.setUserVisibleHint(f, deferStart); } @@ -262,7 +302,11 @@ public class FragmentCompat { * @see android.support.v4.content.ContextCompat#checkSelfPermission( * android.content.Context, String) * @see #shouldShowRequestPermissionRationale(android.app.Fragment, String) + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public static void requestPermissions(@NonNull Fragment fragment, @NonNull String[] permissions, int requestCode) { if (sDelegate != null && sDelegate.requestPermissions(fragment, permissions, requestCode)) { @@ -293,7 +337,11 @@ public class FragmentCompat { * @see android.support.v4.content.ContextCompat#checkSelfPermission( * android.content.Context, String) * @see #requestPermissions(android.app.Fragment, String[], int) + * + * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework + * {@link Fragment}. */ + @Deprecated public static boolean shouldShowRequestPermissionRationale(@NonNull Fragment fragment, @NonNull String permission) { return IMPL.shouldShowRequestPermissionRationale(fragment, permission); diff --git a/android/support/v13/app/FragmentPagerAdapter.java b/android/support/v13/app/FragmentPagerAdapter.java index e0b788ab..112ed023 100644 --- a/android/support/v13/app/FragmentPagerAdapter.java +++ b/android/support/v13/app/FragmentPagerAdapter.java @@ -61,7 +61,10 @@ import android.view.ViewGroup; * * {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager_list.xml * complete} + * + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. */ +@Deprecated public abstract class FragmentPagerAdapter extends PagerAdapter { private static final String TAG = "FragmentPagerAdapter"; private static final boolean DEBUG = false; @@ -70,15 +73,26 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { private FragmentTransaction mCurTransaction = null; private Fragment mCurrentPrimaryItem = null; + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated public FragmentPagerAdapter(FragmentManager fm) { mFragmentManager = fm; } /** * Return the Fragment associated with a specified position. + * + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. */ + @Deprecated public abstract Fragment getItem(int position); + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public void startUpdate(ViewGroup container) { if (container.getId() == View.NO_ID) { @@ -87,6 +101,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @SuppressWarnings("ReferenceEquality") @Override public Object instantiateItem(ViewGroup container, int position) { @@ -116,6 +134,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { return fragment; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public void destroyItem(ViewGroup container, int position, Object object) { if (mCurTransaction == null) { @@ -126,6 +148,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { mCurTransaction.detach((Fragment)object); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @SuppressWarnings("ReferenceEquality") @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { @@ -143,6 +169,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public void finishUpdate(ViewGroup container) { if (mCurTransaction != null) { @@ -152,16 +182,28 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public boolean isViewFromObject(View view, Object object) { return ((Fragment)object).getView() == view; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public Parcelable saveState() { return null; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. + */ + @Deprecated @Override public void restoreState(Parcelable state, ClassLoader loader) { } @@ -174,7 +216,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter { * * @param position Position within this adapter * @return Unique identifier for the item at position + * + * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead. */ + @Deprecated public long getItemId(int position) { return position; } diff --git a/android/support/v13/app/FragmentStatePagerAdapter.java b/android/support/v13/app/FragmentStatePagerAdapter.java index 45a6bf53..76a32245 100644 --- a/android/support/v13/app/FragmentStatePagerAdapter.java +++ b/android/support/v13/app/FragmentStatePagerAdapter.java @@ -64,7 +64,10 @@ import java.util.ArrayList; * * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml * complete} + * + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. */ +@Deprecated public abstract class FragmentStatePagerAdapter extends PagerAdapter { private static final String TAG = "FragStatePagerAdapter"; private static final boolean DEBUG = false; @@ -76,15 +79,26 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { private ArrayList mFragments = new ArrayList(); private Fragment mCurrentPrimaryItem = null; + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated public FragmentStatePagerAdapter(FragmentManager fm) { mFragmentManager = fm; } /** * Return the Fragment associated with a specified position. + * + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. */ + @Deprecated public abstract Fragment getItem(int position); + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public void startUpdate(ViewGroup container) { if (container.getId() == View.NO_ID) { @@ -93,6 +107,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public Object instantiateItem(ViewGroup container, int position) { // If we already have this item instantiated, there is nothing @@ -129,6 +147,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { return fragment; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public void destroyItem(ViewGroup container, int position, Object object) { Fragment fragment = (Fragment) object; @@ -148,6 +170,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { mCurTransaction.remove(fragment); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @SuppressWarnings("ReferenceEquality") @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { @@ -165,6 +191,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public void finishUpdate(ViewGroup container) { if (mCurTransaction != null) { @@ -174,11 +204,19 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public boolean isViewFromObject(View view, Object object) { return ((Fragment)object).getView() == view; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public Parcelable saveState() { Bundle state = null; @@ -201,6 +239,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter { return state; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead. + */ + @Deprecated @Override public void restoreState(Parcelable state, ClassLoader loader) { if (state != null) { diff --git a/android/support/v13/app/FragmentTabHost.java b/android/support/v13/app/FragmentTabHost.java index 2326ccb6..5c34ab57 100644 --- a/android/support/v13/app/FragmentTabHost.java +++ b/android/support/v13/app/FragmentTabHost.java @@ -38,7 +38,10 @@ import java.util.ArrayList; * Version of {@link android.support.v4.app.FragmentTabHost} that can be * used with the platform {@link android.app.Fragment} APIs. You will not * normally use this, instead using action bar tabs. + * + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. */ +@Deprecated public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListener { private final ArrayList mTabs = new ArrayList(); private FrameLayout mRealTabContent; @@ -117,6 +120,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe }; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated public FragmentTabHost(Context context) { // Note that we call through to the version that takes an AttributeSet, // because the simple Context construct can result in a broken object! @@ -124,6 +131,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe initFragmentTabHost(context, null); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated public FragmentTabHost(Context context, AttributeSet attrs) { super(context, attrs); initFragmentTabHost(context, attrs); @@ -167,9 +178,7 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe } /** - * @deprecated Don't call the original TabHost setup, you must instead - * call {@link #setup(Context, FragmentManager)} or - * {@link #setup(Context, FragmentManager, int)}. + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. */ @Override @Deprecated @@ -178,6 +187,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe "Must call setup() that takes a Context and FragmentManager"); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated public void setup(Context context, FragmentManager manager) { ensureHierarchy(context); // Ensure views required by super.setup() super.setup(); @@ -186,6 +199,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe ensureContent(); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated public void setup(Context context, FragmentManager manager, int containerId) { ensureHierarchy(context); // Ensure views required by super.setup() super.setup(); @@ -212,11 +229,19 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override public void setOnTabChangedListener(OnTabChangeListener l) { mOnTabChangeListener = l; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { tabSpec.setContent(new DummyTabFactory(mContext)); String tag = tabSpec.getTag(); @@ -239,6 +264,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe addTab(tabSpec); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -278,12 +307,20 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe } } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttached = false; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); @@ -292,6 +329,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe return ss; } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { @@ -303,6 +344,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe setCurrentTabByTag(ss.curTab); } + /** + * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead. + */ + @Deprecated @Override public void onTabChanged(String tabId) { if (mAttached) { diff --git a/android/support/v17/leanback/app/BrowseFragment.java b/android/support/v17/leanback/app/BrowseFragment.java index c561ea99..a2439e43 100644 --- a/android/support/v17/leanback/app/BrowseFragment.java +++ b/android/support/v17/leanback/app/BrowseFragment.java @@ -1486,6 +1486,9 @@ public class BrowseFragment extends BaseFragment { if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { return; } + if (mMainFragment == null || mMainFragment.getView() == null) { + return; + } startHeadersTransitionInternal(false); mMainFragment.getView().requestFocus(); } diff --git a/android/support/v17/leanback/app/BrowseSupportFragment.java b/android/support/v17/leanback/app/BrowseSupportFragment.java index c28064ca..114e0a7a 100644 --- a/android/support/v17/leanback/app/BrowseSupportFragment.java +++ b/android/support/v17/leanback/app/BrowseSupportFragment.java @@ -1463,6 +1463,9 @@ public class BrowseSupportFragment extends BaseSupportFragment { if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { return; } + if (mMainFragment == null || mMainFragment.getView() == null) { + return; + } startHeadersTransitionInternal(false); mMainFragment.getView().requestFocus(); } diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index 9d159eca..613198fd 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -22,6 +22,7 @@ import static android.support.v7.widget.RecyclerView.VERTICAL; import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -3655,7 +3656,32 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { public boolean performAccessibilityAction(Recycler recycler, State state, int action, Bundle args) { saveContext(recycler, state); - switch (action) { + int translatedAction = action; + boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0; + if (Build.VERSION.SDK_INT >= 23) { + if (mOrientation == HORIZONTAL) { + if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_LEFT.getId()) { + translatedAction = reverseFlowPrimary + ? AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD : + AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD; + } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_RIGHT.getId()) { + translatedAction = reverseFlowPrimary + ? AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD : + AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD; + } + } else { // VERTICAL layout + if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP + .getId()) { + translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD; + } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_DOWN.getId()) { + translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD; + } + } + } + switch (translatedAction) { case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: processSelectionMoves(false, -1); break; @@ -3726,12 +3752,39 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { AccessibilityNodeInfoCompat info) { saveContext(recycler, state); int count = state.getItemCount(); - if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(0)) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0; + if (count > 1 && !isItemFullyVisible(0)) { + if (Build.VERSION.SDK_INT >= 23) { + if (mOrientation == HORIZONTAL) { + info.addAction(reverseFlowPrimary + ? AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_RIGHT : + AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_LEFT); + } else { + info.addAction( + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP); + } + } else { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } info.setScrollable(true); } - if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(count - 1)) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + if (count > 1 && !isItemFullyVisible(count - 1)) { + if (Build.VERSION.SDK_INT >= 23) { + if (mOrientation == HORIZONTAL) { + info.addAction(reverseFlowPrimary + ? AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_LEFT : + AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_RIGHT); + } else { + info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat + .ACTION_SCROLL_DOWN); + } + } else { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } info.setScrollable(true); } final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = diff --git a/android/support/v17/leanback/widget/WindowAlignment.java b/android/support/v17/leanback/widget/WindowAlignment.java index 55fa7589..c6589d49 100644 --- a/android/support/v17/leanback/widget/WindowAlignment.java +++ b/android/support/v17/leanback/widget/WindowAlignment.java @@ -390,11 +390,7 @@ class WindowAlignment { @Override public String toString() { - return new StringBuffer().append("horizontal=") - .append(horizontal.toString()) - .append("; vertical=") - .append(vertical.toString()) - .toString(); + return "horizontal=" + horizontal + "; vertical=" + vertical; } } diff --git a/android/support/v4/app/ActivityCompat.java b/android/support/v4/app/ActivityCompat.java index 5833481a..9d15be1d 100644 --- a/android/support/v4/app/ActivityCompat.java +++ b/android/support/v4/app/ActivityCompat.java @@ -34,7 +34,9 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; +import android.support.v13.view.DragAndDropPermissionsCompat; import android.support.v4.content.ContextCompat; +import android.view.DragEvent; import android.view.View; import java.util.List; @@ -528,6 +530,19 @@ public class ActivityCompat extends ContextCompat { return false; } + /** + * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling + * the access permissions for content URIs associated with the {@link android.view.DragEvent}. + * @param dragEvent Drag event to request permission for + * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content + * URIs. {@code null} if no content URIs are associated with the event or if permissions could + * not be granted. + */ + public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity, + DragEvent dragEvent) { + return DragAndDropPermissionsCompat.request(activity, dragEvent); + } + @RequiresApi(21) private static class SharedElementCallback21Impl extends android.app.SharedElementCallback { diff --git a/android/support/v4/app/Fragment.java b/android/support/v4/app/Fragment.java index e734a274..5b560cd2 100644 --- a/android/support/v4/app/Fragment.java +++ b/android/support/v4/app/Fragment.java @@ -1816,8 +1816,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * use the same value as set in {@link #setEnterTransition(Object)}. * * @param transition The Transition to use to move Views out of the Scene when the Fragment - * is preparing to close. transition must be an - * android.transition.Transition. + * is preparing to close. transition must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link android.support.transition.Transition android.support.transition.Transition}. */ public void setReturnTransition(@Nullable Object transition) { ensureAnimationInfo().mReturnTransition = transition; @@ -1854,8 +1855,10 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * remain unaffected. * * @param transition The Transition to use to move Views out of the Scene when the Fragment - * is being closed not due to popping the back stack. transition - * must be an android.transition.Transition. + * is being closed not due to popping the back stack. transition + * must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link android.support.transition.Transition android.support.transition.Transition}. */ public void setExitTransition(@Nullable Object transition) { ensureAnimationInfo().mExitTransition = transition; @@ -1891,8 +1894,10 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * transition as {@link #setExitTransition(Object)}. * * @param transition The Transition to use to move Views into the scene when reentering from a - * previously-started Activity. transition - * must be an android.transition.Transition. + * previously-started Activity. transition + * must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link android.support.transition.Transition android.support.transition.Transition}. */ public void setReenterTransition(@Nullable Object transition) { ensureAnimationInfo().mReenterTransition = transition; @@ -1925,7 +1930,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * value will cause transferred shared elements to blink to the final position. * * @param transition The Transition to use for shared elements transferred into the content - * Scene. transition must be an android.transition.Transition. + * Scene. transition must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link android.support.transition.Transition android.support.transition.Transition}. */ public void setSharedElementEnterTransition(@Nullable Object transition) { ensureAnimationInfo().mSharedElementEnterTransition = transition; @@ -1958,7 +1965,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * {@link #setSharedElementEnterTransition(Object)}. * * @param transition The Transition to use for shared elements transferred out of the content - * Scene. transition must be an android.transition.Transition. + * Scene. transition must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link android.support.transition.Transition android.support.transition.Transition}. */ public void setSharedElementReturnTransition(@Nullable Object transition) { ensureAnimationInfo().mSharedElementReturnTransition = transition; diff --git a/android/support/v4/app/NotificationCompat.java b/android/support/v4/app/NotificationCompat.java index 1077b1f0..6f74e18c 100644 --- a/android/support/v4/app/NotificationCompat.java +++ b/android/support/v4/app/NotificationCompat.java @@ -32,6 +32,7 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; +import android.media.AudioAttributes; import android.media.AudioManager; import android.net.Uri; import android.os.Build; @@ -116,7 +117,6 @@ public class NotificationCompat { * default stream type is {@link AudioManager#STREAM_NOTIFICATION}. */ public static final int STREAM_DEFAULT = -1; - /** * Bit set in the Notification flags field when LEDs should be turned on * for this notification. @@ -403,6 +403,12 @@ public class NotificationCompat { */ public static final String EXTRA_MESSAGES = "android.messages"; + /** + * Notification key: whether the {@link NotificationCompat.MessagingStyle} notification + * represents a group conversation. + */ + public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; + /** * Keys into the {@link #getExtras} Bundle: the audio contents of this notification. * @@ -438,6 +444,14 @@ public class NotificationCompat { @ColorInt public static final int COLOR_DEFAULT = Color.TRANSPARENT; + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + @IntDef({AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING, + AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM, AudioManager.STREAM_NOTIFICATION, + AudioManager.STREAM_DTMF, AudioManager.STREAM_ACCESSIBILITY}) + @Retention(RetentionPolicy.SOURCE) + public @interface StreamType {} + /** @hide */ @Retention(SOURCE) @IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET}) @@ -957,6 +971,12 @@ public class NotificationCompat { public Builder setSound(Uri sound) { mNotification.sound = sound; mNotification.audioStreamType = Notification.STREAM_DEFAULT; + if (Build.VERSION.SDK_INT >= 21) { + mNotification.audioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build(); + } return this; } @@ -971,9 +991,15 @@ public class NotificationCompat { * @see Notification#STREAM_DEFAULT * @see AudioManager for the STREAM_ constants. */ - public Builder setSound(Uri sound, int streamType) { + public Builder setSound(Uri sound, @StreamType int streamType) { mNotification.sound = sound; mNotification.audioStreamType = streamType; + if (Build.VERSION.SDK_INT >= 21) { + mNotification.audioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(streamType) + .build(); + } return this; } @@ -2062,8 +2088,9 @@ public class NotificationCompat { public static final int MAXIMUM_RETAINED_MESSAGES = 25; CharSequence mUserDisplayName; - CharSequence mConversationTitle; + @Nullable CharSequence mConversationTitle; List mMessages = new ArrayList<>(); + boolean mIsGroupConversation; MessagingStyle() { } @@ -2086,20 +2113,19 @@ public class NotificationCompat { } /** - * Sets the title to be displayed on this conversation. This should only be used for - * group messaging and left unset for one-on-one conversations. + * Sets the title to be displayed on this conversation. May be set to {@code null}. * @param conversationTitle Title displayed for this conversation. * @return this object for method chaining. */ - public MessagingStyle setConversationTitle(CharSequence conversationTitle) { + public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) { mConversationTitle = conversationTitle; return this; } /** - * Return the title to be displayed on this conversation. Can be null and - * should be for one-on-one conversations + * Return the title to be displayed on this conversation. Can be {@code null}. */ + @Nullable public CharSequence getConversationTitle() { return mConversationTitle; } @@ -2147,6 +2173,24 @@ public class NotificationCompat { return mMessages; } + /** + * Sets whether this conversation notification represents a group. + * @param isGroupConversation {@code true} if the conversation represents a group, + * {@code false} otherwise. + * @return this object for method chaining + */ + public MessagingStyle setGroupConversation(boolean isGroupConversation) { + mIsGroupConversation = isGroupConversation; + return this; + } + + /** + * Returns {@code true} if this notification represents a group conversation. + */ + public boolean isGroupConversation() { + return mIsGroupConversation; + } + /** * Retrieves a {@link MessagingStyle} from a {@link Notification}, enabling an application * that has set a {@link MessagingStyle} using {@link NotificationCompat} or @@ -2295,6 +2339,7 @@ public class NotificationCompat { if (!mMessages.isEmpty()) { extras.putParcelableArray(EXTRA_MESSAGES, Message.getBundleArrayForMessages(mMessages)); } + extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation); } /** @@ -2310,6 +2355,7 @@ public class NotificationCompat { if (parcelables != null) { mMessages = Message.getMessagesFromBundleArray(parcelables); } + mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION); } public static final class Message { diff --git a/android/support/v4/app/NotificationCompatBuilder.java b/android/support/v4/app/NotificationCompatBuilder.java index 71f4160f..db775a55 100644 --- a/android/support/v4/app/NotificationCompatBuilder.java +++ b/android/support/v4/app/NotificationCompatBuilder.java @@ -28,6 +28,7 @@ import android.app.Notification; import android.os.Build; import android.os.Bundle; import android.support.annotation.RestrictTo; +import android.text.TextUtils; import android.util.SparseArray; import android.widget.RemoteViews; @@ -69,7 +70,6 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso .setSmallIcon(n.icon, n.iconLevel) .setContent(n.contentView) .setTicker(n.tickerText, b.mTickerView) - .setSound(n.sound, n.audioStreamType) .setVibrate(n.vibrate) .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) @@ -86,6 +86,9 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso .setLargeIcon(b.mLargeIcon) .setNumber(b.mNumber) .setProgress(b.mProgressMax, b.mProgress, b.mProgressIndeterminate); + if (Build.VERSION.SDK_INT < 21) { + mBuilder.setSound(n.sound, n.audioStreamType); + } if (Build.VERSION.SDK_INT >= 16) { mBuilder.setSubText(b.mSubText) .setUsesChronometer(b.mUseChronometer) @@ -141,7 +144,8 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso mBuilder.setCategory(b.mCategory) .setColor(b.mColor) .setVisibility(b.mVisibility) - .setPublicVersion(b.mPublicVersion); + .setPublicVersion(b.mPublicVersion) + .setSound(n.sound, n.audioAttributes); for (String person: b.mPeople) { mBuilder.addPerson(person); @@ -169,6 +173,13 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso if (b.mColorizedSet) { mBuilder.setColorized(b.mColorized); } + + if (!TextUtils.isEmpty(b.mChannelId)) { + mBuilder.setSound(null) + .setDefaults(0) + .setLights(0, 0, 0) + .setVibrate(null); + } } } diff --git a/android/support/v4/app/NotificationManagerCompat.java b/android/support/v4/app/NotificationManagerCompat.java index 1a0f1bca..07fcb6c7 100644 --- a/android/support/v4/app/NotificationManagerCompat.java +++ b/android/support/v4/app/NotificationManagerCompat.java @@ -43,10 +43,10 @@ import android.util.Log; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -560,7 +560,7 @@ public final class NotificationManagerCompat { /** The service stub provided by onServiceConnected */ public INotificationSideChannel service; /** Queue of pending tasks to send to this listener service */ - public LinkedList taskQueue = new LinkedList(); + public ArrayDeque taskQueue = new ArrayDeque<>(); /** Number of retries attempted while connecting to this listener service */ public int retryCount = 0; diff --git a/android/support/v4/content/res/FontResourcesParserCompat.java b/android/support/v4/content/res/FontResourcesParserCompat.java index 8ad07d31..f597e68c 100644 --- a/android/support/v4/content/res/FontResourcesParserCompat.java +++ b/android/support/v4/content/res/FontResourcesParserCompat.java @@ -102,13 +102,17 @@ public class FontResourcesParserCompat { private final @NonNull String mFileName; private int mWeight; private boolean mItalic; + private String mVariationSettings; + private int mTtcIndex; private int mResourceId; public FontFileResourceEntry(@NonNull String fileName, int weight, boolean italic, - int resourceId) { + @Nullable String variationSettings, int ttcIndex, int resourceId) { mFileName = fileName; mWeight = weight; mItalic = italic; + mVariationSettings = variationSettings; + mTtcIndex = ttcIndex; mResourceId = resourceId; } @@ -124,6 +128,14 @@ public class FontResourcesParserCompat { return mItalic; } + public @Nullable String getVariationSettings() { + return mVariationSettings; + } + + public int getTtcIndex() { + return mTtcIndex; + } + public int getResourceId() { return mResourceId; } @@ -260,6 +272,15 @@ public class FontResourcesParserCompat { ? R.styleable.FontFamilyFont_fontStyle : R.styleable.FontFamilyFont_android_fontStyle; boolean isItalic = ITALIC == array.getInt(styleAttr, 0); + final int ttcIndexAttr = array.hasValue(R.styleable.FontFamilyFont_ttcIndex) + ? R.styleable.FontFamilyFont_ttcIndex + : R.styleable.FontFamilyFont_android_ttcIndex; + final int variationSettingsAttr = + array.hasValue(R.styleable.FontFamilyFont_fontVariationSettings) + ? R.styleable.FontFamilyFont_fontVariationSettings + : R.styleable.FontFamilyFont_android_fontVariationSettings; + String variationSettings = array.getString(variationSettingsAttr); + int ttcIndex = array.getInt(ttcIndexAttr, 0); final int resourceAttr = array.hasValue(R.styleable.FontFamilyFont_font) ? R.styleable.FontFamilyFont_font : R.styleable.FontFamilyFont_android_font; @@ -269,7 +290,8 @@ public class FontResourcesParserCompat { while (parser.next() != XmlPullParser.END_TAG) { skip(parser); } - return new FontFileResourceEntry(filename, weight, isItalic, resourceId); + return new FontFileResourceEntry(filename, weight, isItalic, variationSettings, ttcIndex, + resourceId); } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { diff --git a/android/support/v4/graphics/TypefaceCompat.java b/android/support/v4/graphics/TypefaceCompat.java index 734f1837..b763101a 100644 --- a/android/support/v4/graphics/TypefaceCompat.java +++ b/android/support/v4/graphics/TypefaceCompat.java @@ -32,6 +32,7 @@ import android.support.v4.content.res.FontResourcesParserCompat.FamilyResourceEn import android.support.v4.content.res.FontResourcesParserCompat.FontFamilyFilesResourceEntry; import android.support.v4.content.res.FontResourcesParserCompat.ProviderResourceEntry; import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.os.BuildCompat; import android.support.v4.provider.FontsContractCompat; import android.support.v4.provider.FontsContractCompat.FontInfo; import android.support.v4.util.LruCache; @@ -45,7 +46,7 @@ public class TypefaceCompat { private static final TypefaceCompatImpl sTypefaceCompatImpl; static { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (BuildCompat.isAtLeastP()) { sTypefaceCompatImpl = new TypefaceCompatApi28Impl(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sTypefaceCompatImpl = new TypefaceCompatApi26Impl(); diff --git a/android/support/v4/graphics/TypefaceCompatApi24Impl.java b/android/support/v4/graphics/TypefaceCompatApi24Impl.java index 89a6ec40..a8c19888 100644 --- a/android/support/v4/graphics/TypefaceCompatApi24Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi24Impl.java @@ -159,8 +159,7 @@ class TypefaceCompatApi24Impl extends TypefaceCompatBaseImpl { if (buffer == null) { return null; } - // TODO: support ttc index. - if (!addFontWeightStyle(family, buffer, 0, e.getWeight(), e.isItalic())) { + if (!addFontWeightStyle(family, buffer, e.getTtcIndex(), e.getWeight(), e.isItalic())) { return null; } } diff --git a/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/android/support/v4/graphics/TypefaceCompatApi26Impl.java index f23ac0d4..955284e3 100644 --- a/android/support/v4/graphics/TypefaceCompatApi26Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi26Impl.java @@ -61,6 +61,7 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { private static final String FREEZE_METHOD = "freeze"; private static final String ABORT_CREATION_METHOD = "abortCreation"; private static final int RESOLVE_BY_FONT_TABLE = -1; + private static final String DEFAULT_FAMILY = "sans-serif"; protected final Class mFontFamily; protected final Constructor mFontFamilyCtor; @@ -133,11 +134,11 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { * boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes) */ private boolean addFontFromAssetManager(Context context, Object family, String fileName, - int ttcIndex, int weight, int style) { + int ttcIndex, int weight, int style, @Nullable FontVariationAxis[] axes) { try { final Boolean result = (Boolean) mAddFontFromAssetManager.invoke(family, context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex, - weight, style, null /* axes */); + weight, style, axes); return result.booleanValue(); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); @@ -206,9 +207,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { } Object fontFamily = newFamily(); for (final FontFileResourceEntry fontFile : entry.getEntries()) { - // TODO: Add ttc and variation font support. (b/37853920) if (!addFontFromAssetManager(context, fontFamily, fontFile.getFileName(), - 0 /* ttcIndex */, fontFile.getWeight(), fontFile.isItalic() ? 1 : 0)) { + fontFile.getTtcIndex(), fontFile.getWeight(), fontFile.isItalic() ? 1 : 0, + FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) { abortCreation(fontFamily); return null; } @@ -233,6 +234,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { final ContentResolver resolver = context.getContentResolver(); try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(bestFont.getUri(), "r", cancellationSignal)) { + if (pfd == null) { + return null; + } return new Typeface.Builder(pfd.getFileDescriptor()) .setWeight(bestFont.getWeight()) .setItalic(bestFont.isItalic()) @@ -282,7 +286,7 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { Object fontFamily = newFamily(); if (!addFontFromAssetManager(context, fontFamily, path, 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */, - RESOLVE_BY_FONT_TABLE /* italic */)) { + RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) { abortCreation(fontFamily); return null; } diff --git a/android/support/v4/graphics/drawable/IconCompat.java b/android/support/v4/graphics/drawable/IconCompat.java index 359c96b3..dc226c1e 100644 --- a/android/support/v4/graphics/drawable/IconCompat.java +++ b/android/support/v4/graphics/drawable/IconCompat.java @@ -220,6 +220,7 @@ public class IconCompat { * @hide */ @RestrictTo(LIBRARY_GROUP) + @SuppressWarnings("deprecation") public void addToShortcutIntent(@NonNull Intent outIntent, @Nullable Drawable badge) { Bitmap icon; switch (mType) { diff --git a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java index 68f94768..6747d11d 100644 --- a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java +++ b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java @@ -16,7 +16,6 @@ package android.support.v4.hardware.fingerprint; -import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; @@ -58,7 +57,6 @@ public final class FingerprintManagerCompat { * * @return true if at least one fingerprint is enrolled, false otherwise */ - @TargetApi(23) @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints() { if (Build.VERSION.SDK_INT >= 23) { @@ -74,7 +72,6 @@ public final class FingerprintManagerCompat { * * @return true if hardware is present and functional, false otherwise. */ - @TargetApi(23) @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected() { if (Build.VERSION.SDK_INT >= 23) { @@ -99,7 +96,6 @@ public final class FingerprintManagerCompat { * @param callback an object to receive authentication events * @param handler an optional handler for events */ - @TargetApi(23) @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public void authenticate(@Nullable CryptoObject crypto, int flags, @Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback, diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java index 2509cd49..f24da1e1 100644 --- a/android/support/v4/media/session/MediaControllerCompat.java +++ b/android/support/v4/media/session/MediaControllerCompat.java @@ -1919,6 +1919,7 @@ public final class MediaControllerCompat { } } else { synchronized (mPendingCallbacks) { + callback.mHasExtraCallback = false; mPendingCallbacks.add(callback); } } @@ -1931,6 +1932,7 @@ public final class MediaControllerCompat { try { ExtraCallback extraCallback = mCallbackMap.remove(callback); if (extraCallback != null) { + callback.mHasExtraCallback = false; mExtraBinder.unregisterCallbackListener(extraCallback); } } catch (RemoteException e) { diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java index d7634b00..3b061257 100644 --- a/android/support/v4/media/session/PlaybackStateCompat.java +++ b/android/support/v4/media/session/PlaybackStateCompat.java @@ -24,6 +24,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.LongDef; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.text.TextUtils; @@ -45,7 +46,7 @@ public final class PlaybackStateCompat implements Parcelable { * @hide */ @RestrictTo(LIBRARY_GROUP) - @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, + @LongDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING, ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH, ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE, @@ -58,7 +59,7 @@ public final class PlaybackStateCompat implements Parcelable { * @hide */ @RestrictTo(LIBRARY_GROUP) - @IntDef({ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS, + @LongDef({ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_PLAY_PAUSE}) @Retention(RetentionPolicy.SOURCE) public @interface MediaKeyAction {} diff --git a/android/support/v4/provider/FontsContractCompat.java b/android/support/v4/provider/FontsContractCompat.java index 09261869..39acf686 100644 --- a/android/support/v4/provider/FontsContractCompat.java +++ b/android/support/v4/provider/FontsContractCompat.java @@ -274,7 +274,10 @@ public class FontsContractCompat { : new ReplyCallback() { @Override public void onReply(final TypefaceResult typeface) { - if (typeface.mResult == FontFamilyResult.STATUS_OK) { + if (typeface == null) { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, handler); + } else if (typeface.mResult == FontFamilyResult.STATUS_OK) { fontCallback.callbackSuccessAsync(typeface.mTypeface, handler); } else { fontCallback.callbackFailAsync(typeface.mResult, handler); diff --git a/android/support/v4/view/ViewConfigurationCompat.java b/android/support/v4/view/ViewConfigurationCompat.java index f14b8060..60d37a9f 100644 --- a/android/support/v4/view/ViewConfigurationCompat.java +++ b/android/support/v4/view/ViewConfigurationCompat.java @@ -27,10 +27,7 @@ import java.lang.reflect.Method; /** * Helper for accessing features in {@link ViewConfiguration}. - * - * @deprecated Use {@link ViewConfiguration} directly. */ -@Deprecated public final class ViewConfigurationCompat { private static final String TAG = "ViewConfigCompat"; diff --git a/android/support/v4/widget/DrawerLayout.java b/android/support/v4/widget/DrawerLayout.java index a73e1f10..aa2077d6 100644 --- a/android/support/v4/widget/DrawerLayout.java +++ b/android/support/v4/widget/DrawerLayout.java @@ -44,7 +44,6 @@ import android.support.v4.view.AbsSavedState; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; -import android.support.v4.view.ViewGroupCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import android.util.AttributeSet; @@ -331,7 +330,7 @@ public class DrawerLayout extends ViewGroup { ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); - ViewGroupCompat.setMotionEventSplittingEnabled(this, false); + setMotionEventSplittingEnabled(false); if (ViewCompat.getFitsSystemWindows(this)) { if (Build.VERSION.SDK_INT >= 21) { setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { diff --git a/android/support/v4/widget/NestedScrollView.java b/android/support/v4/widget/NestedScrollView.java index 73ff0848..6fe19289 100644 --- a/android/support/v4/widget/NestedScrollView.java +++ b/android/support/v4/widget/NestedScrollView.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -1820,10 +1821,20 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare final int scrollY = getScrollY(); if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); - final int width = getWidth() - getPaddingLeft() - getPaddingRight(); - - canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); - mEdgeGlowTop.setSize(width, getHeight()); + int width = getWidth(); + int height = getHeight(); + int xTranslation = 0; + int yTranslation = Math.min(0, scrollY); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { + width -= getPaddingLeft() + getPaddingRight(); + xTranslation += getPaddingLeft(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { + height -= getPaddingTop() + getPaddingBottom(); + yTranslation += getPaddingTop(); + } + canvas.translate(xTranslation, yTranslation); + mEdgeGlowTop.setSize(width, height); if (mEdgeGlowTop.draw(canvas)) { ViewCompat.postInvalidateOnAnimation(this); } @@ -1831,11 +1842,19 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare } if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); - final int width = getWidth() - getPaddingLeft() - getPaddingRight(); - final int height = getHeight(); - - canvas.translate(-width + getPaddingLeft(), - Math.max(getScrollRange(), scrollY) + height); + int width = getWidth(); + int height = getHeight(); + int xTranslation = 0; + int yTranslation = Math.max(getScrollRange(), scrollY) + height; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { + width -= getPaddingLeft() + getPaddingRight(); + xTranslation += getPaddingLeft(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { + height -= getPaddingTop() + getPaddingBottom(); + yTranslation -= getPaddingBottom(); + } + canvas.translate(xTranslation - width, yTranslation); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); if (mEdgeGlowBottom.draw(canvas)) { diff --git a/android/support/v7/app/AlertController.java b/android/support/v7/app/AlertController.java index 5ff4537d..01bc4499 100644 --- a/android/support/v7/app/AlertController.java +++ b/android/support/v7/app/AlertController.java @@ -65,6 +65,7 @@ class AlertController { private final Context mContext; final AppCompatDialog mDialog; private final Window mWindow; + private final int mButtonIconDimen; private CharSequence mTitle; private CharSequence mMessage; @@ -82,14 +83,17 @@ class AlertController { Button mButtonPositive; private CharSequence mButtonPositiveText; Message mButtonPositiveMessage; + private Drawable mButtonPositiveIcon; Button mButtonNegative; private CharSequence mButtonNegativeText; Message mButtonNegativeMessage; + private Drawable mButtonNegativeIcon; Button mButtonNeutral; private CharSequence mButtonNeutralText; Message mButtonNeutralMessage; + private Drawable mButtonNeutralIcon; NestedScrollView mScrollView; @@ -192,6 +196,7 @@ class AlertController { .getResourceId(R.styleable.AlertDialog_singleChoiceItemLayout, 0); mListItemLayout = a.getResourceId(R.styleable.AlertDialog_listItemLayout, 0); mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true); + mButtonIconDimen = a.getDimensionPixelSize(R.styleable.AlertDialog_buttonIconDimen, 0); a.recycle(); @@ -298,8 +303,8 @@ class AlertController { } /** - * Sets a click listener or a message to be sent when the button is clicked. - * You only need to pass one of {@code listener} or {@code msg}. + * Sets an icon, a click listener or a message to be sent when the button is clicked. + * You only need to pass one of {@code icon}, {@code listener} or {@code msg}. * * @param whichButton Which button, can be one of * {@link DialogInterface#BUTTON_POSITIVE}, @@ -308,9 +313,11 @@ class AlertController { * @param text The text to display in positive button. * @param listener The {@link DialogInterface.OnClickListener} to use. * @param msg The {@link Message} to be sent when clicked. + * @param icon The (@link Drawable) to be used as an icon for the button. + * */ public void setButton(int whichButton, CharSequence text, - DialogInterface.OnClickListener listener, Message msg) { + DialogInterface.OnClickListener listener, Message msg, Drawable icon) { if (msg == null && listener != null) { msg = mHandler.obtainMessage(whichButton, listener); @@ -321,16 +328,19 @@ class AlertController { case DialogInterface.BUTTON_POSITIVE: mButtonPositiveText = text; mButtonPositiveMessage = msg; + mButtonPositiveIcon = icon; break; case DialogInterface.BUTTON_NEGATIVE: mButtonNegativeText = text; mButtonNegativeMessage = msg; + mButtonNegativeIcon = icon; break; case DialogInterface.BUTTON_NEUTRAL: mButtonNeutralText = text; mButtonNeutralMessage = msg; + mButtonNeutralIcon = icon; break; default: @@ -752,35 +762,45 @@ class AlertController { mButtonPositive = (Button) buttonPanel.findViewById(android.R.id.button1); mButtonPositive.setOnClickListener(mButtonHandler); - if (TextUtils.isEmpty(mButtonPositiveText)) { + if (TextUtils.isEmpty(mButtonPositiveText) && mButtonPositiveIcon == null) { mButtonPositive.setVisibility(View.GONE); } else { mButtonPositive.setText(mButtonPositiveText); + if (mButtonPositiveIcon != null) { + mButtonPositiveIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen); + mButtonPositive.setCompoundDrawables(mButtonPositiveIcon, null, null, null); + } mButtonPositive.setVisibility(View.VISIBLE); whichButtons = whichButtons | BIT_BUTTON_POSITIVE; } - mButtonNegative = (Button) buttonPanel.findViewById(android.R.id.button2); + mButtonNegative = buttonPanel.findViewById(android.R.id.button2); mButtonNegative.setOnClickListener(mButtonHandler); - if (TextUtils.isEmpty(mButtonNegativeText)) { + if (TextUtils.isEmpty(mButtonNegativeText) && mButtonNegativeIcon == null) { mButtonNegative.setVisibility(View.GONE); } else { mButtonNegative.setText(mButtonNegativeText); + if (mButtonNegativeIcon != null) { + mButtonNegativeIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen); + mButtonNegative.setCompoundDrawables(mButtonNegativeIcon, null, null, null); + } mButtonNegative.setVisibility(View.VISIBLE); - whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; } mButtonNeutral = (Button) buttonPanel.findViewById(android.R.id.button3); mButtonNeutral.setOnClickListener(mButtonHandler); - if (TextUtils.isEmpty(mButtonNeutralText)) { + if (TextUtils.isEmpty(mButtonNeutralText) && mButtonNeutralIcon == null) { mButtonNeutral.setVisibility(View.GONE); } else { mButtonNeutral.setText(mButtonNeutralText); + if (mButtonPositiveIcon != null) { + mButtonPositiveIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen); + mButtonPositive.setCompoundDrawables(mButtonPositiveIcon, null, null, null); + } mButtonNeutral.setVisibility(View.VISIBLE); - whichButtons = whichButtons | BIT_BUTTON_NEUTRAL; } @@ -852,10 +872,13 @@ class AlertController { public View mCustomTitleView; public CharSequence mMessage; public CharSequence mPositiveButtonText; + public Drawable mPositiveButtonIcon; public DialogInterface.OnClickListener mPositiveButtonListener; public CharSequence mNegativeButtonText; + public Drawable mNegativeButtonIcon; public DialogInterface.OnClickListener mNegativeButtonListener; public CharSequence mNeutralButtonText; + public Drawable mNeutralButtonIcon; public DialogInterface.OnClickListener mNeutralButtonListener; public boolean mCancelable; public DialogInterface.OnCancelListener mOnCancelListener; @@ -923,17 +946,17 @@ class AlertController { if (mMessage != null) { dialog.setMessage(mMessage); } - if (mPositiveButtonText != null) { + if (mPositiveButtonText != null || mPositiveButtonIcon != null) { dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText, - mPositiveButtonListener, null); + mPositiveButtonListener, null, mPositiveButtonIcon); } - if (mNegativeButtonText != null) { + if (mNegativeButtonText != null || mNegativeButtonIcon != null) { dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText, - mNegativeButtonListener, null); + mNegativeButtonListener, null, mNegativeButtonIcon); } - if (mNeutralButtonText != null) { + if (mNeutralButtonText != null || mNeutralButtonIcon != null) { dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText, - mNeutralButtonListener, null); + mNeutralButtonListener, null, mNeutralButtonIcon); } // For a list, the client can either supply an array of items or an // adapter or a cursor diff --git a/android/support/v7/app/AlertDialog.java b/android/support/v7/app/AlertDialog.java index 4b87dcc0..1712f20b 100644 --- a/android/support/v7/app/AlertDialog.java +++ b/android/support/v7/app/AlertDialog.java @@ -207,7 +207,7 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface { * @param msg The {@link Message} to be sent when clicked. */ public void setButton(int whichButton, CharSequence text, Message msg) { - mAlert.setButton(whichButton, text, null, msg); + mAlert.setButton(whichButton, text, null, msg, null); } /** @@ -222,7 +222,25 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface { * @param listener The {@link DialogInterface.OnClickListener} to use. */ public void setButton(int whichButton, CharSequence text, OnClickListener listener) { - mAlert.setButton(whichButton, text, listener, null); + mAlert.setButton(whichButton, text, listener, null, null); + } + + /** + * Sets an icon to be displayed along with the button text and a listener to be invoked when + * the positive button of the dialog is pressed. This method has no effect if called after + * {@link #show()}. + * + * @param whichButton Which button to set the listener on, can be one of + * {@link DialogInterface#BUTTON_POSITIVE}, + * {@link DialogInterface#BUTTON_NEGATIVE}, or + * {@link DialogInterface#BUTTON_NEUTRAL} + * @param text The text to display in positive button. + * @param listener The {@link DialogInterface.OnClickListener} to use. + * @param icon The {@link Drawable} to be set as an icon for the button. + */ + public void setButton(int whichButton, CharSequence text, Drawable icon, + OnClickListener listener) { + mAlert.setButton(whichButton, text, listener, null, icon); } /** @@ -469,6 +487,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface { return this; } + /** + * Set an icon to be displayed for the positive button. + * @param icon The icon to be displayed + * @return This Builder object to allow for chaining of calls to set methods + */ + public Builder setPositiveButtonIcon(Drawable icon) { + P.mPositiveButtonIcon = icon; + return this; + } + /** * Set a listener to be invoked when the negative button of the dialog is pressed. * @param textId The resource id of the text to display in the negative button @@ -495,6 +523,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface { return this; } + /** + * Set an icon to be displayed for the negative button. + * @param icon The icon to be displayed + * @return This Builder object to allow for chaining of calls to set methods + */ + public Builder setNegativeButtonIcon(Drawable icon) { + P.mNegativeButtonIcon = icon; + return this; + } + /** * Set a listener to be invoked when the neutral button of the dialog is pressed. * @param textId The resource id of the text to display in the neutral button @@ -521,6 +559,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface { return this; } + /** + * Set an icon to be displayed for the neutral button. + * @param icon The icon to be displayed + * @return This Builder object to allow for chaining of calls to set methods + */ + public Builder setNeutralButtonIcon(Drawable icon) { + P.mNeutralButtonIcon = icon; + return this; + } + /** * Sets whether the dialog is cancelable or not. Default is true. * diff --git a/android/support/v7/graphics/BucketTests.java b/android/support/v7/graphics/BucketTests.java new file mode 100644 index 00000000..ca8e5085 --- /dev/null +++ b/android/support/v7/graphics/BucketTests.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.graphics; + +import static android.support.v7.graphics.TestUtils.assertCloseColors; +import static android.support.v7.graphics.TestUtils.loadSampleBitmap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public class BucketTests { + + @Test + @SmallTest + public void testSourceBitmapNotRecycled() { + final Bitmap sample = loadSampleBitmap(); + + Palette.from(sample).generate(); + assertFalse(sample.isRecycled()); + } + + @Test(expected = UnsupportedOperationException.class) + @SmallTest + public void testSwatchesUnmodifiable() { + Palette p = Palette.from(loadSampleBitmap()).generate(); + p.getSwatches().remove(0); + } + + @Test + @SmallTest + public void testSwatchesBuilder() { + ArrayList swatches = new ArrayList<>(); + swatches.add(new Palette.Swatch(Color.BLACK, 40)); + swatches.add(new Palette.Swatch(Color.GREEN, 60)); + swatches.add(new Palette.Swatch(Color.BLUE, 10)); + + Palette p = Palette.from(swatches); + + assertEquals(swatches, p.getSwatches()); + } + + @Test + @SmallTest + public void testRegionWhole() { + final Bitmap sample = loadSampleBitmap(); + + Palette.Builder b = new Palette.Builder(sample); + b.setRegion(0, 0, sample.getWidth(), sample.getHeight()); + b.generate(); + } + + @Test + @SmallTest + public void testRegionUpperLeft() { + final Bitmap sample = loadSampleBitmap(); + + Palette.Builder b = new Palette.Builder(sample); + b.setRegion(0, 0, sample.getWidth() / 2, sample.getHeight() / 2); + b.generate(); + } + + @Test + @SmallTest + public void testRegionBottomRight() { + final Bitmap sample = loadSampleBitmap(); + + Palette.Builder b = new Palette.Builder(sample); + b.setRegion(sample.getWidth() / 2, sample.getHeight() / 2, + sample.getWidth(), sample.getHeight()); + b.generate(); + } + + @Test + @SmallTest + public void testOnePixelTallBitmap() { + final Bitmap bitmap = Bitmap.createBitmap(1000, 1, Bitmap.Config.ARGB_8888); + + Palette.Builder b = new Palette.Builder(bitmap); + b.generate(); + } + + @Test + @SmallTest + public void testOnePixelWideBitmap() { + final Bitmap bitmap = Bitmap.createBitmap(1, 1000, Bitmap.Config.ARGB_8888); + + Palette.Builder b = new Palette.Builder(bitmap); + b.generate(); + } + + @Test + @SmallTest + public void testBlueBitmapReturnsBlueSwatch() { + final Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.BLUE); + + final Palette palette = Palette.from(bitmap).generate(); + + assertEquals(1, palette.getSwatches().size()); + + final Palette.Swatch swatch = palette.getSwatches().get(0); + assertCloseColors(Color.BLUE, swatch.getRgb()); + } + + @Test + @SmallTest + public void testBlueBitmapWithRegionReturnsBlueSwatch() { + final Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.BLUE); + + final Palette palette = Palette.from(bitmap) + .setRegion(0, bitmap.getHeight() / 2, bitmap.getWidth(), bitmap.getHeight()) + .generate(); + + assertEquals(1, palette.getSwatches().size()); + + final Palette.Swatch swatch = palette.getSwatches().get(0); + assertCloseColors(Color.BLUE, swatch.getRgb()); + } + + @Test + @SmallTest + public void testDominantSwatch() { + final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + // First fill the canvas with blue + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.BLUE); + + final Paint paint = new Paint(); + // Now we'll draw the top 10px tall rect with green + paint.setColor(Color.GREEN); + canvas.drawRect(0, 0, 100, 10, paint); + + // Now we'll draw the next 20px tall rect with red + paint.setColor(Color.RED); + canvas.drawRect(0, 11, 100, 30, paint); + + // Now generate a palette from the bitmap + final Palette palette = Palette.from(bitmap).generate(); + + // First assert that there are 3 swatches + assertEquals(3, palette.getSwatches().size()); + + // Now assert that the dominant swatch is blue + final Palette.Swatch swatch = palette.getDominantSwatch(); + assertNotNull(swatch); + assertCloseColors(Color.BLUE, swatch.getRgb()); + } + +} diff --git a/android/support/v7/graphics/ConsistencyTest.java b/android/support/v7/graphics/ConsistencyTest.java new file mode 100644 index 00000000..d9ac12ee --- /dev/null +++ b/android/support/v7/graphics/ConsistencyTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.graphics; + +import static android.support.v7.graphics.TestUtils.loadSampleBitmap; + +import static org.junit.Assert.assertEquals; + +import android.graphics.Bitmap; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ConsistencyTest { + + private static final int NUMBER_TRIALS = 10; + + @Test + @MediumTest + public void testConsistency() { + Palette lastPalette = null; + final Bitmap bitmap = loadSampleBitmap(); + + for (int i = 0; i < NUMBER_TRIALS; i++) { + Palette newPalette = Palette.from(bitmap).generate(); + if (lastPalette != null) { + assetPalettesEqual(lastPalette, newPalette); + } + lastPalette = newPalette; + } + } + + private static void assetPalettesEqual(Palette p1, Palette p2) { + assertEquals(p1.getVibrantSwatch(), p2.getVibrantSwatch()); + assertEquals(p1.getLightVibrantSwatch(), p2.getLightVibrantSwatch()); + assertEquals(p1.getDarkVibrantSwatch(), p2.getDarkVibrantSwatch()); + assertEquals(p1.getMutedSwatch(), p2.getMutedSwatch()); + assertEquals(p1.getLightMutedSwatch(), p2.getLightMutedSwatch()); + assertEquals(p1.getDarkMutedSwatch(), p2.getDarkMutedSwatch()); + } +} diff --git a/android/support/v7/graphics/MaxColorsTest.java b/android/support/v7/graphics/MaxColorsTest.java new file mode 100644 index 00000000..fbcf6ae1 --- /dev/null +++ b/android/support/v7/graphics/MaxColorsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.graphics; + +import static android.support.v7.graphics.TestUtils.loadSampleBitmap; + +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MaxColorsTest { + + @Test + @SmallTest + public void testMaxColorCount32() { + testMaxColorCount(32); + } + + @Test + @SmallTest + public void testMaxColorCount1() { + testMaxColorCount(1); + } + + @Test + @SmallTest + public void testMaxColorCount15() { + testMaxColorCount(15); + } + + private void testMaxColorCount(int colorCount) { + Palette newPalette = Palette.from(loadSampleBitmap()) + .maximumColorCount(colorCount) + .generate(); + assertTrue(newPalette.getSwatches().size() <= colorCount); + } +} diff --git a/android/support/v7/graphics/SwatchTests.java b/android/support/v7/graphics/SwatchTests.java new file mode 100644 index 00000000..efbcda40 --- /dev/null +++ b/android/support/v7/graphics/SwatchTests.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.graphics; + +import static android.support.v4.graphics.ColorUtils.HSLToColor; +import static android.support.v4.graphics.ColorUtils.calculateContrast; +import static android.support.v7.graphics.TestUtils.loadSampleBitmap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Color; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SwatchTests { + + private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; + private static final float MIN_CONTRAST_BODY_TEXT = 4.5f; + + @Test + @SmallTest + public void testTextColorContrasts() { + final Palette p = Palette.from(loadSampleBitmap()).generate(); + + for (Palette.Swatch swatch : p.getSwatches()) { + testSwatchTextColorContrasts(swatch); + } + } + + @Test + @SmallTest + public void testHslNotNull() { + final Palette p = Palette.from(loadSampleBitmap()).generate(); + + for (Palette.Swatch swatch : p.getSwatches()) { + assertNotNull(swatch.getHsl()); + } + } + + @Test + @SmallTest + public void testHslIsRgb() { + final Palette p = Palette.from(loadSampleBitmap()).generate(); + + for (Palette.Swatch swatch : p.getSwatches()) { + assertEquals(HSLToColor(swatch.getHsl()), swatch.getRgb()); + } + } + + private void testSwatchTextColorContrasts(Palette.Swatch swatch) { + final int bodyTextColor = swatch.getBodyTextColor(); + assertTrue(calculateContrast(bodyTextColor, swatch.getRgb()) >= MIN_CONTRAST_BODY_TEXT); + + final int titleTextColor = swatch.getTitleTextColor(); + assertTrue(calculateContrast(titleTextColor, swatch.getRgb()) >= MIN_CONTRAST_TITLE_TEXT); + } + + @Test + @SmallTest + public void testEqualsWhenSame() { + Palette.Swatch swatch1 = new Palette.Swatch(Color.WHITE, 50); + Palette.Swatch swatch2 = new Palette.Swatch(Color.WHITE, 50); + assertEquals(swatch1, swatch2); + } + + @Test + @SmallTest + public void testEqualsWhenColorDifferent() { + Palette.Swatch swatch1 = new Palette.Swatch(Color.BLACK, 50); + Palette.Swatch swatch2 = new Palette.Swatch(Color.WHITE, 50); + assertFalse(swatch1.equals(swatch2)); + } + + @Test + @SmallTest + public void testEqualsWhenPopulationDifferent() { + Palette.Swatch swatch1 = new Palette.Swatch(Color.BLACK, 50); + Palette.Swatch swatch2 = new Palette.Swatch(Color.BLACK, 100); + assertFalse(swatch1.equals(swatch2)); + } + + @Test + @SmallTest + public void testEqualsWhenDifferent() { + Palette.Swatch swatch1 = new Palette.Swatch(Color.BLUE, 50); + Palette.Swatch swatch2 = new Palette.Swatch(Color.BLACK, 100); + assertFalse(swatch1.equals(swatch2)); + } +} diff --git a/android/support/v7/graphics/TestUtils.java b/android/support/v7/graphics/TestUtils.java new file mode 100644 index 00000000..8de70c52 --- /dev/null +++ b/android/support/v7/graphics/TestUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.graphics; + +import static org.junit.Assert.assertEquals; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.support.test.InstrumentationRegistry; +import android.support.v7.palette.test.R; + +class TestUtils { + + static Bitmap loadSampleBitmap() { + return BitmapFactory.decodeResource( + InstrumentationRegistry.getContext().getResources(), + R.drawable.photo); + } + + static void assertCloseColors(int expected, int actual) { + assertEquals(Color.red(expected), Color.red(actual), 8); + assertEquals(Color.green(expected), Color.green(actual), 8); + assertEquals(Color.blue(expected), Color.blue(actual), 8); + } + +} diff --git a/android/support/v7/media/MediaRouter.java b/android/support/v7/media/MediaRouter.java index cf6fc1f1..cc372ec9 100644 --- a/android/support/v7/media/MediaRouter.java +++ b/android/support/v7/media/MediaRouter.java @@ -2560,12 +2560,16 @@ public final class MediaRouter { // TODO: Remove the following logging when no longer needed. if (sGlobal == null || (mBluetoothRoute != null && route.isDefault())) { final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); // callStack[3] is the caller of this method. for (int i = 3; i < callStack.length; i++) { StackTraceElement caller = callStack[i]; - sb.append(caller.getClassName() + "." + caller.getMethodName() - + ":" + caller.getLineNumber()).append(" "); + sb.append(caller.getClassName()) + .append(".") + .append(caller.getMethodName()) + .append(":") + .append(caller.getLineNumber()) + .append(" "); } if (sGlobal == null) { Log.w(TAG, "setSelectedRouteInternal is called while sGlobal is null: pkgName=" diff --git a/android/support/v7/util/SortedListTest.java b/android/support/v7/util/SortedListTest.java index f8bc496c..e628de11 100644 --- a/android/support/v7/util/SortedListTest.java +++ b/android/support/v7/util/SortedListTest.java @@ -16,11 +16,18 @@ package android.support.v7.util; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.support.annotation.Nullable; import android.support.test.filters.SmallTest; -import junit.framework.TestCase; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,13 +46,13 @@ import java.util.concurrent.atomic.AtomicInteger; @RunWith(JUnit4.class) @SmallTest -public class SortedListTest extends TestCase { +public class SortedListTest { SortedList mList; - List mAdditions = new ArrayList(); - List mRemovals = new ArrayList(); - List mMoves = new ArrayList(); - List mUpdates = new ArrayList(); + List mAdditions = new ArrayList<>(); + List mRemovals = new ArrayList<>(); + List mMoves = new ArrayList<>(); + List mUpdates = new ArrayList<>(); private boolean mPayloadChanges = false; List mPayloadUpdates = new ArrayList<>(); Queue mCallbackRunnables; @@ -69,11 +76,8 @@ public class SortedListTest extends TestCase { public abstract void onChanged(int position, int count); } - @Override @Before public void setUp() throws Exception { - super.setUp(); - mCallback = new SortedList.Callback() { @Override public int compare(Item o1, Item o2) { diff --git a/android/support/v7/view/menu/CascadingMenuPopup.java b/android/support/v7/view/menu/CascadingMenuPopup.java index 564bbfca..834f8544 100644 --- a/android/support/v7/view/menu/CascadingMenuPopup.java +++ b/android/support/v7/view/menu/CascadingMenuPopup.java @@ -54,7 +54,6 @@ import android.widget.TextView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; /** @@ -85,7 +84,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey final Handler mSubMenuHoverHandler; /** List of menus that were added before this popup was shown. */ - private final List mPendingMenus = new LinkedList<>(); + private final List mPendingMenus = new ArrayList<>(); /** * List of open menus. The first item is the root menu and each diff --git a/android/support/v7/widget/ActionMenuView.java b/android/support/v7/widget/ActionMenuView.java index 76e06da6..14723a0c 100644 --- a/android/support/v7/widget/ActionMenuView.java +++ b/android/support/v7/widget/ActionMenuView.java @@ -268,10 +268,10 @@ public class ActionMenuView extends LinearLayoutCompat implements MenuBuilder.It // Mark indices of children that can receive an extra cell. if (lp.cellsUsed < minCells) { minCells = lp.cellsUsed; - minCellsAt = 1 << i; + minCellsAt = 1L << i; minCellsItemCount = 1; } else if (lp.cellsUsed == minCells) { - minCellsAt |= 1 << i; + minCellsAt |= 1L << i; minCellsItemCount++; } } diff --git a/android/support/v7/widget/AdapterHelperTest.java b/android/support/v7/widget/AdapterHelperTest.java index e0dbde50..a76f40e2 100644 --- a/android/support/v7/widget/AdapterHelperTest.java +++ b/android/support/v7/widget/AdapterHelperTest.java @@ -18,19 +18,21 @@ package android.support.v7.widget; import static android.support.v7.widget.RecyclerView.ViewHolder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + import android.support.test.filters.SmallTest; -import android.test.AndroidTestCase; -import android.util.Log; import android.view.View; -import android.widget.TextView; - -import junit.framework.AssertionFailedError; -import junit.framework.TestResult; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.LinkedList; @@ -41,7 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger; @RunWith(JUnit4.class) @SmallTest -public class AdapterHelperTest extends AndroidTestCase { +public class AdapterHelperTest { private static final boolean DEBUG = false; @@ -49,35 +51,40 @@ public class AdapterHelperTest extends AndroidTestCase { private static final String TAG = "AHT"; - List mViewHolders; + private List mViewHolders; - AdapterHelper mAdapterHelper; + private AdapterHelper mAdapterHelper; - List mFirstPassUpdates, mSecondPassUpdates; + private List mFirstPassUpdates, mSecondPassUpdates; TestAdapter mTestAdapter; - TestAdapter mPreProcessClone; // we clone adapter pre-process to run operations to see result + private TestAdapter mPreProcessClone; + // we clone adapter pre-process to run operations to see result private List mPreLayoutItems; private StringBuilder mLog = new StringBuilder(); - @Override - public void run(TestResult result) { - super.run(result); - if (!result.wasSuccessful()) { - result.addFailure(this, new AssertionFailedError(mLog.toString())); + @Rule + public TestWatcher reportErrorLog = new TestWatcher() { + @Override + protected void failed(Throwable e, Description description) { + System.out.println(mLog.toString()); } - } + + @Override + protected void succeeded(Description description) { + } + }; @Before public void cleanState() { mLog.setLength(0); - mPreLayoutItems = new ArrayList(); - mViewHolders = new ArrayList(); - mFirstPassUpdates = new ArrayList(); - mSecondPassUpdates = new ArrayList(); + mPreLayoutItems = new ArrayList<>(); + mViewHolders = new ArrayList<>(); + mFirstPassUpdates = new ArrayList<>(); + mSecondPassUpdates = new ArrayList<>(); mPreProcessClone = null; mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() { @Override @@ -189,7 +196,7 @@ public class AdapterHelperTest extends AndroidTestCase { if (mCollectLogs) { mLog.append(msg).append("\n"); } else { - Log.d(TAG, msg); + System.out.println(TAG + ":" + msg); } } @@ -205,8 +212,7 @@ public class AdapterHelperTest extends AndroidTestCase { } private void addViewHolder(int position) { - MockViewHolder viewHolder = new MockViewHolder( - new TextView(getContext())); + MockViewHolder viewHolder = new MockViewHolder(); viewHolder.mPosition = position; viewHolder.mItem = mTestAdapter.mItems.get(position); mViewHolders.add(viewHolder); @@ -502,7 +508,7 @@ public class AdapterHelperTest extends AndroidTestCase { } @Test - public void testScenario18() throws InterruptedException { + public void testScenario18() { setupBasic(10, 1, 4); add(2, 11); rm(16, 1); @@ -622,7 +628,7 @@ public class AdapterHelperTest extends AndroidTestCase { } @Test - public void testScenerio30() throws InterruptedException { + public void testScenerio30() { mCollectLogs = true; setupBasic(10, 3, 1); rm(3, 2); @@ -631,7 +637,7 @@ public class AdapterHelperTest extends AndroidTestCase { } @Test - public void testScenerio31() throws InterruptedException { + public void testScenerio31() { mCollectLogs = true; setupBasic(10, 3, 1); rm(3, 1); @@ -844,7 +850,7 @@ public class AdapterHelperTest extends AndroidTestCase { Random random = new Random(System.nanoTime()); for (int i = 0; i < 100; i++) { try { - Log.d(TAG, "running random test " + i); + log("running random test " + i); randomTest(random, Math.max(40, 10 + nextInt(random, i))); } catch (Throwable t) { throw new Throwable("failure at random test " + i + "\n" + t.getMessage() @@ -853,7 +859,7 @@ public class AdapterHelperTest extends AndroidTestCase { } } - public void randomTest(Random random, int opCount) { + private void randomTest(Random random, int opCount) { cleanState(); if (DEBUG) { log("randomTest"); @@ -907,14 +913,14 @@ public class AdapterHelperTest extends AndroidTestCase { preProcess(); } - int nextInt(Random random, int n) { + private int nextInt(Random random, int n) { if (n == 0) { return 0; } return random.nextInt(n); } - public void assertOps(List actual, + private void assertOps(List actual, AdapterHelper.UpdateOp... expected) { assertEquals(expected.length, actual.size()); for (int i = 0; i < expected.length; i++) { @@ -922,12 +928,12 @@ public class AdapterHelperTest extends AndroidTestCase { } } - void assertDispatch(int firstPass, int secondPass) { + private void assertDispatch(int firstPass, int secondPass) { assertEquals(firstPass, mFirstPassUpdates.size()); assertEquals(secondPass, mSecondPassUpdates.size()); } - void preProcess() { + private void preProcess() { for (MockViewHolder vh : mViewHolders) { final int ind = mTestAdapter.mItems.indexOf(vh.mItem); assertEquals("actual adapter position should match", ind, @@ -975,23 +981,20 @@ public class AdapterHelperTest extends AndroidTestCase { assertEquals(0, a2.mPendingAdded.size()); } - AdapterHelper.UpdateOp op(int cmd, int start, int count) { + private AdapterHelper.UpdateOp op(int cmd, int start, int count) { return new AdapterHelper.UpdateOp(cmd, start, count, null); } - AdapterHelper.UpdateOp op(int cmd, int start, int count, Object payload) { + private AdapterHelper.UpdateOp op(int cmd, int start, int count, Object payload) { return new AdapterHelper.UpdateOp(cmd, start, count, payload); } - AdapterHelper.UpdateOp addOp(int start, int count) { - return op(AdapterHelper.UpdateOp.ADD, start, count); - } - - AdapterHelper.UpdateOp rmOp(int start, int count) { + private AdapterHelper.UpdateOp rmOp(int start, int count) { return op(AdapterHelper.UpdateOp.REMOVE, start, count); } - AdapterHelper.UpdateOp upOp(int start, int count, Object payload) { + private AdapterHelper.UpdateOp upOp(int start, int count, @SuppressWarnings + ("SameParameterValue") Object payload) { return op(AdapterHelper.UpdateOp.UPDATE, start, count, payload); } @@ -1002,7 +1005,7 @@ public class AdapterHelperTest extends AndroidTestCase { mTestAdapter.add(start, count); } - boolean isItemLaidOut(int pos) { + private boolean isItemLaidOut(int pos) { for (ViewHolder viewHolder : mViewHolders) { if (viewHolder.mOldPosition == pos) { return true; @@ -1018,7 +1021,7 @@ public class AdapterHelperTest extends AndroidTestCase { mTestAdapter.move(from, to); } - void rm(int start, int count) { + private void rm(int start, int count) { if (DEBUG) { log("rm(" + start + "," + count + ");"); } @@ -1054,9 +1057,9 @@ public class AdapterHelperTest extends AndroidTestCase { Queue mPendingAdded; public TestAdapter(int initialCount, AdapterHelper container) { - mItems = new ArrayList(); + mItems = new ArrayList<>(); mAdapterHelper = container; - mPendingAdded = new LinkedList(); + mPendingAdded = new LinkedList<>(); for (int i = 0; i < initialCount; i++) { mItems.add(new Item()); } @@ -1102,15 +1105,13 @@ public class AdapterHelperTest extends AndroidTestCase { )); } - protected TestAdapter createCopy() { + TestAdapter createCopy() { TestAdapter adapter = new TestAdapter(0, mAdapterHelper); - for (Item item : mItems) { - adapter.mItems.add(item); - } + adapter.mItems.addAll(mItems); return adapter; } - public void applyOps(List updates, + void applyOps(List updates, TestAdapter dataSource) { for (AdapterHelper.UpdateOp op : updates) { switch (op.cmd) { @@ -1140,20 +1141,16 @@ public class AdapterHelperTest extends AndroidTestCase { return mPendingAdded.remove(); } - public void createFakeItemAt(int fakeAddedItemIndex) { - Item fakeItem = new Item(); - ((LinkedList) mPendingAdded).add(fakeAddedItemIndex, fakeItem); - } - public static class Item { private static AtomicInteger itemCounter = new AtomicInteger(); + @SuppressWarnings("unused") private final int id; private int mVersionCount = 0; - private ArrayList mPayloads = new ArrayList(); + private ArrayList mPayloads = new ArrayList<>(); public Item() { id = itemCounter.incrementAndGet(); @@ -1164,26 +1161,23 @@ public class AdapterHelperTest extends AndroidTestCase { mVersionCount++; } - public void handleUpdate(Object payload) { + void handleUpdate(Object payload) { assertSame(payload, mPayloads.get(0)); mPayloads.remove(0); mVersionCount--; } - public int getUpdateCount() { + int getUpdateCount() { return mVersionCount; } } } - void waitForDebugger() { - android.os.Debug.waitForDebugger(); - } - static class MockViewHolder extends RecyclerView.ViewHolder { - public Object mItem; - public MockViewHolder(View itemView) { - super(itemView); + TestAdapter.Item mItem; + + MockViewHolder() { + super(Mockito.mock(View.class)); } @Override diff --git a/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java b/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java index e82e4694..6b9d05a7 100644 --- a/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java +++ b/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java @@ -18,7 +18,6 @@ package android.support.v7.widget; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -26,6 +25,7 @@ import android.graphics.RectF; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.v4.widget.TextViewCompat; import android.support.v7.appcompat.R; @@ -45,8 +45,8 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Hashtable; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; /** * Utility class which encapsulates the logic for the TextView auto-size text feature added to @@ -66,7 +66,8 @@ class AppCompatTextViewAutoSizeHelper { private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1; // Cache of TextView methods used via reflection; the key is the method name and the value is // the method itself or null if it can not be found. - private static Hashtable sTextViewMethodByNameCache = new Hashtable<>(); + private static ConcurrentHashMap sTextViewMethodByNameCache = + new ConcurrentHashMap<>(); // Use this to specify that any of the auto-size configuration int values have not been set. static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f; // Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when @@ -701,7 +702,7 @@ class AppCompatTextViewAutoSizeHelper { return true; } - @TargetApi(23) + @RequiresApi(23) private StaticLayout createStaticLayoutForMeasuring(CharSequence text, Layout.Alignment alignment, int availableWidth, int maxLines) { // Can use the StaticLayout.Builder (along with TextView params added in or after @@ -725,7 +726,6 @@ class AppCompatTextViewAutoSizeHelper { .build(); } - @TargetApi(14) private StaticLayout createStaticLayoutForMeasuringPre23(CharSequence text, Layout.Alignment alignment, int availableWidth) { // Setup defaults. diff --git a/android/support/v7/widget/ButtonBarLayout.java b/android/support/v7/widget/ButtonBarLayout.java index b47a5689..f4bbc6c3 100644 --- a/android/support/v7/widget/ButtonBarLayout.java +++ b/android/support/v7/widget/ButtonBarLayout.java @@ -35,9 +35,6 @@ import android.widget.LinearLayout; */ @RestrictTo(LIBRARY_GROUP) public class ButtonBarLayout extends LinearLayout { - /** Minimum screen height required for button stacking. */ - private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320; - /** Amount of the second button to "peek" above the fold when stacked. */ private static final int PEEK_BUTTON_DP = 16; @@ -50,11 +47,8 @@ public class ButtonBarLayout extends LinearLayout { public ButtonBarLayout(Context context, AttributeSet attrs) { super(context, attrs); - final boolean allowStackingDefault = - getResources().getConfiguration().screenHeightDp >= ALLOW_STACKING_MIN_HEIGHT_DP; final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout); - mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, - allowStackingDefault); + mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true); ta.recycle(); } diff --git a/android/support/v7/widget/CardView.java b/android/support/v7/widget/CardView.java index 58a04f0a..a45ee989 100644 --- a/android/support/v7/widget/CardView.java +++ b/android/support/v7/widget/CardView.java @@ -108,18 +108,57 @@ public class CardView extends FrameLayout { final Rect mShadowBounds = new Rect(); public CardView(@NonNull Context context) { - super(context); - initialize(context, null, 0); + this(context, null); } public CardView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs, 0); + this(context, attrs, R.attr.cardViewStyle); } public CardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - initialize(context, attrs, defStyleAttr); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, + R.style.CardView); + ColorStateList backgroundColor; + if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { + backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); + } else { + // There isn't one set, so we'll compute one based on the theme + final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); + final int themeColorBackground = aa.getColor(0, 0); + aa.recycle(); + + // If the theme colorBackground is light, use our own light color, otherwise dark + final float[] hsv = new float[3]; + Color.colorToHSV(themeColorBackground, hsv); + backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f + ? getResources().getColor(R.color.cardview_light_background) + : getResources().getColor(R.color.cardview_dark_background)); + } + float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); + float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); + float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); + mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); + mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); + int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); + mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, + defaultPadding); + mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, + defaultPadding); + mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, + defaultPadding); + mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, + defaultPadding); + if (elevation > maxElevation) { + maxElevation = elevation; + } + mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); + mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); + a.recycle(); + + IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, + elevation, maxElevation); } @Override @@ -220,50 +259,6 @@ public class CardView extends FrameLayout { } } - private void initialize(Context context, AttributeSet attrs, int defStyleAttr) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, - R.style.CardView); - ColorStateList backgroundColor; - if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { - backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); - } else { - // There isn't one set, so we'll compute one based on the theme - final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); - final int themeColorBackground = aa.getColor(0, 0); - aa.recycle(); - - // If the theme colorBackground is light, use our own light color, otherwise dark - final float[] hsv = new float[3]; - Color.colorToHSV(themeColorBackground, hsv); - backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f - ? getResources().getColor(R.color.cardview_light_background) - : getResources().getColor(R.color.cardview_dark_background)); - } - float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); - float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); - float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); - mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); - mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); - int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); - mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, - defaultPadding); - mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, - defaultPadding); - mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, - defaultPadding); - mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, - defaultPadding); - if (elevation > maxElevation) { - maxElevation = elevation; - } - mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); - mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); - a.recycle(); - - IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, - elevation, maxElevation); - } - @Override public void setMinimumWidth(int minWidth) { mUserSetMinWidth = minWidth; diff --git a/android/support/v7/widget/LinearLayoutManager.java b/android/support/v7/widget/LinearLayoutManager.java index 27df4901..fe4a37a6 100644 --- a/android/support/v7/widget/LinearLayoutManager.java +++ b/android/support/v7/widget/LinearLayoutManager.java @@ -48,9 +48,9 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements static final boolean DEBUG = false; - public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; - public static final int VERTICAL = OrientationHelper.VERTICAL; + public static final int VERTICAL = RecyclerView.VERTICAL; public static final int INVALID_OFFSET = Integer.MIN_VALUE; @@ -66,7 +66,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} */ @RecyclerView.Orientation - int mOrientation; + int mOrientation = RecyclerView.DEFAULT_ORIENTATION; /** * Helper class that keeps temporary layout state. @@ -78,8 +78,6 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements /** * Many calculations are made depending on orientation. To keep it clean, this interface * helps {@link LinearLayoutManager} make those decisions. - * Based on {@link #mOrientation}, an implementation is lazily created in - * {@link #ensureLayoutState} method. */ OrientationHelper mOrientationHelper; @@ -154,7 +152,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements * @param context Current context, will be used to access resources. */ public LinearLayoutManager(Context context) { - this(context, VERTICAL, false); + this(context, RecyclerView.DEFAULT_ORIENTATION, false); } /** @@ -335,13 +333,16 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements if (orientation != HORIZONTAL && orientation != VERTICAL) { throw new IllegalArgumentException("invalid orientation:" + orientation); } + assertNotInLayoutOrScroll(null); - if (orientation == mOrientation) { - return; + + if (orientation != mOrientation || mOrientationHelper == null) { + mOrientationHelper = + OrientationHelper.createOrientationHelper(this, orientation); + mAnchorInfo.mOrientationHelper = mOrientationHelper; + mOrientation = orientation; + requestLayout(); } - mOrientation = orientation; - mOrientationHelper = null; - requestLayout(); } /** @@ -516,7 +517,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements // TestResizingRelayoutWithAutoMeasure), which happens if we were to call // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference // child which can change between layout passes). - mAnchorInfo.assignFromViewAndKeepVisibleRect(focused); + mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); } if (DEBUG) { Log.d(TAG, "Anchor info:" + mAnchorInfo); @@ -781,7 +782,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements } final View focused = getFocusedChild(); if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) { - anchorInfo.assignFromViewAndKeepVisibleRect(focused); + anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); return true; } if (mLastStackFromEnd != mStackFromEnd) { @@ -791,7 +792,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state); if (referenceChild != null) { - anchorInfo.assignFromView(referenceChild); + anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); // If all visible views are removed in 1 pass, reference child might be out of bounds. // If that is the case, offset it back to 0 so that we use these pre-layout children. if (!state.isPreLayout() && supportsPredictiveItemAnimations()) { @@ -985,9 +986,6 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements if (mLayoutState == null) { mLayoutState = createLayoutState(); } - if (mOrientationHelper == null) { - mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); - } } /** @@ -2370,7 +2368,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements /** * Simple data class to keep Anchor information */ - class AnchorInfo { + static class AnchorInfo { + OrientationHelper mOrientationHelper; int mPosition; int mCoordinate; boolean mLayoutFromEnd; @@ -2413,13 +2412,13 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements && lp.getViewLayoutPosition() < state.getItemCount(); } - public void assignFromViewAndKeepVisibleRect(View child) { + public void assignFromViewAndKeepVisibleRect(View child, int position) { final int spaceChange = mOrientationHelper.getTotalSpaceChange(); if (spaceChange >= 0) { - assignFromView(child); + assignFromView(child, position); return; } - mPosition = getPosition(child); + mPosition = position; if (mLayoutFromEnd) { final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; final int childEnd = mOrientationHelper.getDecoratedEnd(child); @@ -2460,7 +2459,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements } } - public void assignFromView(View child) { + public void assignFromView(View child, int position) { if (mLayoutFromEnd) { mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange(); @@ -2468,7 +2467,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements mCoordinate = mOrientationHelper.getDecoratedStart(child); } - mPosition = getPosition(child); + mPosition = position; } } diff --git a/android/support/v7/widget/ListPopupWindow.java b/android/support/v7/widget/ListPopupWindow.java index edc9781c..b98197c9 100644 --- a/android/support/v7/widget/ListPopupWindow.java +++ b/android/support/v7/widget/ListPopupWindow.java @@ -283,7 +283,7 @@ public class ListPopupWindow implements ShowableListMenu { mAdapter.unregisterDataSetObserver(mObserver); } mAdapter = adapter; - if (mAdapter != null) { + if (adapter != null) { adapter.registerDataSetObserver(mObserver); } diff --git a/android/support/v7/widget/OrientationHelper.java b/android/support/v7/widget/OrientationHelper.java index 5e90f2e7..99bcbaa0 100644 --- a/android/support/v7/widget/OrientationHelper.java +++ b/android/support/v7/widget/OrientationHelper.java @@ -47,6 +47,14 @@ public abstract class OrientationHelper { mLayoutManager = layoutManager; } + /** + * Returns the {@link android.support.v7.widget.RecyclerView.LayoutManager LayoutManager} that + * is associated with this OrientationHelper. + */ + public RecyclerView.LayoutManager getLayoutManager() { + return mLayoutManager; + } + /** * Call this method after onLayout method is complete if state is NOT pre-layout. * This method records information like layout bounds that might be useful in the next layout @@ -435,4 +443,4 @@ public abstract class OrientationHelper { } }; } -} \ No newline at end of file +} diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java index 84c28b10..a2879796 100644 --- a/android/support/v7/widget/RecyclerView.java +++ b/android/support/v7/widget/RecyclerView.java @@ -217,6 +217,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro public static final int HORIZONTAL = LinearLayout.HORIZONTAL; public static final int VERTICAL = LinearLayout.VERTICAL; + static final int DEFAULT_ORIENTATION = VERTICAL; public static final int NO_POSITION = -1; public static final long NO_ID = -1; public static final int INVALID_TYPE = -1; @@ -349,7 +350,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return; } if (mLayoutFrozen) { - mLayoutRequestEaten = true; + mLayoutWasDefered = true; return; //we'll process updates when ice age ends. } consumePendingUpdateOperations(); @@ -371,10 +372,21 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro boolean mEnableFastScroller; @VisibleForTesting boolean mFirstLayoutComplete; - // Counting lock to control whether we should ignore requestLayout calls from children or not. - private int mEatRequestLayout = 0; + /** + * The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of + * calls to {@link #startInterceptRequestLayout()} - number of calls to + * {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we + * should defer layout operations caused by layout requests from children of + * {@link RecyclerView}. + */ + private int mInterceptRequestLayoutDepth = 0; + + /** + * True if a call to requestLayout was intercepted and prevented from executing like normal and + * we plan on continuing with normal execution later. + */ + boolean mLayoutWasDefered; - boolean mLayoutRequestEaten; boolean mLayoutFrozen; private boolean mIgnoreMotionEventTillDown; @@ -1355,7 +1367,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * @return true if an animating view is removed */ boolean removeAnimatingView(View view) { - eatRequestLayout(); + startInterceptRequestLayout(); final boolean removed = mChildHelper.removeViewIfHidden(view); if (removed) { final ViewHolder viewHolder = getChildViewHolderInt(view); @@ -1366,7 +1378,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } } // only clear request eaten flag if we removed the view. - resumeRequestLayout(!removed); + stopInterceptRequestLayout(!removed); return removed; } @@ -1736,10 +1748,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE | AdapterHelper.UpdateOp.MOVE)) { TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); mAdapterHelper.preProcess(); - if (!mLayoutRequestEaten) { + if (!mLayoutWasDefered) { if (hasUpdatedView()) { dispatchLayout(); } else { @@ -1747,7 +1759,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mAdapterHelper.consumePostponedUpdates(); } } - resumeRequestLayout(true); + stopInterceptRequestLayout(true); onExitLayoutOrScroll(); TraceCompat.endSection(); } else if (mAdapterHelper.hasPendingUpdates()) { @@ -1791,7 +1803,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro consumePendingUpdateOperations(); if (mAdapter != null) { - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); TraceCompat.beginSection(TRACE_SCROLL_TAG); fillRemainingScrollValues(mState); @@ -1806,7 +1818,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro TraceCompat.endSection(); repositionShadowingViews(); onExitLayoutOrScroll(); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); } if (!mItemDecorations.isEmpty()) { invalidate(); @@ -1983,24 +1995,45 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0; } - - void eatRequestLayout() { - mEatRequestLayout++; - if (mEatRequestLayout == 1 && !mLayoutFrozen) { - mLayoutRequestEaten = false; + /** + * This method should be called before any code that may trigger a child view to cause a call to + * {@link RecyclerView#requestLayout()}. Doing so enables {@link RecyclerView} to avoid + * reacting to additional redundant calls to {@link #requestLayout()}. + *

    + * A call to this method must always be accompanied by a call to + * {@link #stopInterceptRequestLayout(boolean)} that follows the code that may trigger a + * child View to cause a call to {@link RecyclerView#requestLayout()}. + * + * @see #stopInterceptRequestLayout(boolean) + */ + void startInterceptRequestLayout() { + mInterceptRequestLayoutDepth++; + if (mInterceptRequestLayoutDepth == 1 && !mLayoutFrozen) { + mLayoutWasDefered = false; } } - void resumeRequestLayout(boolean performLayoutChildren) { - if (mEatRequestLayout < 1) { + /** + * This method should be called after any code that may trigger a child view to cause a call to + * {@link RecyclerView#requestLayout()}. + *

    + * A call to this method must always be accompanied by a call to + * {@link #startInterceptRequestLayout()} that precedes the code that may trigger a child + * View to cause a call to {@link RecyclerView#requestLayout()}. + * + * @see #startInterceptRequestLayout() + */ + void stopInterceptRequestLayout(boolean performLayoutChildren) { + if (mInterceptRequestLayoutDepth < 1) { //noinspection PointlessBooleanExpression if (DEBUG) { - throw new IllegalStateException("invalid eat request layout count" + throw new IllegalStateException("stopInterceptRequestLayout was called more " + + "times than startInterceptRequestLayout." + exceptionLabel()); } - mEatRequestLayout = 1; + mInterceptRequestLayoutDepth = 1; } - if (!performLayoutChildren) { + if (!performLayoutChildren && !mLayoutFrozen) { // Reset the layout request eaten counter. // This is necessary since eatRequest calls can be nested in which case the other // call will override the inner one. @@ -2009,19 +2042,19 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // eat layout for dispatchLayout // a bunch of req layout calls arrive - mLayoutRequestEaten = false; + mLayoutWasDefered = false; } - if (mEatRequestLayout == 1) { + if (mInterceptRequestLayoutDepth == 1) { // when layout is frozen we should delay dispatchLayout() - if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen + if (performLayoutChildren && mLayoutWasDefered && !mLayoutFrozen && mLayout != null && mAdapter != null) { dispatchLayout(); } if (!mLayoutFrozen) { - mLayoutRequestEaten = false; + mLayoutWasDefered = false; } } - mEatRequestLayout--; + mInterceptRequestLayoutDepth--; } /** @@ -2051,10 +2084,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll"); if (!frozen) { mLayoutFrozen = false; - if (mLayoutRequestEaten && mLayout != null && mAdapter != null) { + if (mLayoutWasDefered && mLayout != null && mAdapter != null) { requestLayout(); } - mLayoutRequestEaten = false; + mLayoutWasDefered = false; } else { final long now = SystemClock.uptimeMillis(); MotionEvent cancelEvent = MotionEvent.obtain(now, now, @@ -2471,9 +2504,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // panic, focused view is not a child anymore, cannot call super. return null; } - eatRequestLayout(); + startInterceptRequestLayout(); mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); } result = ff.findNextFocus(this, focused, direction); } else { @@ -2485,9 +2518,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // panic, focused view is not a child anymore, cannot call super. return null; } - eatRequestLayout(); + startInterceptRequestLayout(); result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); } } if (result != null && !result.hasFocusable()) { @@ -3202,7 +3235,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } // custom onMeasure if (mAdapterUpdateDuringMeasure) { - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); onExitLayoutOrScroll(); @@ -3215,7 +3248,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mState.mInPreLayout = false; } mAdapterUpdateDuringMeasure = false; - resumeRequestLayout(false); + stopInterceptRequestLayout(false); } else if (mState.mRunPredictiveAnimations) { // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true: // this means there is already an onMeasure() call performed to handle the pending @@ -3231,9 +3264,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } else { mState.mItemCount = 0; } - eatRequestLayout(); + startInterceptRequestLayout(); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); mState.mInPreLayout = false; // clear } } @@ -3668,7 +3701,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mState.assertLayoutStep(State.STEP_START); fillRemainingScrollValues(mState); mState.mIsMeasuring = false; - eatRequestLayout(); + startInterceptRequestLayout(); mViewInfoStore.clear(); onEnterLayoutOrScroll(); processAdapterUpdatesAndSetAnimationFlags(); @@ -3748,7 +3781,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro clearOldPositions(); } onExitLayoutOrScroll(); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT; } @@ -3757,7 +3790,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * This step might be run multiple times if necessary (e.g. measure). */ private void dispatchLayoutStep2() { - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); @@ -3775,7 +3808,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; mState.mLayoutStep = State.STEP_ANIMATIONS; onExitLayoutOrScroll(); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); } /** @@ -3784,7 +3817,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro */ private void dispatchLayoutStep3() { mState.assertLayoutStep(State.STEP_ANIMATIONS); - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); mState.mLayoutStep = State.STEP_START; if (mState.mRunSimpleAnimations) { @@ -3860,7 +3893,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); mViewInfoStore.clear(); if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { dispatchOnScrolled(0, 0); @@ -4043,10 +4076,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro @Override public void requestLayout() { - if (mEatRequestLayout == 0 && !mLayoutFrozen) { + if (mInterceptRequestLayoutDepth == 0 && !mLayoutFrozen) { super.requestLayout(); } else { - mLayoutRequestEaten = true; + mLayoutWasDefered = true; } } @@ -4905,7 +4938,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } if (mAdapter != null) { - eatRequestLayout(); + startInterceptRequestLayout(); onEnterLayoutOrScroll(); TraceCompat.beginSection(TRACE_SCROLL_TAG); fillRemainingScrollValues(mState); @@ -4921,7 +4954,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro repositionShadowingViews(); onExitLayoutOrScroll(); - resumeRequestLayout(false); + stopInterceptRequestLayout(false); if (smoothScroller != null && !smoothScroller.isPendingInitialRun() && smoothScroller.isRunning()) { @@ -6544,7 +6577,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * @see #getItemViewType(int) * @see #onBindViewHolder(ViewHolder, int) */ - public abstract VH onCreateViewHolder(ViewGroup parent, int viewType); + @NonNull + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); /** * Called by RecyclerView to display the data at the specified position. This method should @@ -6566,7 +6600,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * item at the given position in the data set. * @param position The position of the item within the adapter's data set. */ - public abstract void onBindViewHolder(VH holder, int position); + public abstract void onBindViewHolder(@NonNull VH holder, int position); /** * Called by RecyclerView to display the data at the specified position. This method @@ -6597,7 +6631,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * @param payloads A non-null list of merged payloads. Can be empty list if requires full * update. */ - public void onBindViewHolder(VH holder, int position, List payloads) { + public void onBindViewHolder(@NonNull VH holder, int position, + @NonNull List payloads) { onBindViewHolder(holder, position); } @@ -6607,7 +6642,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #onCreateViewHolder(ViewGroup, int) */ - public final VH createViewHolder(ViewGroup parent, int viewType) { + public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); final VH holder = onCreateViewHolder(parent, viewType); holder.mItemViewType = viewType; @@ -6622,7 +6657,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #onBindViewHolder(ViewHolder, int) */ - public final void bindViewHolder(VH holder, int position) { + public final void bindViewHolder(@NonNull VH holder, int position) { holder.mPosition = position; if (hasStableIds()) { holder.mItemId = getItemId(position); @@ -6719,7 +6754,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @param holder The ViewHolder for the view being recycled */ - public void onViewRecycled(VH holder) { + public void onViewRecycled(@NonNull VH holder) { } /** @@ -6756,7 +6791,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * RecyclerView will check the View's transient state again before giving a final decision. * Default implementation returns false. */ - public boolean onFailedToRecycleView(VH holder) { + public boolean onFailedToRecycleView(@NonNull VH holder) { return false; } @@ -6770,7 +6805,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @param holder Holder of the view being attached */ - public void onViewAttachedToWindow(VH holder) { + public void onViewAttachedToWindow(@NonNull VH holder) { } /** @@ -6782,7 +6817,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @param holder Holder of the view being detached */ - public void onViewDetachedFromWindow(VH holder) { + public void onViewDetachedFromWindow(@NonNull VH holder) { } /** @@ -6810,7 +6845,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) */ - public void registerAdapterDataObserver(AdapterDataObserver observer) { + public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { mObservable.registerObserver(observer); } @@ -6824,7 +6859,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver) */ - public void unregisterAdapterDataObserver(AdapterDataObserver observer) { + public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { mObservable.unregisterObserver(observer); } @@ -6836,7 +6871,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * @param recyclerView The RecyclerView instance which started observing this adapter. * @see #onDetachedFromRecyclerView(RecyclerView) */ - public void onAttachedToRecyclerView(RecyclerView recyclerView) { + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { } /** @@ -6845,7 +6880,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * @param recyclerView The RecyclerView instance which stopped observing this adapter. * @see #onAttachedToRecyclerView(RecyclerView) */ - public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { } /** @@ -6921,7 +6956,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #notifyItemRangeChanged(int, int) */ - public final void notifyItemChanged(int position, Object payload) { + public final void notifyItemChanged(int position, @Nullable Object payload) { mObservable.notifyItemRangeChanged(position, 1, payload); } @@ -6969,7 +7004,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro * * @see #notifyItemChanged(int) */ - public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { + public final void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { mObservable.notifyItemRangeChanged(positionStart, itemCount, payload); } @@ -10072,7 +10108,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (vScroll == 0 && hScroll == 0) { return false; } - mRecyclerView.smoothScrollBy(hScroll, vScroll); + mRecyclerView.scrollBy(hScroll, vScroll); return true; } @@ -10118,7 +10154,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, defStyleAttr, defStyleRes); properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation, - VERTICAL); + DEFAULT_ORIENTATION); properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); @@ -10833,8 +10869,12 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro */ private void onEnteredHiddenState(RecyclerView parent) { // While the view item is in hidden state, make it invisible for the accessibility. - mWasImportantForAccessibilityBeforeHidden = - ViewCompat.getImportantForAccessibility(itemView); + if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) { + mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState; + } else { + mWasImportantForAccessibilityBeforeHidden = + ViewCompat.getImportantForAccessibility(itemView); + } parent.setChildImportantForAccessibilityInternal(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } @@ -11193,7 +11233,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro // do nothing } - public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { // fallback to onItemRangeChanged(positionStart, itemCount) if app // does not override this method. onItemRangeChanged(positionStart, itemCount); @@ -11670,7 +11710,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro notifyItemRangeChanged(positionStart, itemCount, null); } - public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) { + public void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { // since onItemRangeChanged() is implemented by the app, it could do anything, including // removing itself from {@link mObservers} - and that could cause problems if // an iterator is used on the ArrayList {@link mObservers}. @@ -12063,6 +12104,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro + "mTargetPosition=" + mTargetPosition + ", mData=" + mData + ", mItemCount=" + mItemCount + + ", mIsMeasuring=" + mIsMeasuring + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + ", mDeletedInvisibleItemCountSincePreviousLayout=" + mDeletedInvisibleItemCountSincePreviousLayout diff --git a/android/support/v7/widget/StaggeredGridLayoutManager.java b/android/support/v7/widget/StaggeredGridLayoutManager.java index f3ea0453..55fb14e8 100644 --- a/android/support/v7/widget/StaggeredGridLayoutManager.java +++ b/android/support/v7/widget/StaggeredGridLayoutManager.java @@ -59,9 +59,9 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple static final boolean DEBUG = false; - public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; - public static final int VERTICAL = OrientationHelper.VERTICAL; + public static final int VERTICAL = RecyclerView.VERTICAL; /** * Does not do anything to hide gaps. diff --git a/android/support/v7/widget/TooltipCompat.java b/android/support/v7/widget/TooltipCompat.java index 470c3b2c..4a583da1 100644 --- a/android/support/v7/widget/TooltipCompat.java +++ b/android/support/v7/widget/TooltipCompat.java @@ -16,7 +16,6 @@ package android.support.v7.widget; -import android.annotation.TargetApi; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -28,34 +27,6 @@ import android.view.View; * */ public class TooltipCompat { - private interface ViewCompatImpl { - void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText); - } - - private static class BaseViewCompatImpl implements ViewCompatImpl { - @Override - public void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) { - TooltipCompatHandler.setTooltipText(view, tooltipText); - } - } - - @TargetApi(26) - private static class Api26ViewCompatImpl implements ViewCompatImpl { - @Override - public void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) { - view.setTooltipText(tooltipText); - } - } - - private static final ViewCompatImpl IMPL; - static { - if (Build.VERSION.SDK_INT >= 26) { - IMPL = new Api26ViewCompatImpl(); - } else { - IMPL = new BaseViewCompatImpl(); - } - } - /** * Sets the tooltip text for the view. *

    Prior to API 26 this method sets or clears (when tooltip is null) the view's @@ -66,7 +37,11 @@ public class TooltipCompat { * @param tooltipText the tooltip text */ public static void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) { - IMPL.setTooltipText(view, tooltipText); + if (Build.VERSION.SDK_INT >= 26) { + view.setTooltipText(tooltipText); + } else { + TooltipCompatHandler.setTooltipText(view, tooltipText); + } } private TooltipCompat() {} diff --git a/android/support/v7/widget/ViewInfoStoreTest.java b/android/support/v7/widget/ViewInfoStoreTest.java index 4a224a41..c4163140 100644 --- a/android/support/v7/widget/ViewInfoStoreTest.java +++ b/android/support/v7/widget/ViewInfoStoreTest.java @@ -21,6 +21,13 @@ import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARE import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_POST; import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_PRE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.test.filters.SmallTest; @@ -29,8 +36,6 @@ import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo; import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.View; -import junit.framework.TestCase; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,7 +49,7 @@ import java.util.Map; @SuppressWarnings("ConstantConditions") @RunWith(JUnit4.class) @SmallTest -public class ViewInfoStoreTest extends TestCase { +public class ViewInfoStoreTest { ViewInfoStore mStore; LoggingProcessCallback mCallback; @Before diff --git a/android/support/v7/widget/helper/ItemTouchHelper.java b/android/support/v7/widget/helper/ItemTouchHelper.java index aee48dfa..d2b6a202 100644 --- a/android/support/v7/widget/helper/ItemTouchHelper.java +++ b/android/support/v7/widget/helper/ItemTouchHelper.java @@ -457,7 +457,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration destroyCallbacks(); } mRecyclerView = recyclerView; - if (mRecyclerView != null) { + if (recyclerView != null) { final Resources resources = recyclerView.getResources(); mSwipeEscapeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); diff --git a/android/support/wear/ambient/AmbientDelegateTest.java b/android/support/wear/ambient/AmbientDelegateTest.java new file mode 100644 index 00000000..60332323 --- /dev/null +++ b/android/support/wear/ambient/AmbientDelegateTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.wear.ambient; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.FragmentActivity; + +import com.google.android.wearable.compat.WearableActivityController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** + * Tests for {@link AmbientDelegate}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AmbientDelegateTest { + + @Mock + AmbientDelegate.AmbientCallback mMockAmbientCallback; + @Mock + WearableControllerProvider mMockWearableControllerProvider; + @Mock + WearableActivityController mMockWearableController; + @Mock + FragmentActivity mMockActivity; + + private AmbientDelegate mAmbientDelegateUnderTest; + + @Before + public void setUp() { + mMockAmbientCallback = mock(AmbientDelegate.AmbientCallback.class); + mMockWearableControllerProvider = mock(WearableControllerProvider.class); + mMockWearableController = mock(WearableActivityController.class); + mMockActivity = mock(FragmentActivity.class); + when(mMockWearableControllerProvider + .getWearableController(mMockActivity, mMockAmbientCallback)) + .thenReturn(mMockWearableController); + } + + @Test + public void testNullActivity() { + mAmbientDelegateUnderTest = new AmbientDelegate(null, + mMockWearableControllerProvider, mMockAmbientCallback); + verifyZeroInteractions(mMockWearableControllerProvider); + + assertFalse(mAmbientDelegateUnderTest.isAmbient()); + + } + + @Test + public void testActivityPresent() { + mAmbientDelegateUnderTest = new AmbientDelegate(mMockActivity, + mMockWearableControllerProvider, mMockAmbientCallback); + + mAmbientDelegateUnderTest.onCreate(); + verify(mMockWearableController).onCreate(); + + mAmbientDelegateUnderTest.onResume(); + verify(mMockWearableController).onResume(); + + mAmbientDelegateUnderTest.onPause(); + verify(mMockWearableController).onPause(); + + mAmbientDelegateUnderTest.onStop(); + verify(mMockWearableController).onStop(); + + mAmbientDelegateUnderTest.onDestroy(); + verify(mMockWearableController).onDestroy(); + } +} diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java index 0077a5bd..d3a8a435 100644 --- a/android/support/wear/ambient/AmbientMode.java +++ b/android/support/wear/ambient/AmbientMode.java @@ -48,7 +48,9 @@ import java.io.PrintWriter; * AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this); * boolean isAmbient = controller.isAmbient(); * } + * @deprecated please use {@link AmbientModeSupport} instead. */ +@Deprecated public final class AmbientMode extends Fragment { private static final String TAG = "AmbientMode"; diff --git a/android/support/wear/ambient/AmbientModeResumeTest.java b/android/support/wear/ambient/AmbientModeResumeTest.java new file mode 100644 index 00000000..007c6194 --- /dev/null +++ b/android/support/wear/ambient/AmbientModeResumeTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.ambient; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.widget.util.WakeLockRule; + +import com.google.android.wearable.compat.WearableActivityController; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class AmbientModeResumeTest { + @Rule + public final WakeLockRule mWakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = + new ActivityTestRule<>(AmbientModeResumeTestActivity.class); + + @Test + public void testActivityDefaults() throws Throwable { + assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled()); + assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled()); + } +} diff --git a/android/support/wear/ambient/AmbientModeResumeTestActivity.java b/android/support/wear/ambient/AmbientModeResumeTestActivity.java new file mode 100644 index 00000000..0ca3c15b --- /dev/null +++ b/android/support/wear/ambient/AmbientModeResumeTestActivity.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.wear.ambient; + +import android.app.Activity; +import android.os.Bundle; + +public class AmbientModeResumeTestActivity extends Activity { + AmbientMode.AmbientController mAmbientController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAmbientController = AmbientMode.attachAmbientSupport(this); + } +} diff --git a/android/support/wear/ambient/AmbientModeSupport.java b/android/support/wear/ambient/AmbientModeSupport.java new file mode 100644 index 00000000..97e26d9f --- /dev/null +++ b/android/support/wear/ambient/AmbientModeSupport.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.wear.ambient; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.util.Log; + +import com.google.android.wearable.compat.WearableActivityController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Use this as a headless Fragment to add ambient support to an Activity on Wearable devices. + *

    + * The application that uses this should add the {@link android.Manifest.permission#WAKE_LOCK} + * permission to its manifest. + *

    + * The primary entry point for this code is the {@link #attach(FragmentActivity)} method. + * It should be called with an {@link FragmentActivity} as an argument and that + * {@link FragmentActivity} will then be able to receive ambient lifecycle events through + * an {@link AmbientCallback}. The {@link FragmentActivity} will also receive a + * {@link AmbientController} object from the attachment which can be used to query the current + * status of the ambient mode. An example of how to attach {@link AmbientModeSupport} to your + * {@link FragmentActivity} and use the {@link AmbientController} can be found below: + *

    + *

    {@code
    + *     AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
    + *     boolean isAmbient =  controller.isAmbient();
    + * }
    + */ +public final class AmbientModeSupport extends Fragment { + private static final String TAG = "AmbientMode"; + + /** + * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate + * whether burn-in protection is required. When this property is set to true, views must be + * shifted around periodically in ambient mode. To ensure that content isn't shifted off + * the screen, avoid placing content within 10 pixels of the edge of the screen. Activities + * should also avoid solid white areas to prevent pixel burn-in. Both of these requirements + * only apply in ambient mode, and only when this property is set to true. + */ + public static final String EXTRA_BURN_IN_PROTECTION = + WearableActivityController.EXTRA_BURN_IN_PROTECTION; + + /** + * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate + * whether the device has low-bit ambient mode. When this property is set to true, the screen + * supports fewer bits for each color in ambient mode. In this case, activities should disable + * anti-aliasing in ambient mode. + */ + public static final String EXTRA_LOWBIT_AMBIENT = + WearableActivityController.EXTRA_LOWBIT_AMBIENT; + + /** + * Fragment tag used by default when adding {@link AmbientModeSupport} to add ambient support to + * a {@link FragmentActivity}. + */ + public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode"; + + /** + * Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the + * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used + * to bind the {@link AmbientModeSupport} to the instantiation of this interface. + *

    + *

    {@code
    +     * return new AmbientMode.AmbientCallback() {
    +     *     public void onEnterAmbient(Bundle ambientDetails) {...}
    +     *     public void onExitAmbient(Bundle ambientDetails) {...}
    +     *  }
    +     * }
    + */ + public interface AmbientCallbackProvider { + /** + * @return the {@link AmbientCallback} to be used by this class to communicate with the + * entity interested in ambient events. + */ + AmbientCallback getAmbientCallback(); + } + + /** + * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode. + */ + public abstract static class AmbientCallback { + /** + * Called when an activity is entering ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). All drawing should complete by the conclusion + * of this method. Note that {@code invalidate()} calls will be executed before resuming + * lower-power mode. + * + * @param ambientDetails bundle containing information about the display being used. + * It includes information about low-bit color and burn-in protection. + */ + public void onEnterAmbient(Bundle ambientDetails) {} + + /** + * Called when the system is updating the display for ambient mode. Activities may use this + * opportunity to update or invalidate views. + */ + public void onUpdateAmbient() {} + + /** + * Called when an activity should exit ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). + */ + public void onExitAmbient() {} + } + + private final AmbientDelegate.AmbientCallback mCallback = + new AmbientDelegate.AmbientCallback() { + @Override + public void onEnterAmbient(Bundle ambientDetails) { + if (mSuppliedCallback != null) { + mSuppliedCallback.onEnterAmbient(ambientDetails); + } + } + + @Override + public void onExitAmbient() { + if (mSuppliedCallback != null) { + mSuppliedCallback.onExitAmbient(); + } + } + + @Override + public void onUpdateAmbient() { + if (mSuppliedCallback != null) { + mSuppliedCallback.onUpdateAmbient(); + } + } + }; + private AmbientDelegate mDelegate; + @Nullable + private AmbientCallback mSuppliedCallback; + private AmbientController mController; + + /** + * Constructor + */ + public AmbientModeSupport() { + mController = new AmbientController(); + } + + @Override + @CallSuper + public void onAttach(Context context) { + super.onAttach(context); + mDelegate = new AmbientDelegate(getActivity(), new WearableControllerProvider(), mCallback); + + if (context instanceof AmbientCallbackProvider) { + mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback(); + } else { + Log.w(TAG, "No callback provided - enabling only smart resume"); + } + } + + @Override + @CallSuper + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mDelegate.onCreate(); + if (mSuppliedCallback != null) { + mDelegate.setAmbientEnabled(); + } + } + + @Override + @CallSuper + public void onResume() { + super.onResume(); + mDelegate.onResume(); + } + + @Override + @CallSuper + public void onPause() { + mDelegate.onPause(); + super.onPause(); + } + + @Override + @CallSuper + public void onStop() { + mDelegate.onStop(); + super.onStop(); + } + + @Override + @CallSuper + public void onDestroy() { + mDelegate.onDestroy(); + super.onDestroy(); + } + + @Override + @CallSuper + public void onDetach() { + mDelegate = null; + super.onDetach(); + } + + /** + * Attach ambient support to the given activity. Calling this method with an Activity + * implementing the {@link AmbientCallbackProvider} interface will provide you with an + * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively, + * you can call this method with an Activity which does not implement + * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume + * functionality. This is equivalent to providing (@code null} from + * the {@link AmbientCallbackProvider}. + * + * @param activity the activity to attach ambient support to. + * @return the associated {@link AmbientController} which can be used to query the state of + * ambient mode. + */ + public static AmbientController attach(T activity) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + AmbientModeSupport ambientFragment = + (AmbientModeSupport) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (ambientFragment == null) { + AmbientModeSupport fragment = new AmbientModeSupport(); + fragmentManager + .beginTransaction() + .add(fragment, FRAGMENT_TAG) + .commit(); + ambientFragment = fragment; + } + return ambientFragment.mController; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mDelegate != null) { + mDelegate.dump(prefix, fd, writer, args); + } + } + + @VisibleForTesting + void setAmbientDelegate(AmbientDelegate delegate) { + mDelegate = delegate; + } + + /** + * A class for interacting with the ambient mode on a wearable device. This class can be used to + * query the current state of ambient mode. An instance of this class is returned to the user + * when they attach their {@link Activity} to {@link AmbientModeSupport}. + */ + public final class AmbientController { + private static final String TAG = "AmbientController"; + + // Do not initialize outside of this class. + AmbientController() {} + + /** + * @return {@code true} if the activity is currently in ambient. + */ + public boolean isAmbient() { + return mDelegate == null ? false : mDelegate.isAmbient(); + } + } +} diff --git a/android/support/wear/ambient/AmbientModeTest.java b/android/support/wear/ambient/AmbientModeTest.java new file mode 100644 index 00000000..f96c0c25 --- /dev/null +++ b/android/support/wear/ambient/AmbientModeTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.ambient; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.widget.util.WakeLockRule; + +import com.google.android.wearable.compat.WearableActivityController; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class AmbientModeTest { + @Rule + public final WakeLockRule mWakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = new ActivityTestRule<>( + AmbientModeTestActivity.class); + + @Test + public void testEnterAmbientCallback() throws Throwable { + AmbientModeTestActivity activity = mActivityRule.getActivity(); + + WearableActivityController.getLastInstance().enterAmbient(); + assertTrue(activity.mEnterAmbientCalled); + assertFalse(activity.mUpdateAmbientCalled); + assertFalse(activity.mExitAmbientCalled); + } + + @Test + public void testUpdateAmbientCallback() throws Throwable { + AmbientModeTestActivity activity = mActivityRule.getActivity(); + + WearableActivityController.getLastInstance().updateAmbient(); + assertFalse(activity.mEnterAmbientCalled); + assertTrue(activity.mUpdateAmbientCalled); + assertFalse(activity.mExitAmbientCalled); + } + + @Test + public void testExitAmbientCallback() throws Throwable { + AmbientModeTestActivity activity = mActivityRule.getActivity(); + + WearableActivityController.getLastInstance().exitAmbient(); + assertFalse(activity.mEnterAmbientCalled); + assertFalse(activity.mUpdateAmbientCalled); + assertTrue(activity.mExitAmbientCalled); + } + + @Test + public void testIsAmbientEnabled() { + assertTrue(WearableActivityController.getLastInstance().isAmbientEnabled()); + } + + @Test + public void testCallsControllerIsAmbient() { + AmbientModeTestActivity activity = mActivityRule.getActivity(); + + WearableActivityController.getLastInstance().setAmbient(true); + assertTrue(activity.getAmbientController().isAmbient()); + + WearableActivityController.getLastInstance().setAmbient(false); + assertFalse(activity.getAmbientController().isAmbient()); + } +} diff --git a/android/support/wear/ambient/AmbientModeTestActivity.java b/android/support/wear/ambient/AmbientModeTestActivity.java new file mode 100644 index 00000000..26155d8f --- /dev/null +++ b/android/support/wear/ambient/AmbientModeTestActivity.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.wear.ambient; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +public class AmbientModeTestActivity extends FragmentActivity + implements AmbientMode.AmbientCallbackProvider { + AmbientMode.AmbientController mAmbientController; + + boolean mEnterAmbientCalled; + boolean mUpdateAmbientCalled; + boolean mExitAmbientCalled; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAmbientController = AmbientMode.attachAmbientSupport(this); + } + + @Override + public AmbientMode.AmbientCallback getAmbientCallback() { + return new MyAmbientCallback(); + } + + private class MyAmbientCallback extends AmbientMode.AmbientCallback { + + @Override + public void onEnterAmbient(Bundle ambientDetails) { + mEnterAmbientCalled = true; + } + + @Override + public void onUpdateAmbient() { + mUpdateAmbientCalled = true; + } + + @Override + public void onExitAmbient() { + mExitAmbientCalled = true; + } + } + + public AmbientMode.AmbientController getAmbientController() { + return mAmbientController; + } + +} diff --git a/android/support/wear/utils/MetadataTestActivity.java b/android/support/wear/utils/MetadataTestActivity.java new file mode 100644 index 00000000..f64247ee --- /dev/null +++ b/android/support/wear/utils/MetadataTestActivity.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.utils; + +import android.app.Activity; +import android.os.Bundle; +import android.support.wear.test.R; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MetadataTestActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + assertTrue(MetadataConstants.isStandalone(this)); + assertTrue(MetadataConstants.isNotificationBridgingEnabled(this)); + assertEquals(R.drawable.preview_face, + MetadataConstants.getPreviewDrawableResourceId(this, false)); + assertEquals(R.drawable.preview_face_circular, + MetadataConstants.getPreviewDrawableResourceId(this, true)); + } +} diff --git a/android/support/wear/widget/BoxInsetLayoutTest.java b/android/support/wear/widget/BoxInsetLayoutTest.java new file mode 100644 index 00000000..731f57a1 --- /dev/null +++ b/android/support/wear/widget/BoxInsetLayoutTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.wear.widget.util.MoreViewAssertions.approximateBottom; +import static android.support.wear.widget.util.MoreViewAssertions.approximateTop; +import static android.support.wear.widget.util.MoreViewAssertions.bottom; +import static android.support.wear.widget.util.MoreViewAssertions.left; +import static android.support.wear.widget.util.MoreViewAssertions.right; +import static android.support.wear.widget.util.MoreViewAssertions.screenBottom; +import static android.support.wear.widget.util.MoreViewAssertions.screenLeft; +import static android.support.wear.widget.util.MoreViewAssertions.screenRight; +import static android.support.wear.widget.util.MoreViewAssertions.screenTop; +import static android.support.wear.widget.util.MoreViewAssertions.top; + +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.test.R; +import android.support.wear.widget.util.WakeLockRule; +import android.util.DisplayMetrics; +import android.view.View; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class BoxInsetLayoutTest { + private static final float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2 + + @Rule + public final WakeLockRule mWakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = new ActivityTestRule<>( + LayoutTestActivity.class, true, false); + + @Test + public void testCase1() throws Throwable { + mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity + .EXTRA_LAYOUT_RESOURCE_ID, R.layout.box_inset_layout_testcase_1)); + DisplayMetrics dm = InstrumentationRegistry.getTargetContext().getResources() + .getDisplayMetrics(); + int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels)); + + int desiredPadding = 0; + if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) { + desiredPadding = boxInset; + } + + ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){ + @Override + public void run() { + View box = mActivityRule.getActivity().findViewById(R.id.box); + mIdViewMap.put(R.id.box, box); + } + }; + mActivityRule.runOnUiThread(customRunnable); + + View box = customRunnable.mIdViewMap.get(R.id.box); + // proxy for window location + View boxParent = (View) box.getParent(); + int parentLeft = boxParent.getLeft(); + int parentTop = boxParent.getTop(); + int parentRight = boxParent.getLeft() + boxParent.getWidth(); + int parentBottom = boxParent.getTop() + boxParent.getHeight(); + + // Child 1 is match_parent width and height + // layout_box=right|bottom + // Padding of boxInset should be added to the right and bottom sides only + onView(withId(R.id.child1)) + .check(screenLeft(equalTo(parentLeft))) + .check(screenTop(equalTo(parentTop))) + .check(screenRight(equalTo(parentRight - desiredPadding))) + .check(screenBottom(equalTo(parentBottom - desiredPadding))); + + // Content 1 is is width and height match_parent + // The bottom and right sides should be inset by boxInset pixels due to padding + // on the parent view + onView(withId(R.id.content1)) + .check(screenLeft(equalTo(parentLeft))) + .check(screenTop(equalTo(parentTop))) + .check(screenRight(equalTo(parentRight - desiredPadding))) + .check(screenBottom(equalTo(parentBottom - desiredPadding))); + } + + @Test + public void testCase2() throws Throwable { + mActivityRule.launchActivity( + new Intent().putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.box_inset_layout_testcase_2)); + DisplayMetrics dm = + InstrumentationRegistry.getTargetContext().getResources().getDisplayMetrics(); + int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels)); + + int desiredPadding = 0; + if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) { + desiredPadding = boxInset; + } + + ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){ + @Override + public void run() { + View box = mActivityRule.getActivity().findViewById(R.id.box); + View child1 = mActivityRule.getActivity().findViewById(R.id.child1); + View child2 = mActivityRule.getActivity().findViewById(R.id.child2); + View child3 = mActivityRule.getActivity().findViewById(R.id.child3); + View child4 = mActivityRule.getActivity().findViewById(R.id.child4); + mIdViewMap.put(R.id.box, box); + mIdViewMap.put(R.id.child1, child1); + mIdViewMap.put(R.id.child2, child2); + mIdViewMap.put(R.id.child3, child3); + mIdViewMap.put(R.id.child4, child4); + + } + }; + mActivityRule.runOnUiThread(customRunnable); + + View box = customRunnable.mIdViewMap.get(R.id.box); + View child1 = customRunnable.mIdViewMap.get(R.id.child1); + View child2 = customRunnable.mIdViewMap.get(R.id.child2); + View child3 = customRunnable.mIdViewMap.get(R.id.child3); + View child4 = customRunnable.mIdViewMap.get(R.id.child4); + + // proxy for window location + View boxParent = (View) box.getParent(); + int parentLeft = boxParent.getLeft(); + int parentTop = boxParent.getTop(); + int parentRight = boxParent.getLeft() + boxParent.getWidth(); + int parentBottom = boxParent.getTop() + boxParent.getHeight(); + int parentWidth = boxParent.getWidth(); + int parentHeight = boxParent.getHeight(); + + // Child 1 is width match_parent, height=60dp, gravity top + // layout_box=all means it should have padding added to left, top and right + onView(withId(R.id.child1)) + .check(screenLeft(is(equalTo(parentLeft + desiredPadding)))) + .check(screenTop(is(equalTo(parentTop + desiredPadding)))) + .check(screenRight(is(equalTo(parentRight - desiredPadding)))) + .check(screenBottom(is(equalTo(parentTop + desiredPadding + child1.getHeight())))); + + // Content 1 is width and height match_parent + // the left top and right edges should be inset by boxInset pixels, due to + // padding in the parent + onView(withId(R.id.content1)) + .check(screenLeft(equalTo(parentLeft + desiredPadding))) + .check(screenTop(equalTo(parentTop + desiredPadding))) + .check(screenRight(equalTo(parentRight - desiredPadding))); + + // Child 2 is width match_parent, height=60dp, gravity bottom + // layout_box=all means it should have padding added to left, bottom and right + onView(withId(R.id.child2)) + .check(screenLeft(is(equalTo(parentLeft + desiredPadding)))) + .check(screenTop(is(equalTo(parentBottom - desiredPadding - child2.getHeight())))) + .check(screenRight(is(equalTo(parentRight - desiredPadding)))) + .check(screenBottom(is(equalTo(parentBottom - desiredPadding)))); + + // Content 2 is width and height match_parent + // the left bottom and right edges should be inset by boxInset pixels, due to + // padding in the parent + onView(withId(R.id.content2)) + .check(screenLeft(equalTo(parentLeft + desiredPadding))) + .check(screenRight(equalTo(parentRight - desiredPadding))) + .check(screenBottom(equalTo(parentBottom - desiredPadding))); + + // Child 3 is width wrap_content, height=20dp, gravity left|center_vertical. + // layout_box=all means it should have padding added to left + // marginLeft be ignored due to gravity and layout_box=all (screenLeft=0) + onView(withId(R.id.child3)) + .check(screenLeft(is(equalTo(parentLeft + desiredPadding)))) + .check(approximateTop(is(closeTo((parentHeight / 2 - child3.getHeight() / 2), 1)))) + .check(screenRight(is(equalTo(parentLeft + desiredPadding + child3.getWidth())))) + .check(approximateBottom(is( + closeTo((parentHeight / 2 + child3.getHeight() / 2), 1)))); + + // Content 3 width and height match_parent + // the left edge should be offset from the screen edge by boxInset pixels, due to left on + // the parent + onView(withId(R.id.content3)).check(screenLeft(equalTo(desiredPadding))); + + // Child 4 is width wrap_content, height=20dp, gravity right|center_vertical. + // layout_box=all means it should have padding added to right + // it should have marginRight ignored due to gravity and layout_box=all (screenRight=max) + onView(withId(R.id.child4)) + .check(screenLeft(is(parentWidth - desiredPadding - child4.getWidth()))) + .check(approximateTop(is(closeTo((parentHeight / 2 - child3.getHeight() / 2), 1)))) + .check(screenRight(is(equalTo(parentWidth - desiredPadding)))) + .check(approximateBottom(is( + closeTo((parentHeight / 2 + child4.getHeight() / 2), 1)))); + + // Content 4 width and height wrap_content + // the right edge should be offset from the screen edge by boxInset pixels, due to + // right on the parent + onView(withId(R.id.content4)).check(screenRight(equalTo(parentWidth - desiredPadding))); + } + + @Test + public void testCase3() throws Throwable { + mActivityRule.launchActivity( + new Intent().putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.box_inset_layout_testcase_3)); + DisplayMetrics dm = + InstrumentationRegistry.getTargetContext().getResources().getDisplayMetrics(); + int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels)); + + int desiredPadding = 0; + if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) { + desiredPadding = boxInset; + } + + ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){ + @Override + public void run() { + View box = mActivityRule.getActivity().findViewById(R.id.box); + View child1 = mActivityRule.getActivity().findViewById(R.id.child1); + View child2 = mActivityRule.getActivity().findViewById(R.id.child2); + View child3 = mActivityRule.getActivity().findViewById(R.id.child3); + View child4 = mActivityRule.getActivity().findViewById(R.id.child4); + mIdViewMap.put(R.id.box, box); + mIdViewMap.put(R.id.child1, child1); + mIdViewMap.put(R.id.child2, child2); + mIdViewMap.put(R.id.child3, child3); + mIdViewMap.put(R.id.child4, child4); + } + }; + mActivityRule.runOnUiThread(customRunnable); + + View box = customRunnable.mIdViewMap.get(R.id.box); + View child1 = customRunnable.mIdViewMap.get(R.id.child1); + View child2 = customRunnable.mIdViewMap.get(R.id.child2); + View child3 = customRunnable.mIdViewMap.get(R.id.child3); + View child4 = customRunnable.mIdViewMap.get(R.id.child4); + // proxy for window location + View boxParent = (View) box.getParent(); + int parentLeft = boxParent.getLeft(); + int parentTop = boxParent.getTop(); + int parentBottom = boxParent.getTop() + boxParent.getHeight(); + int parentWidth = boxParent.getWidth(); + + // Child 1 is width and height wrap_content + // gravity is top|left, position should be 0,0 on screen + onView(withId(R.id.child1)) + .check(screenLeft(is(equalTo(parentLeft + desiredPadding)))) + .check(screenTop(is(equalTo(parentTop + desiredPadding)))) + .check(screenRight(is(equalTo(parentLeft + desiredPadding + child1.getWidth())))) + .check(screenBottom(is(equalTo(parentTop + desiredPadding + child1.getHeight())))); + + // Content 1 is width and height wrap_content + // the left and top edges should be offset from the screen edges by boxInset pixels + onView(withId(R.id.content1)) + .check(screenLeft(equalTo(parentLeft + desiredPadding))) + .check(screenTop(equalTo(parentTop + desiredPadding))); + + // Child 2 is width and height wrap_content + // gravity is top|right, position should be 0,max on screen + onView(withId(R.id.child2)) + .check(screenLeft(is(equalTo(parentWidth - desiredPadding - child2.getWidth())))) + .check(screenTop(is(equalTo(parentTop + desiredPadding)))) + .check(screenRight(is(equalTo(parentWidth - desiredPadding)))) + .check(screenBottom(is(equalTo(parentTop + desiredPadding + child2.getHeight())))); + + // Content 2 is width and height wrap_content + // the top and right edges should be offset from the screen edges by boxInset pixels + onView(withId(R.id.content2)) + .check(screenTop(equalTo(parentTop + desiredPadding))) + .check(screenRight(equalTo(parentWidth - desiredPadding))); + + // Child 3 is width and height wrap_content + // gravity is bottom|right, position should be max,max on screen + onView(withId(R.id.child3)) + .check(screenLeft(is(equalTo(parentWidth - desiredPadding - child3.getWidth())))) + .check(screenTop(is( + equalTo(parentBottom - desiredPadding - child3.getHeight())))) + .check(screenRight(is(equalTo(parentWidth - desiredPadding)))) + .check(screenBottom(is(equalTo(parentBottom - desiredPadding)))); + + // Content 3 is width and height wrap_content + // the right and bottom edges should be offset from the screen edges by boxInset pixels + onView(withId(R.id.content3)) + .check(screenBottom(equalTo(parentBottom - desiredPadding))) + .check(screenRight(equalTo(parentWidth - desiredPadding))); + + // Child 4 is width and height wrap_content + // gravity is bottom|left, position should be max,0 on screen + onView(withId(R.id.child4)) + .check(screenLeft(is(equalTo(parentLeft + desiredPadding)))) + .check(screenTop(is(equalTo(parentBottom - desiredPadding - child4.getHeight())))) + .check(screenRight(is(equalTo(parentLeft + desiredPadding + child4.getWidth())))) + .check(screenBottom(is(equalTo(parentBottom - desiredPadding)))); + + // Content 3 is width and height wrap_content + // the bottom and left edges should be offset from the screen edges by boxInset pixels + onView(withId(R.id.content4)).check( + screenBottom(equalTo(parentBottom - desiredPadding))) + .check(screenLeft(equalTo(parentLeft + desiredPadding))); + } + + @Test + public void testCase4() throws Throwable { + mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity + .EXTRA_LAYOUT_RESOURCE_ID, R.layout.box_inset_layout_testcase_4)); + DisplayMetrics dm = InstrumentationRegistry.getTargetContext().getResources() + .getDisplayMetrics(); + int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels)); + + int desiredPadding = 0; + if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) { + desiredPadding = boxInset; + } + + ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){ + @Override + public void run() { + View container = mActivityRule.getActivity().findViewById(R.id.container); + View child1 = mActivityRule.getActivity().findViewById(R.id.child1); + mIdViewMap.put(R.id.container, container); + mIdViewMap.put(R.id.child1, child1); + + } + }; + mActivityRule.runOnUiThread(customRunnable); + + View container = customRunnable.mIdViewMap.get(R.id.container); + View child1 = customRunnable.mIdViewMap.get(R.id.child1); + // Child 1 is match_parent width and wrap_content height + // layout_box=right|left + // Padding of boxInset should be added to the right and bottom sides only + onView(withId(R.id.child1)).check(left(equalTo(desiredPadding))).check( + top(equalTo(container.getTop()))).check( + right(equalTo(dm.widthPixels - desiredPadding))).check( + bottom(equalTo(container.getTop() + child1.getHeight()))); + } + + private abstract class ViewFetchingRunnable implements Runnable { + Map mIdViewMap = new HashMap<>(); + } +} diff --git a/android/support/wear/widget/CircularProgressLayoutControllerTest.java b/android/support/wear/widget/CircularProgressLayoutControllerTest.java new file mode 100644 index 00000000..2f625b6c --- /dev/null +++ b/android/support/wear/widget/CircularProgressLayoutControllerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.support.test.annotation.UiThreadTest; +import android.support.test.filters.LargeTest; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.widget.CircularProgressDrawable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.concurrent.TimeUnit; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class CircularProgressLayoutControllerTest { + + private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1); + private static final long UPDATE_INTERVAL = TimeUnit.MILLISECONDS.toMillis(30); + + private CircularProgressLayoutController mControllerUnderTest; + + @Mock + CircularProgressDrawable mMockDrawable; + @Mock + CircularProgressLayout mMockLayout; + @Mock + CircularProgressLayout.OnTimerFinishedListener mMockListener; + + @Before + public void setUp() { + mMockDrawable = mock(CircularProgressDrawable.class); + mMockLayout = mock(CircularProgressLayout.class); + mMockListener = mock(CircularProgressLayout.OnTimerFinishedListener.class); + when(mMockLayout.getProgressDrawable()).thenReturn(mMockDrawable); + when(mMockLayout.getOnTimerFinishedListener()).thenReturn(mMockListener); + mControllerUnderTest = new CircularProgressLayoutController(mMockLayout); + } + + @Test + public void testSetIndeterminate() { + mControllerUnderTest.setIndeterminate(true); + + assertEquals(true, mControllerUnderTest.isIndeterminate()); + verify(mMockDrawable).start(); + } + + @Test + public void testIsIndeterminateAfterSetToFalse() { + mControllerUnderTest.setIndeterminate(true); + mControllerUnderTest.setIndeterminate(false); + + assertEquals(false, mControllerUnderTest.isIndeterminate()); + verify(mMockDrawable).stop(); + } + + @LargeTest + @Test + @UiThreadTest + public void testIsTimerRunningAfterStart() { + mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL); + + assertEquals(true, mControllerUnderTest.isTimerRunning()); + } + + @Test + @UiThreadTest + public void testIsTimerRunningAfterStop() { + mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL); + mControllerUnderTest.stopTimer(); + + assertEquals(false, mControllerUnderTest.isTimerRunning()); + } + + @Test + @UiThreadTest + public void testSwitchFromIndeterminateToDeterminate() { + mControllerUnderTest.setIndeterminate(true); + mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL); + + assertEquals(false, mControllerUnderTest.isIndeterminate()); + assertEquals(true, mControllerUnderTest.isTimerRunning()); + verify(mMockDrawable).stop(); + } + + @Test + @UiThreadTest + public void testSwitchFromDeterminateToIndeterminate() { + mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL); + mControllerUnderTest.setIndeterminate(true); + + assertEquals(true, mControllerUnderTest.isIndeterminate()); + assertEquals(false, mControllerUnderTest.isTimerRunning()); + verify(mMockDrawable).start(); + } +} diff --git a/android/support/wear/widget/CircularProgressLayoutTest.java b/android/support/wear/widget/CircularProgressLayoutTest.java new file mode 100644 index 00000000..ff98c30c --- /dev/null +++ b/android/support/wear/widget/CircularProgressLayoutTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Intent; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.test.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class CircularProgressLayoutTest { + + private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1); + + @Rule + public final ActivityTestRule mActivityRule = new ActivityTestRule<>( + LayoutTestActivity.class, true, false); + private CircularProgressLayout mLayoutUnderTest; + + @Before + public void setUp() { + mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity + .EXTRA_LAYOUT_RESOURCE_ID, R.layout.circular_progress_layout)); + mLayoutUnderTest = mActivityRule.getActivity().findViewById(R.id.circular_progress_layout); + mLayoutUnderTest.setOnTimerFinishedListener(new FakeListener()); + } + + @Test + public void testListenerIsNotified() { + mLayoutUnderTest.setTotalTime(TOTAL_TIME); + startTimerOnUiThread(); + waitForTimer(TOTAL_TIME + 100); + assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener()); + assertTrue(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished); + } + + @Test + public void testListenerIsNotNotifiedWhenStopped() { + mLayoutUnderTest.setTotalTime(TOTAL_TIME); + startTimerOnUiThread(); + stopTimerOnUiThread(); + waitForTimer(TOTAL_TIME + 100); + assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener()); + assertFalse(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished); + } + + private class FakeListener implements CircularProgressLayout.OnTimerFinishedListener { + + boolean mFinished; + + @Override + public void onTimerFinished(CircularProgressLayout layout) { + mFinished = true; + } + } + + private void startTimerOnUiThread() { + mActivityRule.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLayoutUnderTest.startTimer(); + } + }); + } + + private void stopTimerOnUiThread() { + mActivityRule.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mLayoutUnderTest.stopTimer(); + } + }); + } + + private void waitForTimer(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/android/support/wear/widget/LayoutTestActivity.java b/android/support/wear/widget/LayoutTestActivity.java new file mode 100644 index 00000000..ec909dbf --- /dev/null +++ b/android/support/wear/widget/LayoutTestActivity.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class LayoutTestActivity extends Activity { + public static final String EXTRA_LAYOUT_RESOURCE_ID = "layout_resource_id"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + if (!intent.hasExtra(EXTRA_LAYOUT_RESOURCE_ID)) { + throw new IllegalArgumentException( + "Intent extras must contain EXTRA_LAYOUT_RESOURCE_ID"); + } + int layoutId = intent.getIntExtra(EXTRA_LAYOUT_RESOURCE_ID, -1); + setContentView(layoutId); + } +} diff --git a/android/support/wear/widget/RoundedDrawableTest.java b/android/support/wear/widget/RoundedDrawableTest.java new file mode 100644 index 00000000..b01b3fad --- /dev/null +++ b/android/support/wear/widget/RoundedDrawableTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.wear.widget; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.test.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Tests for {@link RoundedDrawable} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RoundedDrawableTest { + + @Rule + public final ActivityTestRule mActivityRule = new ActivityTestRule<>( + LayoutTestActivity.class, true, false); + private static final int BITMAP_WIDTH = 64; + private static final int BITMAP_HEIGHT = 32; + + private RoundedDrawable mRoundedDrawable; + private BitmapDrawable mBitmapDrawable; + + @Mock + Canvas mMockCanvas; + + @Before + public void setUp() { + mMockCanvas = mock(Canvas.class); + mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity + .EXTRA_LAYOUT_RESOURCE_ID, + android.support.wear.test.R.layout.rounded_drawable_layout)); + mRoundedDrawable = new RoundedDrawable(); + mBitmapDrawable = + new BitmapDrawable( + mActivityRule.getActivity().getResources(), + Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888)); + } + + @Test + public void colorFilterIsAppliedCorrectly() { + ColorFilter cf = new ColorFilter(); + mRoundedDrawable.setColorFilter(cf); + assertEquals(cf, mRoundedDrawable.mPaint.getColorFilter()); + } + + @Test + public void alphaIsAppliedCorrectly() { + int alpha = 128; + mRoundedDrawable.setAlpha(alpha); + assertEquals(alpha, mRoundedDrawable.mPaint.getAlpha()); + } + + @Test + public void radiusIsAppliedCorrectly() { + int radius = 10; + Rect bounds = new Rect(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT); + mRoundedDrawable.setDrawable(mBitmapDrawable); + mRoundedDrawable.setClipEnabled(true); + mRoundedDrawable.setRadius(radius); + mRoundedDrawable.setBounds(bounds); + mRoundedDrawable.draw(mMockCanvas); + // One for background and one for the actual drawable, this should be called two times. + verify(mMockCanvas, times(2)) + .drawRoundRect( + eq(new RectF(0, 0, bounds.width(), bounds.height())), + eq((float) radius), + eq((float) radius), + any(Paint.class)); + } + + @Test + public void scalingIsAppliedCorrectly() { + int radius = 14; + // 14 px radius should apply 5 px padding due to formula ceil(radius * 1 - 1 / sqrt(2)) + Rect bounds = new Rect(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT); + mRoundedDrawable.setDrawable(mBitmapDrawable); + mRoundedDrawable.setClipEnabled(false); + mRoundedDrawable.setRadius(radius); + mRoundedDrawable.setBounds(bounds); + mRoundedDrawable.draw(mMockCanvas); + assertEquals(BITMAP_WIDTH - 10, mBitmapDrawable.getBounds().width()); + assertEquals(BITMAP_HEIGHT - 10, mBitmapDrawable.getBounds().height()); + assertEquals(bounds.centerX(), mBitmapDrawable.getBounds().centerX()); + assertEquals(bounds.centerY(), mBitmapDrawable.getBounds().centerY()); + // Background should also be drawn + verify(mMockCanvas) + .drawRoundRect( + eq(new RectF(0, 0, bounds.width(), bounds.height())), + eq((float) radius), + eq((float) radius), + any(Paint.class)); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void inflate() { + RoundedDrawable roundedDrawable = + (RoundedDrawable) mActivityRule.getActivity().getDrawable( + R.drawable.rounded_drawable); + assertEquals( + mActivityRule.getActivity().getColor(R.color.rounded_drawable_background_color), + roundedDrawable.getBackgroundColor()); + assertTrue(roundedDrawable.isClipEnabled()); + assertNotNull(roundedDrawable.getDrawable()); + assertEquals(mActivityRule.getActivity().getResources().getDimensionPixelSize( + R.dimen.rounded_drawable_radius), roundedDrawable.getRadius()); + } +} diff --git a/android/support/wear/widget/ScrollManagerTest.java b/android/support/wear/widget/ScrollManagerTest.java new file mode 100644 index 00000000..34faea3f --- /dev/null +++ b/android/support/wear/widget/ScrollManagerTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import static java.lang.Math.cos; +import static java.lang.Math.sin; + +import android.os.SystemClock; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.widget.util.WakeLockRule; +import android.view.MotionEvent; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class ScrollManagerTest { + private static final int TEST_WIDTH = 400; + private static final int TEST_HEIGHT = 400; + private static final int STEP_COUNT = 300; + + private static final int EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE = 36; + private static final int EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE = 199; + + @Rule + public final WakeLockRule wakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = + new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true); + + @Mock + WearableRecyclerView mMockWearableRecyclerView; + + ScrollManager mScrollManagerUnderTest; + + @Before + public void setUp() throws Throwable { + MockitoAnnotations.initMocks(this); + mScrollManagerUnderTest = new ScrollManager(); + mScrollManagerUnderTest.setRecyclerView(mMockWearableRecyclerView, TEST_WIDTH, TEST_HEIGHT); + } + + @Test + public void testStraightUpScrollingGestureLeft() throws Throwable { + // Pretend to scroll in a straight line from center left to upper left + scroll(mScrollManagerUnderTest, 30, 30, 200, 150); + // The scroll manager should require the recycler view to scroll up and only up + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE)) + .scrollBy(0, 1); + } + + @Test + public void testStraightDownScrollingGestureLeft() throws Throwable { + // Pretend to scroll in a straight line upper left to center left + scroll(mScrollManagerUnderTest, 30, 30, 150, 200); + // The scroll manager should require the recycler view to scroll down and only down + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE)) + .scrollBy(0, -1); + } + + @Test + public void testStraightUpScrollingGestureRight() throws Throwable { + // Pretend to scroll in a straight line from center right to upper right + scroll(mScrollManagerUnderTest, 370, 370, 200, 150); + // The scroll manager should require the recycler view to scroll down and only down + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE)) + .scrollBy(0, -1); + } + + @Test + public void testStraightDownScrollingGestureRight() throws Throwable { + // Pretend to scroll in a straight line upper right to center right + scroll(mScrollManagerUnderTest, 370, 370, 150, 200); + // The scroll manager should require the recycler view to scroll up and only up + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE)) + .scrollBy(0, 1); + } + + @Test + public void testCircularScrollingGestureLeft() throws Throwable { + // Pretend to scroll in an arch from center left to center right + scrollOnArch(mScrollManagerUnderTest, 30, 200, 180.0f); + // The scroll manager should never reverse the scroll direction and scroll up + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE)) + .scrollBy(0, 1); + } + + @Test + public void testCircularScrollingGestureRight() throws Throwable { + // Pretend to scroll in an arch from center left to center right + scrollOnArch(mScrollManagerUnderTest, 370, 200, -180.0f); + // The scroll manager should never reverse the scroll direction and scroll down. + verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE)) + .scrollBy(0, -1); + } + + private static void scroll(ScrollManager scrollManager, float fromX, float toX, float fromY, + float toY) { + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + float y = fromY; + float x = fromX; + + float yStep = (toY - fromY) / STEP_COUNT; + float xStep = (toX - fromX) / STEP_COUNT; + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + scrollManager.onTouchEvent(event); + for (int i = 0; i < STEP_COUNT; ++i) { + y += yStep; + x += xStep; + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); + scrollManager.onTouchEvent(event); + } + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + scrollManager.onTouchEvent(event); + } + + private static void scrollOnArch(ScrollManager scrollManager, float fromX, float fromY, + float deltaAngle) { + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + + float stepAngle = deltaAngle / STEP_COUNT; + double relativeX = fromX - (TEST_WIDTH / 2); + double relativeY = fromY - (TEST_HEIGHT / 2); + float radius = (float) Math.sqrt(relativeX * relativeX + relativeY * relativeY); + float angle = getAngle(fromX, fromY, TEST_WIDTH, TEST_HEIGHT); + + float y = fromY; + float x = fromX; + + MotionEvent event = MotionEvent.obtain(downTime, eventTime, + MotionEvent.ACTION_DOWN, x, y, 0); + scrollManager.onTouchEvent(event); + for (int i = 0; i < STEP_COUNT; ++i) { + angle += stepAngle; + x = getX(angle, radius, TEST_WIDTH); + y = getY(angle, radius, TEST_HEIGHT); + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0); + scrollManager.onTouchEvent(event); + } + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + scrollManager.onTouchEvent(event); + } + + private static float getX(double angle, double radius, double viewWidth) { + double radianAngle = Math.toRadians(angle - 90); + double relativeX = cos(radianAngle) * radius; + return (float) (relativeX + (viewWidth / 2)); + } + + private static float getY(double angle, double radius, double viewHeight) { + double radianAngle = Math.toRadians(angle - 90); + double relativeY = sin(radianAngle) * radius; + return (float) (relativeY + (viewHeight / 2)); + } + + private static float getAngle(double x, double y, double viewWidth, double viewHeight) { + double relativeX = x - (viewWidth / 2); + double relativeY = y - (viewHeight / 2); + double rowAngle = Math.atan2(relativeX, relativeY); + double angle = -Math.toDegrees(rowAngle) - 180; + if (angle < 0) { + angle += 360; + } + return (float) angle; + } +} diff --git a/android/support/wear/widget/SwipeDismissFrameLayoutTest.java b/android/support/wear/widget/SwipeDismissFrameLayoutTest.java new file mode 100644 index 00000000..e4e47aaf --- /dev/null +++ b/android/support/wear/widget/SwipeDismissFrameLayoutTest.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.swipeRight; +import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView; +import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.RectF; +import android.support.annotation.IdRes; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.GeneralSwipeAction; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Swipe; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v7.widget.RecyclerView; +import android.support.wear.test.R; +import android.support.wear.widget.util.ArcSwipe; +import android.support.wear.widget.util.WakeLockRule; +import android.view.View; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SwipeDismissFrameLayoutTest { + + private static final long MAX_WAIT_TIME = 4000; //ms + private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback(); + + @Rule + public final WakeLockRule wakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule activityRule = + new ActivityTestRule<>( + SwipeDismissFrameLayoutTestActivity.class, + true, /** initial touch mode */ + false /** launchActivity */ + ); + + private int mLayoutWidth; + private int mLayoutHeight; + + @Test + @SmallTest + public void testCanScrollHorizontally() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + SwipeDismissFrameLayout testLayout = + (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); + // WHEN we check that the layout is horizontally scrollable from left to right. + // THEN the layout is found to be horizontally swipeable from left to right. + assertTrue(testLayout.canScrollHorizontally(-20)); + // AND the layout is found to NOT be horizontally swipeable from right to left. + assertFalse(testLayout.canScrollHorizontally(20)); + + // WHEN we switch off the swipe-to-dismiss functionality for the layout + testLayout.setSwipeable(false); + // THEN the layout is found NOT to be horizontally swipeable from left to right. + assertFalse(testLayout.canScrollHorizontally(-20)); + // AND the layout is found to NOT be horizontally swipeable from right to left. + assertFalse(testLayout.canScrollHorizontally(20)); + } + + @Test + @SmallTest + public void canScrollHorizontallyShouldBeFalseWhenInvisible() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); + // GIVEN the layout is invisible + // Note: We have to run this on the main thread, because of thread checks in View.java. + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + testLayout.setVisibility(View.INVISIBLE); + } + }); + // WHEN we check that the layout is horizontally scrollable + // THEN the layout is found to be NOT horizontally swipeable from left to right. + assertFalse(testLayout.canScrollHorizontally(-20)); + // AND the layout is found to NOT be horizontally swipeable from right to left. + assertFalse(testLayout.canScrollHorizontally(20)); + } + + @Test + @SmallTest + public void canScrollHorizontallyShouldBeFalseWhenGone() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + final SwipeDismissFrameLayout testLayout = + (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); + // GIVEN the layout is gone + // Note: We have to run this on the main thread, because of thread checks in View.java. + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + testLayout.setVisibility(View.GONE); + } + }); + // WHEN we check that the layout is horizontally scrollable + // THEN the layout is found to be NOT horizontally swipeable from left to right. + assertFalse(testLayout.canScrollHorizontally(-20)); + // AND the layout is found to NOT be horizontally swipeable from right to left. + assertFalse(testLayout.canScrollHorizontally(20)); + } + + @Test + @SmallTest + public void testSwipeDismissEnabledByDefault() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + SwipeDismissFrameLayout testLayout = + (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); + // WHEN we check that the layout is dismissible + // THEN the layout is find to be dismissible + assertTrue(testLayout.isSwipeable()); + } + + @Test + @SmallTest + public void testSwipeDismissesViewIfEnabled() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + // WHEN we perform a swipe to dismiss + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); + // AND hidden + assertHidden(R.id.swipe_dismiss_root); + } + + @Test + @SmallTest + public void testSwipeDoesNotDismissViewIfDisabled() { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + SwipeDismissFrameLayout testLayout = + (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); + testLayout.setSwipeable(false); + // WHEN we perform a swipe to dismiss + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); + // THEN the layout is not hidden + assertNotHidden(R.id.swipe_dismiss_root); + } + + @Test + @SmallTest + public void testAddRemoveCallback() { + // GIVEN a freshly setup SwipeDismissFrameLayout + setUpSimpleLayout(); + Activity activity = activityRule.getActivity(); + SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); + // WHEN we remove the swipe callback + testLayout.removeCallback(mDismissCallback); + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); + // THEN the layout is not hidden + assertNotHidden(R.id.swipe_dismiss_root); + } + + @Test + @SmallTest + public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. + setUpSwipeDismissWithHorizontalRecyclerView(); + activityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + Activity activity = activityRule.getActivity(); + RecyclerView testLayout = activity.findViewById(R.id.recycler_container); + // Scroll to a position from which the child is scrollable. + testLayout.scrollToPosition(50); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + // WHEN we perform a swipe to dismiss from the center of the screen. + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); + // THEN the layout is not hidden + assertNotHidden(R.id.swipe_dismiss_root); + } + + + @Test + @SmallTest + public void testEdgeSwipeDoesDismissViewIfScrollable() { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. + setUpSwipeDismissWithHorizontalRecyclerView(); + // WHEN we perform a swipe to dismiss from the left edge of the screen. + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); + // THEN the layout is hidden + assertHidden(R.id.swipe_dismiss_root); + } + + @Test + @SmallTest + public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an + // inner circle. + setUpSwipeableRegion(); + // WHEN we perform a swipe to dismiss from the left edge of the screen. + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); + // THEN the layout is not not hidden + assertNotHidden(R.id.swipe_dismiss_root); + } + + @Test + @SmallTest + public void testSwipeDoesDismissViewIfStartsInRightPosition() { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an + // inner circle. + setUpSwipeableRegion(); + // WHEN we perform a swipe to dismiss from the center of the screen. + onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); + // THEN the layout is hidden + assertHidden(R.id.swipe_dismiss_root); + } + + /** + @Test public void testSwipeInPreferenceFragmentAndNavDrawer() { + // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner + // circle. + setUpPreferenceFragmentAndNavDrawer(); + // WHEN we perform a swipe to dismiss from the center of the screen to the bottom. + onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter()); + // THEN the navigation drawer is shown. + assertPeeking(R.id.top_drawer); + }*/ + + @Test + @SmallTest + public void testArcSwipeDoesNotTriggerDismiss() throws Throwable { + // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content + setUpSwipeDismissWithVerticalRecyclerView(); + int center = mLayoutHeight / 2; + int halfBound = mLayoutWidth / 2; + RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound); + // WHEN the view is scrolled on an arc from top to bottom. + onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds)); + // THEN the layout is not dismissed and not hidden. + assertNotHidden(R.id.swipe_dismiss_root); + // AND the content view is scrolled. + assertScrolledY(R.id.recycler_container); + } + + /** + * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with + * a single static child. + */ + private void setUpSimpleLayout() { + activityRule.launchActivity( + new Intent() + .putExtra( + LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.swipe_dismiss_layout_testcase_1)); + setDismissCallback(); + } + + + /** + * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable + * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link + * android.support.v7.widget.RecyclerView} as a child, ready to accept an adapter. + */ + private void setUpSwipeDismissWithHorizontalRecyclerView() { + Intent launchIntent = new Intent(); + launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.swipe_dismiss_layout_testcase_2); + launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true); + activityRule.launchActivity(launchIntent); + setDismissCallback(); + } + + /** + * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable + * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link + * WearableRecyclerView} as a child, ready to accept an adapter. + */ + private void setUpSwipeDismissWithVerticalRecyclerView() { + Intent launchIntent = new Intent(); + launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.swipe_dismiss_layout_testcase_2); + launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false); + activityRule.launchActivity(launchIntent); + setDismissCallback(); + } + + /** + * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react + * to swipe-dismiss gestures. + */ + private void setUpSwipeableRegion() { + activityRule.launchActivity( + new Intent() + .putExtra( + LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.swipe_dismiss_layout_testcase_1)); + setCallback( + new DismissCallback() { + @Override + public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x, + float y) { + float normalizedX = x - mLayoutWidth / 2; + float normalizedY = y - mLayoutWidth / 2; + float squareX = normalizedX * normalizedX; + float squareY = normalizedY * normalizedY; + // 30 is an arbitrary number limiting the circle. + return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30); + } + }); + } + + /** + * Sets up a more involved test case where the layout consists of a + * {@code WearableNavigationDrawer} and a + * {@code android.support.wear.internal.view.SwipeDismissPreferenceFragment} + */ + /* + private void setUpPreferenceFragmentAndNavDrawer() { + activityRule.launchActivity( + new Intent() + .putExtra( + LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, + R.layout.swipe_dismiss_layout_testcase_3)); + Activity activity = activityRule.getActivity(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + WearableNavigationDrawer wearableNavigationDrawer = + (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer); + wearableNavigationDrawer.setAdapter( + new WearableNavigationDrawer.WearableNavigationDrawerAdapter() { + @Override + public String getItemText(int pos) { + return "test"; + } + + @Override + public Drawable getItemDrawable(int pos) { + return null; + } + + @Override + public void onItemSelected(int pos) { + return; + } + + @Override + public int getCount() { + return 3; + } + }); + }); + }*/ + private void setDismissCallback() { + setCallback(mDismissCallback); + } + + private void setCallback(SwipeDismissFrameLayout.Callback callback) { + Activity activity = activityRule.getActivity(); + SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); + mLayoutWidth = testLayout.getWidth(); + mLayoutHeight = testLayout.getHeight(); + testLayout.addCallback(callback); + } + + /** + * private static void assertPeeking(@IdRes int layoutId) { + * onView(withId(layoutId)) + * .perform( + * waitForMatchingView( + * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME)); + * } + */ + + private static void assertHidden(@IdRes int layoutId) { + onView(withId(layoutId)) + .perform( + waitForMatchingView( + allOf(withId(layoutId), + withEffectiveVisibility(ViewMatchers.Visibility.GONE)), + MAX_WAIT_TIME)); + } + + private static void assertNotHidden(@IdRes int layoutId) { + onView(withId(layoutId)) + .perform( + waitForMatchingView( + allOf(withId(layoutId), + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)), + MAX_WAIT_TIME)); + } + + private static void assertScrolledY(@IdRes int layoutId) { + onView(withId(layoutId)) + .perform( + waitForMatchingView( + allOf(withId(layoutId), withPositiveVerticalScrollOffset()), + MAX_WAIT_TIME)); + } + + private static ViewAction swipeRightFromCenter() { + return new GeneralSwipeAction( + Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER); + } + + private static ViewAction swipeRightFromLeftEdge() { + return new GeneralSwipeAction( + Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT, + Press.FINGER); + } + + private static ViewAction swipeTopFromBottomOnArc(RectF bounds) { + return new GeneralSwipeAction( + new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds), + GeneralLocation.BOTTOM_CENTER, + GeneralLocation.TOP_CENTER, + Press.FINGER); + } + + /** Helper class hiding the view after a successful swipe-to-dismiss. */ + private static class DismissCallback extends SwipeDismissFrameLayout.Callback { + + @Override + public void onDismissed(SwipeDismissFrameLayout layout) { + layout.setVisibility(View.GONE); + } + } +} diff --git a/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java b/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java new file mode 100644 index 00000000..5d868324 --- /dev/null +++ b/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.wear.test.R; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class SwipeDismissFrameLayoutTestActivity extends LayoutTestActivity { + + public static final String EXTRA_LAYOUT_HORIZONTAL = "layout_horizontal"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + int layoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID, -1); + boolean horizontal = getIntent().getBooleanExtra(EXTRA_LAYOUT_HORIZONTAL, false); + + if (layoutId == R.layout.swipe_dismiss_layout_testcase_2) { + createScrollableContent(horizontal); + } + } + + private void createScrollableContent(boolean horizontal) { + RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_container); + if (recyclerView == null) { + throw new NullPointerException("There has to be a relevant container defined"); + } + recyclerView.setLayoutManager( + new LinearLayoutManager( + this, + horizontal ? LinearLayoutManager.HORIZONTAL : LinearLayoutManager.VERTICAL, + false)); + recyclerView.setAdapter(new MyRecyclerViewAdapter()); + } + + private static class MyRecyclerViewAdapter + extends RecyclerView.Adapter { + @Override + public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView textView = new TextView(parent.getContext()); + textView.setText("A LOT OF TEXT VIEW"); + textView.setGravity(Gravity.CENTER_VERTICAL); + return new CustomViewHolder(textView); + } + + @Override + public void onBindViewHolder(CustomViewHolder holder, int position) { + } + + @Override + public int getItemCount() { + return 100; + } + + static class CustomViewHolder extends RecyclerView.ViewHolder { + + CustomViewHolder(View view) { + super(view); + } + } + } +} diff --git a/android/support/wear/widget/SwipeDismissPreferenceFragment.java b/android/support/wear/widget/SwipeDismissPreferenceFragment.java new file mode 100644 index 00000000..a892cb68 --- /dev/null +++ b/android/support/wear/widget/SwipeDismissPreferenceFragment.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.support.wear.widget.SwipeDismissFrameLayout.Callback; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * {@link PreferenceFragment} that supports swipe-to-dismiss. + * + *

    Unlike a regular PreferenceFragment, this Fragment has a solid color background using the + * background color from the theme. This allows the fragment to be layered on top of other + * fragments so that the previous layer is seen when this fragment is swiped away. + */ +public class SwipeDismissPreferenceFragment extends PreferenceFragment { + + private SwipeDismissFrameLayout mSwipeLayout; + + private final Callback mCallback = + new Callback() { + @Override + public void onSwipeStarted(SwipeDismissFrameLayout layout) { + SwipeDismissPreferenceFragment.this.onSwipeStart(); + } + + @Override + public void onSwipeCanceled(SwipeDismissFrameLayout layout) { + SwipeDismissPreferenceFragment.this.onSwipeCancelled(); + } + + @Override + public void onDismissed(SwipeDismissFrameLayout layout) { + SwipeDismissPreferenceFragment.this.onDismiss(); + } + }; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + mSwipeLayout = new SwipeDismissFrameLayout(getActivity()); + mSwipeLayout.addCallback(mCallback); + + View contents = super.onCreateView(inflater, mSwipeLayout, savedInstanceState); + + mSwipeLayout.setBackgroundColor(getBackgroundColor()); + mSwipeLayout.addView(contents); + + return mSwipeLayout; + } + + /** Called when the fragment is dismissed with a swipe. */ + public void onDismiss() { + } + + /** Called when a swipe-to-dismiss gesture is started. */ + public void onSwipeStart() { + } + + /** Called when a swipe-to-dismiss gesture is cancelled. */ + public void onSwipeCancelled() { + } + + /** + * Sets whether or not the preferences list can be focused. If {@code focusable} is false, any + * existing focus will be cleared. + */ + public void setFocusable(boolean focusable) { + if (focusable) { + mSwipeLayout.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + mSwipeLayout.setFocusable(true); + } else { + // Prevent any child views from receiving focus. + mSwipeLayout.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + + mSwipeLayout.setFocusable(false); + mSwipeLayout.clearFocus(); + } + } + + private int getBackgroundColor() { + TypedValue value = new TypedValue(); + getActivity().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true); + return value.data; + } +} diff --git a/android/support/wear/widget/WearableLinearLayoutManagerTest.java b/android/support/wear/widget/WearableLinearLayoutManagerTest.java new file mode 100644 index 00000000..49da7b21 --- /dev/null +++ b/android/support/wear/widget/WearableLinearLayoutManagerTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static org.junit.Assert.assertEquals; + +import android.app.Activity; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.wear.test.R; +import android.support.wear.widget.util.WakeLockRule; +import android.view.View; +import android.widget.FrameLayout; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicReference; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class WearableLinearLayoutManagerTest { + + @Rule + public final WakeLockRule wakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = + new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true); + + WearableLinearLayoutManager mWearableLinearLayoutManagerUnderTest; + + @Before + public void setUp() throws Throwable { + Activity activity = mActivityRule.getActivity(); + CurvingLayoutCallback mCurvingCallback = new CurvingLayoutCallback(activity); + mCurvingCallback.setOffset(10); + mWearableLinearLayoutManagerUnderTest = + new WearableLinearLayoutManager(mActivityRule.getActivity(), mCurvingCallback); + } + + @Test + public void testRoundOffsetting() throws Throwable { + ((CurvingLayoutCallback) mWearableLinearLayoutManagerUnderTest.getLayoutCallback()) + .setRound(true); + final AtomicReference wrvReference = new AtomicReference<>(); + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + // Set a fixed layout so that the test adapts to different device screens. + wrv.setLayoutParams(new FrameLayout.LayoutParams(390, 390)); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setLayoutManager(mWearableLinearLayoutManagerUnderTest); + wrvReference.set(wrv); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + WearableRecyclerView wrv = wrvReference.get(); + + View child1 = wrv.getChildAt(0); + View child2 = wrv.getChildAt(1); + View child3 = wrv.getChildAt(2); + View child4 = wrv.getChildAt(3); + View child5 = wrv.getChildAt(4); + + // The left position and the translation of the child is modified if the screen is round. + // Check if the 5th child is not null as some devices will not be able to display 5 views. + assertEquals(136, child1.getLeft()); + assertEquals(-6.3, child1.getTranslationY(), 0.1); + + assertEquals(91, child2.getLeft(), 1); + assertEquals(-15.21, child2.getTranslationY(), 0.1); + + assertEquals(58, child3.getLeft(), 1); + assertEquals(-13.5, child3.getTranslationY(), 0.1); + + assertEquals(42, child4.getLeft(), 1); + assertEquals(-4.5, child4.getTranslationY(), 0.1); + + if (child5 != null) { + assertEquals(43, child5.getLeft(), 1); + assertEquals(6.7, child5.getTranslationY(), 0.1); + } + } + + @Test + public void testStraightOffsetting() throws Throwable { + ((CurvingLayoutCallback) mWearableLinearLayoutManagerUnderTest.getLayoutCallback()) + .setRound( + false); + final AtomicReference wrvReference = new AtomicReference<>(); + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setLayoutManager(mWearableLinearLayoutManagerUnderTest); + wrvReference.set(wrv); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + WearableRecyclerView wrv = wrvReference.get(); + + View child1 = wrv.getChildAt(0); + View child2 = wrv.getChildAt(1); + View child3 = wrv.getChildAt(2); + View child4 = wrv.getChildAt(3); + View child5 = wrv.getChildAt(4); + + // The left position and the translation of the child is not modified if the screen is + // straight. Check if the 5th child is not null as some devices will not be able to display + // 5 views. + assertEquals(0, child1.getLeft()); + assertEquals(0.0f, child1.getTranslationY(), 0); + + assertEquals(0, child2.getLeft()); + assertEquals(0.0f, child2.getTranslationY(), 0); + + assertEquals(0, child3.getLeft()); + assertEquals(0.0f, child3.getTranslationY(), 0); + + assertEquals(0, child4.getLeft()); + assertEquals(0.0f, child4.getTranslationY(), 0); + + if (child5 != null) { + assertEquals(0, child5.getLeft()); + assertEquals(0.0f, child5.getTranslationY(), 0); + } + } +} diff --git a/android/support/wear/widget/WearableRecyclerViewTest.java b/android/support/wear/widget/WearableRecyclerViewTest.java new file mode 100644 index 00000000..5c176386 --- /dev/null +++ b/android/support/wear/widget/WearableRecyclerViewTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView; +import static android.support.wear.widget.util.MoreViewAssertions.withNoVerticalScrollOffset; +import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.support.annotation.IdRes; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.GeneralSwipeAction; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Swipe; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v7.widget.RecyclerView; +import android.support.wear.test.R; +import android.support.wear.widget.util.WakeLockRule; +import android.view.View; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class WearableRecyclerViewTest { + + private static final long MAX_WAIT_TIME = 10000; + @Mock + WearableRecyclerView.LayoutManager mMockChildLayoutManager; + + @Rule + public final WakeLockRule wakeLock = new WakeLockRule(); + + @Rule + public final ActivityTestRule mActivityRule = + new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testCaseInitState() { + WearableRecyclerView wrv = new WearableRecyclerView(mActivityRule.getActivity()); + wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext())); + + assertFalse(wrv.isEdgeItemsCenteringEnabled()); + assertFalse(wrv.isCircularScrollingGestureEnabled()); + assertEquals(1.0f, wrv.getBezelFraction(), 0.01f); + assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f); + } + + @Test + public void testEdgeItemsCenteringOnAndOff() throws Throwable { + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setEdgeItemsCenteringEnabled(true); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + View child = wrv.getChildAt(0); + assertNotNull("child", child); + assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop()); + } + }); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setEdgeItemsCenteringEnabled(false); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + View child = wrv.getChildAt(0); + assertNotNull("child", child); + assertEquals(0, child.getTop()); + + } + }); + } + + @Test + public void testEdgeItemsCenteringBeforeChildrenDrawn() throws Throwable { + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + Activity activity = mActivityRule.getActivity(); + WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv); + RecyclerView.Adapter adapter = wrv.getAdapter(); + wrv.setAdapter(null); + wrv.setEdgeItemsCenteringEnabled(true); + wrv.setAdapter(adapter); + } + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + // Verify the first child + View child = wrv.getChildAt(0); + assertNotNull("child", child); + assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop()); + } + }); + } + + @Test + public void testCircularScrollingGesture() throws Throwable { + onView(withId(R.id.wrv)).perform(swipeDownFromTopRight()); + assertNotScrolledY(R.id.wrv); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setCircularScrollingGestureEnabled(true); + } + }); + + onView(withId(R.id.wrv)).perform(swipeDownFromTopRight()); + assertScrolledY(R.id.wrv); + } + + @Test + public void testCurvedOffsettingHelper() throws Throwable { + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + WearableRecyclerView wrv = + (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv); + wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext())); + } + }); + + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + onView(withId(R.id.wrv)).perform(swipeDownFromTopRight()); + + mActivityRule.runOnUiThread(new Runnable() { + @Override + public void run() { + Activity activity = mActivityRule.getActivity(); + WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv); + if (activity.getResources().getConfiguration().isScreenRound()) { + View child = wrv.getChildAt(0); + assertTrue(child.getLeft() > 0); + } else { + for (int i = 0; i < wrv.getChildCount(); i++) { + assertEquals(0, wrv.getChildAt(i).getLeft()); + } + } + } + }); + } + + private static ViewAction swipeDownFromTopRight() { + return new GeneralSwipeAction( + Swipe.FAST, GeneralLocation.TOP_RIGHT, GeneralLocation.BOTTOM_RIGHT, + Press.FINGER); + } + + private void assertScrolledY(@IdRes int layoutId) { + onView(withId(layoutId)).perform(waitForMatchingView( + allOf(withId(layoutId), withPositiveVerticalScrollOffset()), MAX_WAIT_TIME)); + } + + private void assertNotScrolledY(@IdRes int layoutId) { + onView(withId(layoutId)).perform(waitForMatchingView( + allOf(withId(layoutId), withNoVerticalScrollOffset()), MAX_WAIT_TIME)); + } +} diff --git a/android/support/wear/widget/WearableRecyclerViewTestActivity.java b/android/support/wear/widget/WearableRecyclerViewTestActivity.java new file mode 100644 index 00000000..2329fc51 --- /dev/null +++ b/android/support/wear/widget/WearableRecyclerViewTestActivity.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.support.wear.test.R; +import android.view.ViewGroup; +import android.widget.TextView; + +public class WearableRecyclerViewTestActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.wearable_recycler_view_basic); + WearableRecyclerView wrv = findViewById(R.id.wrv); + wrv.setLayoutManager(new WearableLinearLayoutManager(this)); + wrv.setAdapter(new TestAdapter()); + } + + private class ViewHolder extends RecyclerView.ViewHolder { + TextView mView; + ViewHolder(TextView itemView) { + super(itemView); + mView = itemView; + } + } + + private class TestAdapter extends WearableRecyclerView.Adapter { + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView view = new TextView(parent.getContext()); + view.setLayoutParams(new RecyclerView.LayoutParams(200, 50)); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.mView.setText("holder at position " + position); + holder.mView.setTag(position); + } + + @Override + public int getItemCount() { + return 100; + } + } +} diff --git a/android/support/wear/widget/drawer/DrawerTestActivity.java b/android/support/wear/widget/drawer/DrawerTestActivity.java new file mode 100644 index 00000000..414b97b9 --- /dev/null +++ b/android/support/wear/widget/drawer/DrawerTestActivity.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.drawer; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.IntDef; +import android.support.wear.test.R; +import android.support.wear.widget.drawer.WearableDrawerLayout.DrawerStateCallback; +import android.support.wear.widget.drawer.WearableNavigationDrawerView.WearableNavigationDrawerAdapter; +import android.util.ArrayMap; +import android.view.Gravity; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +/** + * Test {@link Activity} for {@link WearableDrawerLayout} and implementations of {@link + * android.support.wear.widget.drawer.WearableDrawerView}. + */ +public class DrawerTestActivity extends Activity { + + private static final int DRAWER_SIZE = 5; + private static final String STYLE_EXTRA = "style"; + private static final String OPEN_TOP_IN_ONCREATE_EXTRA = "openTopInOnCreate"; + private static final String OPEN_BOTTOM_IN_ONCREATE_EXTRA = "openBottomInOnCreate"; + private static final String CLOSE_FIRST_DRAWER_OPENED = "closeFirstDrawerOpened"; + private static final Map STYLE_TO_RES_ID = new ArrayMap<>(); + + static { + STYLE_TO_RES_ID.put( + DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE, + R.layout.test_multi_page_nav_drawer_layout); + STYLE_TO_RES_ID.put( + DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE, + R.layout.test_single_page_nav_drawer_layout); + STYLE_TO_RES_ID.put( + DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE, + R.layout.test_only_action_drawer_with_title_layout); + + } + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private final WearableNavigationDrawerAdapter mDrawerAdapter = + new WearableNavigationDrawerAdapter() { + @Override + public String getItemText(int pos) { + return Integer.toString(pos); + } + + @Override + public Drawable getItemDrawable(int pos) { + return getDrawable(android.R.drawable.star_on); + } + + @Override + public int getCount() { + return DRAWER_SIZE; + } + }; + private WearableActionDrawerView mActionDrawer; + private WearableDrawerLayout mDrawerLayout; + private WearableNavigationDrawerView mNavigationDrawer; + private final Runnable mCloseTopDrawerRunnable = + new Runnable() { + @Override + public void run() { + mNavigationDrawer.getController().closeDrawer(); + } + }; + private final DrawerStateCallback mCloseFirstDrawerOpenedCallback = + new DrawerStateCallback() { + @Override + public void onDrawerOpened(WearableDrawerLayout layout, + WearableDrawerView drawerView) { + mMainThreadHandler.postDelayed(mCloseTopDrawerRunnable, 1000); + } + }; + @DrawerStyle private int mNavigationStyle; + private boolean mOpenTopDrawerInOnCreate; + private boolean mOpenBottomDrawerInOnCreate; + private boolean mCloseFirstDrawerOpened; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + parseIntent(getIntent()); + + setContentView(STYLE_TO_RES_ID.get(mNavigationStyle)); + + mDrawerLayout = (WearableDrawerLayout) findViewById(R.id.drawer_layout); + mNavigationDrawer = (WearableNavigationDrawerView) findViewById(R.id.navigation_drawer); + mActionDrawer = (WearableActionDrawerView) findViewById(R.id.action_drawer); + + if (mCloseFirstDrawerOpened) { + mDrawerLayout.setDrawerStateCallback(mCloseFirstDrawerOpenedCallback); + } + + if (mNavigationDrawer != null) { + mNavigationDrawer.setAdapter(mDrawerAdapter); + if (mOpenTopDrawerInOnCreate) { + mDrawerLayout.openDrawer(Gravity.TOP); + } else { + mDrawerLayout.peekDrawer(Gravity.TOP); + } + } + + if (mActionDrawer != null) { + if (mOpenBottomDrawerInOnCreate) { + mDrawerLayout.openDrawer(Gravity.BOTTOM); + } else { + mDrawerLayout.peekDrawer(Gravity.BOTTOM); + } + } + } + + private void parseIntent(Intent intent) { + //noinspection WrongConstant - Linter doesn't know intent contains a NavigationStyle + mNavigationStyle = intent.getIntExtra(STYLE_EXTRA, DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE); + mOpenTopDrawerInOnCreate = intent.getBooleanExtra(OPEN_TOP_IN_ONCREATE_EXTRA, false); + mOpenBottomDrawerInOnCreate = intent.getBooleanExtra(OPEN_BOTTOM_IN_ONCREATE_EXTRA, false); + mCloseFirstDrawerOpened = intent.getBooleanExtra(CLOSE_FIRST_DRAWER_OPENED, false); + } + + /** + * Which configuration of drawers should be used. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE, + DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE, + DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE + }) + public @interface DrawerStyle { + int BOTH_DRAWER_NAV_SINGLE_PAGE = 0; + int BOTH_DRAWER_NAV_MULTI_PAGE = 1; + int ONLY_ACTION_DRAWER_WITH_TITLE = 2; + } + + /** + * Builds an {@link Intent} to start this {@link Activity} with the appropriate extras. + */ + public static class Builder { + + @DrawerStyle private int mStyle = DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE; + private boolean mOpenTopDrawerInOnCreate = false; + private boolean mOpenBottomDrawerInOnCreate = false; + private boolean mCloseFirstDrawerOpened = false; + + public Builder setStyle(@DrawerStyle int style) { + mStyle = style; + return this; + } + + public Builder openTopDrawerInOnCreate() { + mOpenTopDrawerInOnCreate = true; + return this; + } + + public Builder openBottomDrawerInOnCreate() { + mOpenBottomDrawerInOnCreate = true; + return this; + } + + public Builder closeFirstDrawerOpened() { + mCloseFirstDrawerOpened = true; + return this; + } + + public Intent build() { + return new Intent() + .putExtra(STYLE_EXTRA, mStyle) + .putExtra(OPEN_TOP_IN_ONCREATE_EXTRA, mOpenTopDrawerInOnCreate) + .putExtra(OPEN_BOTTOM_IN_ONCREATE_EXTRA, mOpenBottomDrawerInOnCreate) + .putExtra(CLOSE_FIRST_DRAWER_OPENED, mCloseFirstDrawerOpened); + } + } +} diff --git a/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java b/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java new file mode 100644 index 00000000..d1e44e67 --- /dev/null +++ b/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.drawer; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.swipeDown; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withParent; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Intent; +import android.support.test.espresso.PerformException; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.util.HumanReadables; +import android.support.test.espresso.util.TreeIterables; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v7.widget.RecyclerView; +import android.support.wear.test.R; +import android.support.wear.widget.drawer.DrawerTestActivity.DrawerStyle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.TimeoutException; + +import javax.annotation.Nullable; + +/** + * Espresso tests for {@link WearableDrawerLayout}. + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class WearableDrawerLayoutEspressoTest { + + private static final long MAX_WAIT_MS = 4000; + + @Rule public final ActivityTestRule activityRule = + new ActivityTestRule<>( + DrawerTestActivity.class, true /* touchMode */, false /* initialLaunch*/); + + private final Intent mSinglePageIntent = + new DrawerTestActivity.Builder().setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE) + .build(); + @Mock WearableNavigationDrawerView.OnItemSelectedListener mNavDrawerItemSelectedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void openingNavigationDrawerDoesNotCloseActionDrawer() { + // GIVEN a drawer layout with a peeking action and navigation drawer + activityRule.launchActivity(mSinglePageIntent); + DrawerTestActivity activity = activityRule.getActivity(); + WearableDrawerView actionDrawer = + (WearableDrawerView) activity.findViewById(R.id.action_drawer); + WearableDrawerView navigationDrawer = + (WearableDrawerView) activity.findViewById(R.id.navigation_drawer); + assertTrue(actionDrawer.isPeeking()); + assertTrue(navigationDrawer.isPeeking()); + + // WHEN the top drawer is opened + openDrawer(navigationDrawer); + onView(withId(R.id.navigation_drawer)) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isOpened(true)), + MAX_WAIT_MS)); + + // THEN the action drawer should still be peeking + assertTrue(actionDrawer.isPeeking()); + } + + @Test + public void swipingDownNavigationDrawerDoesNotCloseActionDrawer() { + // GIVEN a drawer layout with a peeking action and navigation drawer + activityRule.launchActivity(mSinglePageIntent); + onView(withId(R.id.action_drawer)).check(matches(isPeeking())); + onView(withId(R.id.navigation_drawer)).check(matches(isPeeking())); + + // WHEN the top drawer is opened by swiping down + onView(withId(R.id.drawer_layout)).perform(swipeDown()); + onView(withId(R.id.navigation_drawer)) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isOpened(true)), + MAX_WAIT_MS)); + + // THEN the action drawer should still be peeking + onView(withId(R.id.action_drawer)).check(matches(isPeeking())); + } + + + @Test + public void firstNavDrawerItemShouldBeSelectedInitially() { + // GIVEN a top drawer + // WHEN it is first opened + activityRule.launchActivity(mSinglePageIntent); + onView(withId(R.id.drawer_layout)).perform(swipeDown()); + onView(withId(R.id.navigation_drawer)) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isOpened(true)), + MAX_WAIT_MS)); + + // THEN the text should display "0". + onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0"))); + } + + @Test + public void selectingNavItemChangesTextAndClosedDrawer() { + // GIVEN an open top drawer + activityRule.launchActivity(mSinglePageIntent); + onView(withId(R.id.drawer_layout)).perform(swipeDown()); + onView(withId(R.id.navigation_drawer)) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isOpened(true)), + MAX_WAIT_MS)); + + // WHEN the second item is selected + onView(withId(R.id.ws_nav_drawer_icon_1)).perform(click()); + + // THEN the text should display "1" and it should close. + onView(withId(R.id.ws_nav_drawer_text)) + .perform( + waitForMatchingView( + allOf(withId(R.id.ws_nav_drawer_text), withText("1")), + MAX_WAIT_MS)); + onView(withId(R.id.navigation_drawer)) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isClosed(true)), + MAX_WAIT_MS)); + } + + @Test + public void programmaticallySelectingNavItemChangesTextInSinglePage() { + // GIVEN an open top drawer + activityRule.launchActivity(new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE) + .openTopDrawerInOnCreate() + .build()); + final WearableNavigationDrawerView navDrawer = + activityRule.getActivity().findViewById(R.id.navigation_drawer); + navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener); + + // WHEN the second item is selected programmatically + selectNavItem(navDrawer, 1); + + // THEN the text should display "1" and the listener should be notified. + onView(withId(R.id.ws_nav_drawer_text)) + .check(matches(withText("1"))); + verify(mNavDrawerItemSelectedListener).onItemSelected(1); + } + + @Test + public void programmaticallySelectingNavItemChangesTextInMultiPage() { + // GIVEN an open top drawer + activityRule.launchActivity(new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE) + .openTopDrawerInOnCreate() + .build()); + final WearableNavigationDrawerView navDrawer = + activityRule.getActivity().findViewById(R.id.navigation_drawer); + navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener); + + // WHEN the second item is selected programmatically + selectNavItem(navDrawer, 1); + + // THEN the text should display "1" and the listener should be notified. + onView(allOf(withId(R.id.ws_navigation_drawer_item_text), isDisplayed())) + .check(matches(withText("1"))); + verify(mNavDrawerItemSelectedListener).onItemSelected(1); + } + + @Test + public void navDrawerShouldOpenWhenCalledInOnCreate() { + // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate + // WHEN it is launched + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE) + .openTopDrawerInOnCreate() + .build()); + + // THEN the nav drawer should be open + onView(withId(R.id.navigation_drawer)).check(matches(isOpened(true))); + } + + @Test + public void actionDrawerShouldOpenWhenCalledInOnCreate() { + // GIVEN an activity with only an action drawer which is opened in onCreate + // WHEN it is launched + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .openBottomDrawerInOnCreate() + .build()); + + // THEN the action drawer should be open + onView(withId(R.id.action_drawer)).check(matches(isOpened(true))); + } + + @Test + public void navDrawerShouldOpenWhenCalledInOnCreateAndThenCloseWhenRequested() { + // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate, then closes it + // WHEN it is launched + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE) + .openTopDrawerInOnCreate() + .closeFirstDrawerOpened() + .build()); + + // THEN the nav drawer should be open and then close + onView(withId(R.id.navigation_drawer)) + .check(matches(isOpened(true))) + .perform( + waitForMatchingView( + allOf(withId(R.id.navigation_drawer), isClosed(true)), + MAX_WAIT_MS)); + } + + @Test + public void openedNavDrawerShouldPreventSwipeToClose() { + // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE) + .openTopDrawerInOnCreate() + .build()); + + // THEN the view should prevent swipe to close + onView(withId(R.id.navigation_drawer)).check(matches(not(allowsSwipeToClose()))); + } + + @Test + public void closedNavDrawerShouldNotPreventSwipeToClose() { + // GIVEN an activity which doesn't start with the nav drawer open + activityRule.launchActivity(mSinglePageIntent); + + // THEN the view should allow swipe to close + onView(withId(R.id.navigation_drawer)).check(matches(allowsSwipeToClose())); + } + + @Test + public void scrolledDownActionDrawerCanScrollUpWhenReOpened() { + // GIVEN a freshly launched activity + activityRule.launchActivity(mSinglePageIntent); + WearableActionDrawerView actionDrawer = + (WearableActionDrawerView) activityRule.getActivity() + .findViewById(R.id.action_drawer); + RecyclerView recyclerView = (RecyclerView) actionDrawer.getDrawerContent(); + + // WHEN the action drawer is opened and scrolled to the last item (Item 6) + openDrawer(actionDrawer); + scrollToPosition(recyclerView, 5); + onView(withId(R.id.action_drawer)) + .perform( + waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)), + MAX_WAIT_MS)) + .perform( + waitForMatchingView(allOf(withText("Item 6"), isCompletelyDisplayed()), + MAX_WAIT_MS)); + // and then it is peeked + peekDrawer(actionDrawer); + onView(withId(R.id.action_drawer)) + .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()), + MAX_WAIT_MS)); + // and re-opened + openDrawer(actionDrawer); + onView(withId(R.id.action_drawer)) + .perform( + waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)), + MAX_WAIT_MS)); + + // THEN item 6 should be visible, but swiping down should scroll up, not close the drawer. + onView(withText("Item 6")).check(matches(isDisplayed())); + onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true))); + } + + @Test + public void actionDrawerPeekIconShouldNotBeNull() { + // GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML + activityRule.launchActivity(mSinglePageIntent); + DrawerTestActivity activity = activityRule.getActivity(); + ImageView peekIconView = + (ImageView) activity + .findViewById(R.id.ws_action_drawer_peek_action_icon); + // THEN its peek icon should not be null + assertNotNull(peekIconView.getDrawable()); + } + + @Test + public void tappingActionDrawerPeekIconShouldTriggerFirstAction() { + // GIVEN a drawer layout with a peeking action drawer, title, and mock click listener + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .build()); + WearableActionDrawerView actionDrawer = + (WearableActionDrawerView) activityRule.getActivity() + .findViewById(R.id.action_drawer); + OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class); + actionDrawer.setOnMenuItemClickListener(mockClickListener); + // WHEN the action drawer peek view is tapped + onView(withId(R.id.ws_drawer_view_peek_container)) + .perform(waitForMatchingView( + allOf( + withId(R.id.ws_drawer_view_peek_container), + isCompletelyDisplayed()), + MAX_WAIT_MS)) + .perform(click()); + // THEN its click listener should be notified + verify(mockClickListener).onMenuItemClick(any(MenuItem.class)); + } + + @Test + public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() { + // GIVEN a drawer layout with an open action drawer with a title, and mock click listener + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .openBottomDrawerInOnCreate() + .build()); + WearableActionDrawerView actionDrawer = + (WearableActionDrawerView) activityRule.getActivity() + .findViewById(R.id.action_drawer); + OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class); + actionDrawer.setOnMenuItemClickListener(mockClickListener); + + // WHEN the action drawer is closed to its peek state and then tapped + peekDrawer(actionDrawer); + onView(withId(R.id.action_drawer)) + .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()), + MAX_WAIT_MS)); + actionDrawer.getPeekContainer().callOnClick(); + + // THEN its click listener should be notified + verify(mockClickListener).onMenuItemClick(any(MenuItem.class)); + } + + @Test + public void changingActionDrawerItemShouldUpdateView() { + // GIVEN a drawer layout with an open action drawer + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .openBottomDrawerInOnCreate() + .build()); + WearableActionDrawerView actionDrawer = + activityRule.getActivity().findViewById(R.id.action_drawer); + final MenuItem secondItem = actionDrawer.getMenu().getItem(1); + + // WHEN its second item is changed + actionDrawer.post(new Runnable() { + @Override + public void run() { + secondItem.setTitle("Modified item"); + } + }); + + // THEN the new item should be displayed + onView(withText("Modified item")).check(matches(isDisplayed())); + } + + @Test + public void removingActionDrawerItemShouldUpdateView() { + // GIVEN a drawer layout with an open action drawer + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .openBottomDrawerInOnCreate() + .build()); + final WearableActionDrawerView actionDrawer = + activityRule.getActivity().findViewById(R.id.action_drawer); + MenuItem secondItem = actionDrawer.getMenu().getItem(1); + final int itemId = secondItem.getItemId(); + final String title = secondItem.getTitle().toString(); + final int initialSize = getChildByType(actionDrawer, RecyclerView.class) + .getAdapter() + .getItemCount(); + + // WHEN its second item is removed + actionDrawer.post(new Runnable() { + @Override + public void run() { + actionDrawer.getMenu().removeItem(itemId); + } + }); + + // THEN it should decrease by 1 in size and it should no longer contain the item's text + onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class))) + .perform(waitForRecyclerToBeSize(initialSize - 1, MAX_WAIT_MS)) + .perform(waitForMatchingView(recyclerWithoutText(is(title)), MAX_WAIT_MS)); + } + + @Test + public void addingActionDrawerItemShouldUpdateView() { + // GIVEN a drawer layout with an open action drawer + activityRule.launchActivity( + new DrawerTestActivity.Builder() + .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE) + .openBottomDrawerInOnCreate() + .build()); + final WearableActionDrawerView actionDrawer = + activityRule.getActivity().findViewById(R.id.action_drawer); + + RecyclerView recycler = getChildByType(actionDrawer, RecyclerView.class); + final RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager(); + final int initialSize = recycler.getAdapter().getItemCount(); + + // WHEN an item is added and the view is scrolled down (to make sure the view is created) + actionDrawer.post(new Runnable() { + @Override + public void run() { + actionDrawer.getMenu().add(0, 42, Menu.NONE, "New Item"); + layoutManager.scrollToPosition(initialSize); + } + }); + + // THEN it should decrease by 1 in size and the there should be a view with the item's text + onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class))) + .perform(waitForRecyclerToBeSize(initialSize + 1, MAX_WAIT_MS)) + .perform(waitForMatchingView(withText("New Item"), MAX_WAIT_MS)); + } + + private void scrollToPosition(final RecyclerView recyclerView, final int position) { + recyclerView.post(new Runnable() { + @Override + public void run() { + recyclerView.scrollToPosition(position); + } + }); + } + + private void selectNavItem(final WearableNavigationDrawerView navDrawer, final int index) { + navDrawer.post(new Runnable() { + @Override + public void run() { + navDrawer.setCurrentItem(index, false); + } + }); + } + + private void peekDrawer(final WearableDrawerView drawer) { + drawer.post(new Runnable() { + @Override + public void run() { + drawer.getController().peekDrawer(); + } + }); + } + + private void openDrawer(final WearableDrawerView drawer) { + drawer.post(new Runnable() { + @Override + public void run() { + drawer.getController().openDrawer(); + } + }); + } + + private static TypeSafeMatcher isOpened(final boolean isOpened) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("is opened == " + isOpened); + } + + @Override + public boolean matchesSafely(View view) { + return ((WearableDrawerView) view).isOpened() == isOpened; + } + }; + } + + private static TypeSafeMatcher isClosed(final boolean isClosed) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View view) { + WearableDrawerView drawer = (WearableDrawerView) view; + return drawer.isClosed() == isClosed; + } + + @Override + public void describeTo(Description description) { + description.appendText("is closed"); + } + }; + } + + private TypeSafeMatcher isPeeking() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View view) { + WearableDrawerView drawer = (WearableDrawerView) view; + return drawer.isPeeking(); + } + + @Override + public void describeTo(Description description) { + description.appendText("is peeking"); + } + }; + } + + private TypeSafeMatcher allowsSwipeToClose() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(View view) { + return !view.canScrollHorizontally(-2) && !view.canScrollHorizontally(2); + } + + @Override + public void describeTo(Description description) { + description.appendText("can be swiped closed"); + } + }; + } + + /** + * Returns a {@link TypeSafeMatcher} that returns {@code true} when the {@link RecyclerView} + * does not contain a {@link TextView} with text matched by {@code textMatcher}. + */ + private TypeSafeMatcher recyclerWithoutText(final Matcher textMatcher) { + return new TypeSafeMatcher() { + + @Override + public void describeTo(Description description) { + description.appendText("Without recycler text "); + textMatcher.describeTo(description); + } + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof RecyclerView)) { + return false; + } + + RecyclerView recycler = ((RecyclerView) view); + if (recycler.isAnimating()) { + // While the RecyclerView is animating, it will return null ViewHolders and we + // won't be able to tell whether the item has been removed or not. + return false; + } + + for (int i = 0; i < recycler.getAdapter().getItemCount(); i++) { + RecyclerView.ViewHolder holder = recycler.findViewHolderForAdapterPosition(i); + if (holder != null) { + TextView text = getChildByType(holder.itemView, TextView.class); + if (text != null && textMatcher.matches(text.getText())) { + return false; + } + } + } + + return true; + } + }; + } + + /** + * Waits for the {@link RecyclerView} to contain {@code targetCount} items, up to {@code millis} + * milliseconds. Throws exception if the time limit is reached before reaching the desired + * number of items. + */ + public ViewAction waitForRecyclerToBeSize(final int targetCount, final long millis) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return isAssignableFrom(RecyclerView.class); + } + + @Override + public String getDescription() { + return "Waiting for recycler to be size=" + targetCount; + } + + @Override + public void perform(UiController uiController, View view) { + if (!(view instanceof RecyclerView)) { + return; + } + + RecyclerView recycler = (RecyclerView) view; + uiController.loopMainThreadUntilIdle(); + final long startTime = System.currentTimeMillis(); + final long endTime = startTime + millis; + do { + if (recycler.getAdapter().getItemCount() == targetCount) { + return; + } + uiController.loopMainThreadForAtLeast(100); // at least 3 frames + } while (System.currentTimeMillis() < endTime); + + // timeout happens + throw new PerformException.Builder() + .withActionDescription(this.getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .withCause(new TimeoutException()) + .build(); + } + }; + } + + /** + * Returns the first child of {@code root} to be an instance of class {@code T}, or {@code null} + * if none were found. + */ + @Nullable + private T getChildByType(View root, Class classOfChildToFind) { + for (View child : TreeIterables.breadthFirstViewTraversal(root)) { + if (classOfChildToFind.isInstance(child)) { + return (T) child; + } + } + + return null; + } +} diff --git a/android/support/wear/widget/util/ArcSwipe.java b/android/support/wear/widget/util/ArcSwipe.java new file mode 100644 index 00000000..2630d19f --- /dev/null +++ b/android/support/wear/widget/util/ArcSwipe.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.util; + +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; +import android.os.SystemClock; +import android.support.annotation.VisibleForTesting; +import android.support.test.espresso.UiController; +import android.support.test.espresso.action.MotionEvents; +import android.support.test.espresso.action.Swiper; +import android.support.v4.util.Preconditions; +import android.util.Log; +import android.view.MotionEvent; + +/** + * Swiper for gestures meant to be performed on an arc - part of a circle - not a straight line. + * This class assumes a square bounding box with the radius of the circle being half the height of + * the box. + */ +public class ArcSwipe implements Swiper { + + /** Enum describing the exact gesture which will perform the curved swipe. */ + public enum Gesture { + /** Swipes quickly between the co-ordinates, clockwise. */ + FAST_CLOCKWISE(SWIPE_FAST_DURATION_MS, true), + /** Swipes deliberately slowly between the co-ordinates, clockwise. */ + SLOW_CLOCKWISE(SWIPE_SLOW_DURATION_MS, true), + /** Swipes quickly between the co-ordinates, anticlockwise. */ + FAST_ANTICLOCKWISE(SWIPE_FAST_DURATION_MS, false), + /** Swipes deliberately slowly between the co-ordinates, anticlockwise. */ + SLOW_ANTICLOCKWISE(SWIPE_SLOW_DURATION_MS, false); + + private final int mDuration; + private final boolean mClockwise; + + Gesture(int duration, boolean clockwise) { + mDuration = duration; + mClockwise = clockwise; + } + } + + /** The number of motion events to send for each swipe. */ + private static final int SWIPE_EVENT_COUNT = 10; + + /** Length of time a "fast" swipe should last for, in milliseconds. */ + private static final int SWIPE_FAST_DURATION_MS = 100; + + /** Length of time a "slow" swipe should last for, in milliseconds. */ + private static final int SWIPE_SLOW_DURATION_MS = 1500; + + private static final String TAG = ArcSwipe.class.getSimpleName(); + private final RectF mBounds; + private final Gesture mGesture; + + public ArcSwipe(Gesture gesture, RectF bounds) { + Preconditions.checkArgument(bounds.height() == bounds.width()); + mGesture = gesture; + mBounds = bounds; + } + + @Override + public Swiper.Status sendSwipe( + UiController uiController, + float[] startCoordinates, + float[] endCoordinates, + float[] precision) { + return sendArcSwipe( + uiController, + startCoordinates, + endCoordinates, + precision, + mGesture.mDuration, + mGesture.mClockwise); + } + + private float[][] interpolate(float[] start, float[] end, int steps, boolean isClockwise) { + float startAngle = getAngle(start[0], start[1]); + float endAngle = getAngle(end[0], end[1]); + + Path path = new Path(); + PathMeasure pathMeasure = new PathMeasure(); + path.moveTo(start[0], start[1]); + path.arcTo(mBounds, startAngle, getSweepAngle(startAngle, endAngle, isClockwise)); + pathMeasure.setPath(path, false); + float pathLength = pathMeasure.getLength(); + + float[][] res = new float[steps][2]; + float[] mPathTangent = new float[2]; + + for (int i = 1; i < steps + 1; i++) { + pathMeasure.getPosTan((pathLength * i) / (steps + 2f), res[i - 1], mPathTangent); + } + + return res; + } + + private Swiper.Status sendArcSwipe( + UiController uiController, + float[] startCoordinates, + float[] endCoordinates, + float[] precision, + int duration, + boolean isClockwise) { + + float[][] steps = interpolate(startCoordinates, endCoordinates, SWIPE_EVENT_COUNT, + isClockwise); + final int delayBetweenMovements = duration / steps.length; + + MotionEvent downEvent = MotionEvents.sendDown(uiController, startCoordinates, + precision).down; + try { + for (int i = 0; i < steps.length; i++) { + if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) { + Log.e(TAG, + "Injection of move event as part of the swipe failed. Sending cancel " + + "event."); + MotionEvents.sendCancel(uiController, downEvent); + return Swiper.Status.FAILURE; + } + + long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i; + long timeUntilDesired = desiredTime - SystemClock.uptimeMillis(); + if (timeUntilDesired > 10) { + uiController.loopMainThreadForAtLeast(timeUntilDesired); + } + } + + if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) { + Log.e(TAG, + "Injection of up event as part of the swipe failed. Sending cancel event."); + MotionEvents.sendCancel(uiController, downEvent); + return Swiper.Status.FAILURE; + } + } finally { + downEvent.recycle(); + } + return Swiper.Status.SUCCESS; + } + + @VisibleForTesting + float getAngle(double x, double y) { + double relativeX = x - (mBounds.width() / 2); + double relativeY = y - (mBounds.height() / 2); + double rowAngle = Math.atan2(relativeX, relativeY); + double angle = -Math.toDegrees(rowAngle) - 180; + if (angle < 0) { + angle += 360; + } + return (float) angle; + } + + @VisibleForTesting + float getSweepAngle(float startAngle, float endAngle, boolean isClockwise) { + float sweepAngle = endAngle - startAngle; + if (sweepAngle < 0) { + sweepAngle += 360; + } + return isClockwise ? sweepAngle : (360 - sweepAngle); + } +} diff --git a/android/support/wear/widget/util/ArcSwipeTest.java b/android/support/wear/widget/util/ArcSwipeTest.java new file mode 100644 index 00000000..f403e1b3 --- /dev/null +++ b/android/support/wear/widget/util/ArcSwipeTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.util; + +import static org.junit.Assert.assertEquals; + +import android.graphics.RectF; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ArcSwipe}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ArcSwipeTest { + private ArcSwipe mArcSwipeUnderTest; + private final RectF mFakeBounds = new RectF(0, 0, 400, 400); + + @Before + public void setup() { + mArcSwipeUnderTest = new ArcSwipe(ArcSwipe.Gesture.FAST_CLOCKWISE, mFakeBounds); + } + + @Test + public void testSweepAngleClockwise() { + assertEquals(0, mArcSwipeUnderTest.getSweepAngle(0, 0, true), 0.0f); + assertEquals(360, mArcSwipeUnderTest.getSweepAngle(0, 360, true), 0.0f); + assertEquals(90, mArcSwipeUnderTest.getSweepAngle(0, 90, true), 0.0f); + assertEquals(90, mArcSwipeUnderTest.getSweepAngle(90, 180, true), 0.0f); + assertEquals(225, mArcSwipeUnderTest.getSweepAngle(45, 270, true), 0.0f); + assertEquals(270, mArcSwipeUnderTest.getSweepAngle(90, 0, true), 0.0f); + assertEquals(170, mArcSwipeUnderTest.getSweepAngle(280, 90, true), 0.0f); + } + + @Test + public void testSweepAngleAntiClockwise() { + assertEquals(360, mArcSwipeUnderTest.getSweepAngle(0, 0, false), 0.0f); + assertEquals(0, mArcSwipeUnderTest.getSweepAngle(0, 360, false), 0.0f); + assertEquals(270, mArcSwipeUnderTest.getSweepAngle(0, 90, false), 0.0f); + assertEquals(270, mArcSwipeUnderTest.getSweepAngle(90, 180, false), 0.0f); + assertEquals(135, mArcSwipeUnderTest.getSweepAngle(45, 270, false), 0.0f); + assertEquals(90, mArcSwipeUnderTest.getSweepAngle(90, 0, false), 0.0f); + assertEquals(190, mArcSwipeUnderTest.getSweepAngle(280, 90, false), 0.0f); + } + + @Test + public void testGetAngle() { + assertEquals(0, mArcSwipeUnderTest.getAngle(200, 0), 0.0f); + assertEquals(90, mArcSwipeUnderTest.getAngle(400, 200), 0.0f); + assertEquals(180, mArcSwipeUnderTest.getAngle(200, 400), 0.0f); + assertEquals(270, mArcSwipeUnderTest.getAngle(0, 200), 0.0f); + } +} diff --git a/android/support/wear/widget/util/AsyncViewActions.java b/android/support/wear/widget/util/AsyncViewActions.java new file mode 100644 index 00000000..3db4619b --- /dev/null +++ b/android/support/wear/widget/util/AsyncViewActions.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.util; + +import android.support.test.espresso.PerformException; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.util.HumanReadables; +import android.support.test.espresso.util.TreeIterables; +import android.view.View; + +import org.hamcrest.Matchers; +import org.hamcrest.StringDescription; + +import java.util.concurrent.TimeoutException; + +public class AsyncViewActions { + + /** Perform action of waiting for a specific view id. */ + public static ViewAction waitForMatchingView( + final org.hamcrest.Matcher viewMatcher, final long millis) { + return new ViewAction() { + @Override + public void perform(UiController uiController, View view) { + uiController.loopMainThreadUntilIdle(); + final long startTime = System.currentTimeMillis(); + final long endTime = startTime + millis; + do { + for (View child : TreeIterables.breadthFirstViewTraversal(view)) { + // found matching view + if (viewMatcher.matches(child)) { + return; + } + } + uiController.loopMainThreadForAtLeast(100); // at least 3 frames + } while (System.currentTimeMillis() < endTime); + + // timeout happens + throw new PerformException.Builder() + .withActionDescription(this.getDescription()) + .withViewDescription(HumanReadables.describe(view)) + .withCause(new TimeoutException()) + .build(); + } + + @Override + public String getDescription() { + return "Wait for view which matches " + StringDescription.asString(viewMatcher); + } + + @Override + public org.hamcrest.Matcher getConstraints() { + return Matchers.any(View.class); + } + }; + } +} diff --git a/android/support/wear/widget/util/MoreViewAssertions.java b/android/support/wear/widget/util/MoreViewAssertions.java new file mode 100644 index 00000000..503336ba --- /dev/null +++ b/android/support/wear/widget/util/MoreViewAssertions.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.util; + +import static android.support.test.espresso.matcher.ViewMatchers.assertThat; + +import android.support.test.espresso.NoMatchingViewException; +import android.support.test.espresso.ViewAssertion; +import android.support.test.espresso.util.HumanReadables; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +public class MoreViewAssertions { + + public static ViewAssertion left(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View left: " + HumanReadables.describe(view), view.getLeft(), matcher); + } + }; + } + + public static ViewAssertion approximateTop(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View top: " + HumanReadables.describe(view), ((double) view.getTop()), + matcher); + } + }; + } + + public static ViewAssertion top(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View top: " + HumanReadables.describe(view), view.getTop(), matcher); + } + }; + } + + public static ViewAssertion right(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View right: " + HumanReadables.describe(view), view.getRight(), + matcher); + } + }; + } + + public static ViewAssertion bottom(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View bottom: " + HumanReadables.describe(view), view.getBottom(), + matcher); + } + }; + } + + public static ViewAssertion approximateBottom(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + assertThat("View bottom: " + HumanReadables.describe(view), ((double) view + .getBottom()), matcher); + } + }; + } + + /** + * Returns a new ViewAssertion against a match of the view's left position, relative to the + * left + * edge of the containing window. + * + * @param matcher matcher for the left position + */ + public static ViewAssertion screenLeft(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + int[] screenXy = {0, 0}; + view.getLocationInWindow(screenXy); + assertThat("View screenLeft: " + HumanReadables.describe(view), screenXy[0], + matcher); + } + }; + } + + /** + * Returns a new ViewAssertion against a match of the view's top position, relative to the top + * edge of the containing window. + * + * @param matcher matcher for the top position + */ + public static ViewAssertion screenTop(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + int[] screenXy = {0, 0}; + view.getLocationInWindow(screenXy); + assertThat("View screenTop: " + HumanReadables.describe(view), screenXy[1], + matcher); + } + }; + } + + /** + * Returns a new ViewAssertion against a match of the view's right position, relative to the + * left + * edge of the containing window. + * + * @param matcher matcher for the right position + */ + public static ViewAssertion screenRight(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + int[] screenXy = {0, 0}; + view.getLocationInWindow(screenXy); + assertThat("View screenRight: " + HumanReadables.describe(view), + screenXy[0] + view.getWidth(), matcher); + } + }; + } + + /** + * Returns a new ViewAssertion against a match of the view's bottom position, relative to the + * top + * edge of the containing window. + * + * @param matcher matcher for the bottom position + */ + public static ViewAssertion screenBottom(final Matcher matcher) { + return new ViewAssertion() { + @Override + public void check(View view, NoMatchingViewException noViewException) { + int[] screenXy = {0, 0}; + view.getLocationInWindow(screenXy); + assertThat("View screenBottom: " + HumanReadables.describe(view), + screenXy[1] + view.getHeight(), matcher); + } + }; + } + + public static Matcher withTranslationX(final int xTranslation) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("with x translation == " + xTranslation); + } + + @Override + public boolean matchesSafely(View view) { + return view.getTranslationX() == xTranslation; + } + }; + } + + public static Matcher withPositiveVerticalScrollOffset() { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("with positive y scroll offset"); + } + + @Override + public boolean matchesSafely(RecyclerView view) { + return view.computeVerticalScrollOffset() > 0; + } + }; + } + + public static Matcher withNoVerticalScrollOffset() { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("with no y scroll offset"); + } + + @Override + public boolean matchesSafely(RecyclerView view) { + return view.computeVerticalScrollOffset() == 0; + } + }; + } +} diff --git a/android/support/wear/widget/util/WakeLockRule.java b/android/support/wear/widget/util/WakeLockRule.java new file mode 100644 index 00000000..13b627e7 --- /dev/null +++ b/android/support/wear/widget/util/WakeLockRule.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.wear.widget.util; + +import android.content.Context; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.support.test.InstrumentationRegistry; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Rule which holds a wake lock for the duration of the test. + */ +public class WakeLockRule implements TestRule { + @SuppressWarnings("deprecation") + private static final int WAKELOCK_FLAGS = + PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; + + @Override + public Statement apply(final Statement statement, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + WakeLock wakeLock = createWakeLock(); + wakeLock.acquire(); + try { + statement.evaluate(); + } finally { + wakeLock.release(); + } + } + }; + } + + private WakeLock createWakeLock() { + Context context = InstrumentationRegistry.getTargetContext(); + PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return power.newWakeLock(WAKELOCK_FLAGS, context.getPackageName()); + } +} diff --git a/android/system/OsConstants.java b/android/system/OsConstants.java index 3e1f007d..83a1b41a 100644 --- a/android/system/OsConstants.java +++ b/android/system/OsConstants.java @@ -368,6 +368,7 @@ public final class OsConstants { public static final int O_APPEND = placeholder(); public static final int O_CLOEXEC = placeholder(); public static final int O_CREAT = placeholder(); + /** @hide */ public static final int O_DIRECT = placeholder(); public static final int O_EXCL = placeholder(); public static final int O_NOCTTY = placeholder(); public static final int O_NOFOLLOW = placeholder(); diff --git a/android/telecom/Call.java b/android/telecom/Call.java index a07f2bbf..20911012 100644 --- a/android/telecom/Call.java +++ b/android/telecom/Call.java @@ -416,8 +416,15 @@ public final class Call { */ public static final int PROPERTY_SELF_MANAGED = 0x00000100; + /** + * Indicates the call used Assisted Dialing. + * See also {@link Connection#PROPERTY_ASSISTED_DIALING_USED} + * @hide + */ + public static final int PROPERTY_ASSISTED_DIALING_USED = 0x00000200; + //****************************************************************************************** - // Next PROPERTY value: 0x00000200 + // Next PROPERTY value: 0x00000400 //****************************************************************************************** private final String mTelecomCallId; @@ -577,6 +584,9 @@ public final class Call { if(hasProperty(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) { builder.append(" PROPERTY_HAS_CDMA_VOICE_PRIVACY"); } + if(hasProperty(properties, PROPERTY_ASSISTED_DIALING_USED)) { + builder.append(" PROPERTY_ASSISTED_DIALING_USED"); + } builder.append("]"); return builder.toString(); } @@ -858,7 +868,8 @@ public final class Call { * @hide */ @IntDef({HANDOVER_FAILURE_DEST_APP_REJECTED, HANDOVER_FAILURE_DEST_NOT_SUPPORTED, - HANDOVER_FAILURE_DEST_INVALID_PERM, HANDOVER_FAILURE_DEST_USER_REJECTED}) + HANDOVER_FAILURE_DEST_INVALID_PERM, HANDOVER_FAILURE_DEST_USER_REJECTED, + HANDOVER_FAILURE_ONGOING_EMERG_CALL}) @Retention(RetentionPolicy.SOURCE) public @interface HandoverFailureErrors {} @@ -886,6 +897,12 @@ public final class Call { */ public static final int HANDOVER_FAILURE_DEST_USER_REJECTED = 4; + /** + * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when there + * is ongoing emergency call. + */ + public static final int HANDOVER_FAILURE_ONGOING_EMERG_CALL = 5; + /** * Invoked when the state of this {@code Call} has changed. See {@link #getState()}. @@ -1935,6 +1952,15 @@ public final class Call { } } + /** {@hide} */ + final void internalOnHandoverFailed(int error) { + for (CallbackRecord record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(() -> callback.onHandoverFailed(call, error)); + } + } + private void fireStateChanged(final int newState) { for (CallbackRecord record : mCallbackRecords) { final Call call = this; diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java index 2bb1c4ed..aaef8d3d 100644 --- a/android/telecom/Connection.java +++ b/android/telecom/Connection.java @@ -23,7 +23,6 @@ import com.android.internal.telecom.IVideoProvider; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.annotation.TestApi; import android.app.Notification; import android.bluetooth.BluetoothDevice; import android.content.Intent; @@ -397,13 +396,17 @@ public abstract class Connection extends Conferenceable { /** * Set by the framework to indicate that a connection has an active RTT session associated with * it. - * @hide */ - @TestApi public static final int PROPERTY_IS_RTT = 1 << 8; + /** + * Set by the framework to indicate that a connection is using assisted dialing. + * @hide + */ + public static final int PROPERTY_ASSISTED_DIALING_USED = 1 << 9; + //********************************************************************************************** - // Next PROPERTY value: 1<<9 + // Next PROPERTY value: 1<<10 //********************************************************************************************** /** @@ -831,9 +834,7 @@ public abstract class Connection extends Conferenceable { /** * Provides methods to read and write RTT data to/from the in-call app. - * @hide */ - @TestApi public static final class RttTextStream { private static final int READ_BUFFER_SIZE = 1000; private final InputStreamReader mPipeFromInCall; @@ -2608,10 +2609,8 @@ public abstract class Connection extends Conferenceable { /** * Informs listeners that a previously requested RTT session via * {@link ConnectionRequest#isRequestingRtt()} or - * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)} has succeeded. - * @hide + * {@link #onStartRtt(RttTextStream)} has succeeded. */ - @TestApi public final void sendRttInitiationSuccess() { setRttProperty(); mListeners.forEach((l) -> l.onRttInitiationSuccess(Connection.this)); @@ -2619,14 +2618,11 @@ public abstract class Connection extends Conferenceable { /** * Informs listeners that a previously requested RTT session via - * {@link ConnectionRequest#isRequestingRtt()} or - * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)} + * {@link ConnectionRequest#isRequestingRtt()} or {@link #onStartRtt(RttTextStream)} * has failed. * @param reason One of the reason codes defined in {@link RttModifyStatus}, with the * exception of {@link RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}. - * @hide */ - @TestApi public final void sendRttInitiationFailure(int reason) { unsetRttProperty(); mListeners.forEach((l) -> l.onRttInitiationFailure(Connection.this, reason)); @@ -2635,9 +2631,7 @@ public abstract class Connection extends Conferenceable { /** * Informs listeners that a currently active RTT session has been terminated by the remote * side of the coll. - * @hide */ - @TestApi public final void sendRttSessionRemotelyTerminated() { mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this)); } @@ -2645,9 +2639,7 @@ public abstract class Connection extends Conferenceable { /** * Informs listeners that the remote side of the call has requested an upgrade to include an * RTT session in the call. - * @hide */ - @TestApi public final void sendRemoteRttRequest() { mListeners.forEach((l) -> l.onRemoteRttRequest(Connection.this)); } @@ -2864,17 +2856,13 @@ public abstract class Connection extends Conferenceable { * request, respectively. * @param rttTextStream The object that should be used to send text to or receive text from * the in-call app. - * @hide */ - @TestApi public void onStartRtt(@NonNull RttTextStream rttTextStream) {} /** * Notifies this {@link Connection} that it should terminate any existing RTT communication * channel. No response to Telecom is needed for this method. - * @hide */ - @TestApi public void onStopRtt() {} /** @@ -2882,11 +2870,9 @@ public abstract class Connection extends Conferenceable { * request sent via {@link #sendRemoteRttRequest}. Acceptance of the request is * indicated by the supplied {@link RttTextStream} being non-null, and rejection is * indicated by {@code rttTextStream} being {@code null} - * @hide * @param rttTextStream The object that should be used to send text to or receive text from * the in-call app. */ - @TestApi public void handleRttUpgradeResponse(@Nullable RttTextStream rttTextStream) {} /** diff --git a/android/telecom/ConnectionRequest.java b/android/telecom/ConnectionRequest.java index e169e5f8..658b4734 100644 --- a/android/telecom/ConnectionRequest.java +++ b/android/telecom/ConnectionRequest.java @@ -16,7 +16,6 @@ package android.telecom; -import android.annotation.TestApi; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; @@ -310,9 +309,7 @@ public final class ConnectionRequest implements Parcelable { * send and receive RTT text to/from the in-call app. * @return An instance of {@link android.telecom.Connection.RttTextStream}, or {@code null} * if this connection request is not requesting an RTT session upon connection establishment. - * @hide */ - @TestApi public Connection.RttTextStream getRttTextStream() { if (isRequestingRtt()) { return new Connection.RttTextStream(mRttPipeToInCall, mRttPipeFromInCall); @@ -324,9 +321,7 @@ public final class ConnectionRequest implements Parcelable { /** * Convenience method for determining whether the ConnectionRequest is requesting an RTT session * @return {@code true} if RTT is requested, {@code false} otherwise. - * @hide */ - @TestApi public boolean isRequestingRtt() { return mRttPipeFromInCall != null && mRttPipeToInCall != null; } diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java index 7e833066..6af01aee 100644 --- a/android/telecom/ConnectionService.java +++ b/android/telecom/ConnectionService.java @@ -21,6 +21,7 @@ import android.app.Service; import android.content.ComponentName; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -143,6 +144,9 @@ public abstract class ConnectionService extends Service { private static final String SESSION_START_RTT = "CS.+RTT"; private static final String SESSION_STOP_RTT = "CS.-RTT"; private static final String SESSION_RTT_UPGRADE_RESPONSE = "CS.rTRUR"; + private static final String SESSION_CONNECTION_SERVICE_FOCUS_LOST = "CS.cSFL"; + private static final String SESSION_CONNECTION_SERVICE_FOCUS_GAINED = "CS.cSFG"; + private static final String SESSION_HANDOVER_FAILED = "CS.haF"; private static final int MSG_ADD_CONNECTION_SERVICE_ADAPTER = 1; private static final int MSG_CREATE_CONNECTION = 2; @@ -172,6 +176,9 @@ public abstract class ConnectionService extends Service { private static final int MSG_ON_STOP_RTT = 27; private static final int MSG_RTT_UPGRADE_RESPONSE = 28; private static final int MSG_CREATE_CONNECTION_COMPLETE = 29; + private static final int MSG_CONNECTION_SERVICE_FOCUS_LOST = 30; + private static final int MSG_CONNECTION_SERVICE_FOCUS_GAINED = 31; + private static final int MSG_HANDOVER_FAILED = 32; private static Connection sNullConnection; @@ -274,6 +281,22 @@ public abstract class ConnectionService extends Service { } } + @Override + public void handoverFailed(String callId, ConnectionRequest request, int reason, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_HANDOVER_FAILED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = request; + args.arg3 = Log.createSubsession(); + args.arg4 = reason; + mHandler.obtainMessage(MSG_HANDOVER_FAILED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + @Override public void abort(String callId, Session.Info sessionInfo) { Log.startSession(sessionInfo, SESSION_ABORT); @@ -591,6 +614,26 @@ public abstract class ConnectionService extends Service { Log.endSession(); } } + + @Override + public void connectionServiceFocusLost(Session.Info sessionInfo) throws RemoteException { + Log.startSession(sessionInfo, SESSION_CONNECTION_SERVICE_FOCUS_LOST); + try { + mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_LOST).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void connectionServiceFocusGained(Session.Info sessionInfo) throws RemoteException { + Log.startSession(sessionInfo, SESSION_CONNECTION_SERVICE_FOCUS_GAINED); + try { + mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_GAINED).sendToTarget(); + } finally { + Log.endSession(); + } + } }; private final Handler mHandler = new Handler(Looper.getMainLooper()) { @@ -723,6 +766,36 @@ public abstract class ConnectionService extends Service { } break; } + case MSG_HANDOVER_FAILED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, SESSION_HANDLER + + SESSION_HANDOVER_FAILED); + try { + final String id = (String) args.arg1; + final ConnectionRequest request = (ConnectionRequest) args.arg2; + final int reason = (int) args.arg4; + if (!mAreAccountsInitialized) { + Log.d(this, "Enqueueing pre-init request %s", id); + mPreInitializationConnectionRequests.add( + new android.telecom.Logging.Runnable( + SESSION_HANDLER + + SESSION_HANDOVER_FAILED + ".pICR", + null /*lock*/) { + @Override + public void loggedRun() { + handoverFailed(id, request, reason); + } + }.prepare()); + } else { + Log.i(this, "createConnectionFailed %s", id); + handoverFailed(id, request, reason); + } + } finally { + args.recycle(); + Log.endSession(); + } + break; + } case MSG_ABORT: { SomeArgs args = (SomeArgs) msg.obj; Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_ABORT); @@ -1012,6 +1085,12 @@ public abstract class ConnectionService extends Service { } break; } + case MSG_CONNECTION_SERVICE_FOCUS_GAINED: + onConnectionServiceFocusGained(); + break; + case MSG_CONNECTION_SERVICE_FOCUS_LOST: + onConnectionServiceFocusLost(); + break; default: break; } @@ -1371,13 +1450,25 @@ public abstract class ConnectionService extends Service { isIncoming, isUnknown); - Connection connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request) - : isIncoming ? onCreateIncomingConnection(callManagerAccount, request) - : onCreateOutgoingConnection(callManagerAccount, request); + Connection connection = null; + if (getApplicationContext().getApplicationInfo().targetSdkVersion > + Build.VERSION_CODES.O_MR1 && request.getExtras() != null && + request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER,false)) { + if (!isIncoming) { + connection = onCreateOutgoingHandoverConnection(callManagerAccount, request); + } else { + connection = onCreateIncomingHandoverConnection(callManagerAccount, request); + } + } else { + connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request) + : isIncoming ? onCreateIncomingConnection(callManagerAccount, request) + : onCreateOutgoingConnection(callManagerAccount, request); + } Log.d(this, "createConnection, connection: %s", connection); if (connection == null) { + Log.i(this, "createConnection, implementation returned null connection."); connection = Connection.createFailedConnection( - new DisconnectCause(DisconnectCause.ERROR)); + new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONNECTION")); } connection.setTelecomCallId(callId); @@ -1442,6 +1533,13 @@ public abstract class ConnectionService extends Service { } } + private void handoverFailed(final String callId, final ConnectionRequest request, + int reason) { + + Log.i(this, "handoverFailed %s", callId); + onHandoverFailed(request, reason); + } + /** * Called by Telecom when the creation of a new Connection has completed and it is now added * to Telecom. @@ -1862,6 +1960,16 @@ public abstract class ConnectionService extends Service { addExistingConnection(phoneAccountHandle, connection, null /* conference */); } + /** + * Call to inform Telecom that your {@link ConnectionService} has released call resources (e.g + * microphone, camera). + * + * @see ConnectionService#onConnectionServiceFocusLost() + */ + public final void connectionServiceFocusReleased() { + mAdapter.onConnectionServiceFocusReleased(); + } + /** * Adds a connection created by the {@link ConnectionService} and informs telecom of the new * connection. @@ -2135,6 +2243,20 @@ public abstract class ConnectionService extends Service { */ public void onRemoteExistingConnectionAdded(RemoteConnection connection) {} + /** + * Called when the {@link ConnectionService} has lost the call focus. + * The {@link ConnectionService} should release the call resources and invokes + * {@link ConnectionService#connectionServiceFocusReleased()} to inform telecom that it has + * released the call resources. + */ + public void onConnectionServiceFocusLost() {} + + /** + * Called when the {@link ConnectionService} has gained the call focus. The + * {@link ConnectionService} can acquire the call resources at this time. + */ + public void onConnectionServiceFocusGained() {} + /** * @hide */ diff --git a/android/telecom/ConnectionServiceAdapter.java b/android/telecom/ConnectionServiceAdapter.java index 92a9dc23..0d319bbc 100644 --- a/android/telecom/ConnectionServiceAdapter.java +++ b/android/telecom/ConnectionServiceAdapter.java @@ -628,4 +628,17 @@ final class ConnectionServiceAdapter implements DeathRecipient { } } } + + /** + * Notifies Telecom that the {@link ConnectionService} has released the call resource. + */ + void onConnectionServiceFocusReleased() { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Log.d(this, "onConnectionServiceFocusReleased"); + adapter.onConnectionServiceFocusReleased(Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } } diff --git a/android/telecom/ConnectionServiceAdapterServant.java b/android/telecom/ConnectionServiceAdapterServant.java index 3fbdeb1e..3e1bf779 100644 --- a/android/telecom/ConnectionServiceAdapterServant.java +++ b/android/telecom/ConnectionServiceAdapterServant.java @@ -73,6 +73,7 @@ final class ConnectionServiceAdapterServant { private static final int MSG_ON_RTT_REMOTELY_TERMINATED = 32; private static final int MSG_ON_RTT_UPGRADE_REQUEST = 33; private static final int MSG_SET_PHONE_ACCOUNT_CHANGED = 34; + private static final int MSG_CONNECTION_SERVICE_FOCUS_RELEASED = 35; private final IConnectionServiceAdapter mDelegate; @@ -329,6 +330,9 @@ final class ConnectionServiceAdapterServant { } break; } + case MSG_CONNECTION_SERVICE_FOCUS_RELEASED: + mDelegate.onConnectionServiceFocusReleased(null /*Session.Info*/); + break; } } }; @@ -601,6 +605,11 @@ final class ConnectionServiceAdapterServant { args.arg2 = pHandle; mHandler.obtainMessage(MSG_SET_PHONE_ACCOUNT_CHANGED, args).sendToTarget(); } + + @Override + public void onConnectionServiceFocusReleased(Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_RELEASED).sendToTarget(); + } }; public ConnectionServiceAdapterServant(IConnectionServiceAdapter delegate) { diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java index d558bbae..74fa62d6 100644 --- a/android/telecom/InCallService.java +++ b/android/telecom/InCallService.java @@ -80,6 +80,7 @@ public abstract class InCallService extends Service { private static final int MSG_ON_CONNECTION_EVENT = 9; private static final int MSG_ON_RTT_UPGRADE_REQUEST = 10; private static final int MSG_ON_RTT_INITIATION_FAILURE = 11; + private static final int MSG_ON_HANDOVER_FAILED = 12; /** Default Handler used to consolidate binder method calls onto a single thread. */ private final Handler mHandler = new Handler(Looper.getMainLooper()) { @@ -150,6 +151,12 @@ public abstract class InCallService extends Service { mPhone.internalOnRttInitiationFailure(callId, reason); break; } + case MSG_ON_HANDOVER_FAILED: { + String callId = (String) msg.obj; + int error = msg.arg1; + mPhone.internalOnHandoverFailed(callId, error); + break; + } default: break; } @@ -225,6 +232,11 @@ public abstract class InCallService extends Service { public void onRttInitiationFailure(String callId, int reason) { mHandler.obtainMessage(MSG_ON_RTT_INITIATION_FAILURE, reason, 0, callId).sendToTarget(); } + + @Override + public void onHandoverFailed(String callId, int error) { + mHandler.obtainMessage(MSG_ON_HANDOVER_FAILED, error, 0, callId).sendToTarget(); + } } private Phone.Listener mPhoneListener = new Phone.Listener() { diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java index 421b1a4b..b5394b9b 100644 --- a/android/telecom/Phone.java +++ b/android/telecom/Phone.java @@ -223,6 +223,13 @@ public final class Phone { } } + final void internalOnHandoverFailed(String callId, int error) { + Call call = mCallByTelecomCallId.get(callId); + if (call != null) { + call.internalOnHandoverFailed(error); + } + } + /** * Called to destroy the phone and cleanup any lingering calls. */ diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java index 74b94650..fcfc5931 100644 --- a/android/telecom/PhoneAccount.java +++ b/android/telecom/PhoneAccount.java @@ -964,6 +964,9 @@ public final class PhoneAccount implements Parcelable { if (hasCapabilities(CAPABILITY_SIM_SUBSCRIPTION)) { sb.append("SimSub "); } + if (hasCapabilities(CAPABILITY_RTT)) { + sb.append("Rtt"); + } return sb.toString(); } diff --git a/android/telecom/RemoteConnectionService.java b/android/telecom/RemoteConnectionService.java index 85906ad1..59ce5908 100644 --- a/android/telecom/RemoteConnectionService.java +++ b/android/telecom/RemoteConnectionService.java @@ -212,6 +212,9 @@ final class RemoteConnectionService { Session.Info sessionInfo) { } + @Override + public void onConnectionServiceFocusReleased(Session.Info sessionInfo) {} + @Override public void addConferenceCall( final String callId, ParcelableConference parcel, Session.Info sessionInfo) { diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java index 92d458f1..15355ac7 100644 --- a/android/telecom/TelecomManager.java +++ b/android/telecom/TelecomManager.java @@ -581,6 +581,22 @@ public class TelecomManager { public static final String EXTRA_CALL_BACK_INTENT = "android.telecom.extra.CALL_BACK_INTENT"; + /** + * The dialer activity responsible for placing emergency calls from, for example, a locked + * keyguard. + * @hide + */ + public static final ComponentName EMERGENCY_DIALER_COMPONENT = + ComponentName.createRelative("com.android.phone", ".EmergencyDialer"); + + /** + * The boolean indicated by this extra controls whether or not a call is eligible to undergo + * assisted dialing. This extra is stored under {@link #EXTRA_OUTGOING_CALL_EXTRAS}. + * @hide + */ + public static final String EXTRA_USE_ASSISTED_DIALING = + "android.telecom.extra.USE_ASSISTED_DIALING"; + /** * The following 4 constants define how properties such as phone numbers and names are * displayed to the user. @@ -588,7 +604,7 @@ public class TelecomManager { /** * Indicates that the address or number of a call is allowed to be displayed for caller ID. - */ + */ public static final int PRESENTATION_ALLOWED = 1; /** diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java index 69371a18..6a47d050 100644 --- a/android/telephony/CarrierConfigManager.java +++ b/android/telephony/CarrierConfigManager.java @@ -300,10 +300,10 @@ public class CarrierConfigManager { * If true all networks are considered as home network a.k.a non-roaming. When false, * the 2 pairs of CMDA and GSM roaming/non-roaming arrays are consulted. * - * @see KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY - * @see KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY - * @see KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY - * @see KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY + * @see #KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY + * @see #KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY + * @see #KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY + * @see #KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY */ public static final String KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool"; @@ -734,6 +734,13 @@ public class CarrierConfigManager { */ public static final String KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL = "show_iccid_in_sim_status_bool"; + /** + * Flag specifying whether the {@link android.telephony.SignalStrength} is shown in the SIM + * Status screen. The default value is true. + */ + public static final String KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL = + "show_signal_strength_in_sim_status_bool"; + /** * Flag specifying whether an additional (client initiated) intent needs to be sent on System * update @@ -989,6 +996,12 @@ public class CarrierConfigManager { public static final String KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL = "stk_disable_launch_browser_bool"; + /** + * Boolean indicating if show data RAT icon on status bar even when data is disabled + * @hide + */ + public static final String KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL = + "always_show_data_rat_icon_bool"; // These variables are used by the MMS service and exposed through another API, {@link // SmsManager}. The variable names and string values are copied from there. @@ -1296,6 +1309,19 @@ public class CarrierConfigManager { */ public static final String KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL = "allow_hold_in_ims_call"; + + /** + * Flag indicating whether the carrier always wants to play an "on-hold" tone when a call has + * been remotely held. + *

    + * When {@code true}, if the IMS stack indicates that the call session has been held, a signal + * will be sent from Telephony to play an audible "on-hold" tone played to the user. + * When {@code false}, a hold tone will only be played if the audio session becomes inactive. + * @hide + */ + public static final String KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL = + "always_play_remote_hold_tone_bool"; + /** * When true, indicates that adding a call is disabled when there is an ongoing video call * or when there is an ongoing call on wifi which was downgraded from video and VoWifi is @@ -1633,6 +1659,11 @@ public class CarrierConfigManager { public static final String KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL = "show_ims_registration_status_bool"; + /** + * Flag indicating whether the carrier supports RTT over IMS. + */ + public static final String KEY_RTT_SUPPORTED_BOOL = "rtt_supported_bool"; + /** * The flag to disable the popup dialog which warns the user of data charges. * @hide @@ -1686,12 +1717,20 @@ public class CarrierConfigManager { public static final String KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL = "spn_display_rule_use_roaming_from_service_state_bool"; + /** + * Determines whether any carrier has been identified and its specific config has been applied, + * default to false. + * @hide + */ + public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool"; + /** The default value for every variable. */ private final static PersistableBundle sDefaults; static { sDefaults = new PersistableBundle(); sDefaults.putBoolean(KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL, true); + sDefaults.putBoolean(KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL, false); sDefaults.putBoolean(KEY_ADDITIONAL_CALL_SETTING_BOOL, true); sDefaults.putBoolean(KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL, false); sDefaults.putBoolean(KEY_ALLOW_LOCAL_DTMF_TONES_BOOL, true); @@ -1768,6 +1807,7 @@ public class CarrierConfigManager { sDefaults.putString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING, ""); sDefaults.putStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY, null); sDefaults.putBoolean(KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL, false); + sDefaults.putBoolean(KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL, true); sDefaults.putBoolean(KEY_CI_ACTION_ON_SYS_UPDATE_BOOL, false); sDefaults.putString(KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING, ""); sDefaults.putString(KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_STRING, ""); @@ -1963,10 +2003,13 @@ public class CarrierConfigManager { sDefaults.putStringArray(KEY_NON_ROAMING_OPERATOR_STRING_ARRAY, null); sDefaults.putStringArray(KEY_ROAMING_OPERATOR_STRING_ARRAY, null); sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false); + sDefaults.putBoolean(KEY_RTT_SUPPORTED_BOOL, false); sDefaults.putBoolean(KEY_DISABLE_CHARGE_INDICATION_BOOL, false); sDefaults.putStringArray(KEY_FEATURE_ACCESS_CODES_STRING_ARRAY, null); sDefaults.putBoolean(KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL, false); sDefaults.putBoolean(KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL, false); + sDefaults.putBoolean(KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL, false); + sDefaults.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, false); } /** @@ -2011,6 +2054,33 @@ public class CarrierConfigManager { return getConfigForSubId(SubscriptionManager.getDefaultSubscriptionId()); } + /** + * Determines whether a configuration {@link PersistableBundle} obtained from + * {@link #getConfig()} or {@link #getConfigForSubId(int)} corresponds to an identified carrier. + *

    + * When an app receives the {@link CarrierConfigManager#ACTION_CARRIER_CONFIG_CHANGED} + * broadcast which informs it that the carrier configuration has changed, it is possible + * that another reload of the carrier configuration has begun since the intent was sent. + * In this case, the carrier configuration the app fetches (e.g. via {@link #getConfig()}) + * may not represent the configuration for the current carrier. It should be noted that it + * does not necessarily mean the configuration belongs to current carrier when this function + * return true because it may belong to another previous identified carrier. Users should + * always call {@link #getConfig()} or {@link #getConfigForSubId(int)} after receiving the + * broadcast {@link #ACTION_CARRIER_CONFIG_CHANGED}. + *

    + *

    + * After using {@link #getConfig()} or {@link #getConfigForSubId(int)} an app should always + * use this method to confirm whether any carrier specific configuration has been applied. + *

    + * + * @param bundle the configuration bundle to be checked. + * @return boolean true if any carrier specific configuration bundle has been applied, false + * otherwise or the bundle is null. + */ + public static boolean isConfigForIdentifiedCarrier(PersistableBundle bundle) { + return bundle != null && bundle.getBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL); + } + /** * Calling this method triggers telephony services to fetch the current carrier configuration. *

    @@ -2024,7 +2094,7 @@ public class CarrierConfigManager { * {@link android.service.carrier.CarrierService#onLoadConfig} will be called from an * arbitrary thread. *

    - * @see #hasCarrierPrivileges + * @see TelephonyManager#hasCarrierPrivileges */ public void notifyConfigChangedForSubId(int subId) { try { diff --git a/android/telephony/CellIdentityGsm.java b/android/telephony/CellIdentityGsm.java index c9684062..376e6aa7 100644 --- a/android/telephony/CellIdentityGsm.java +++ b/android/telephony/CellIdentityGsm.java @@ -202,7 +202,7 @@ public final class CellIdentityGsm implements Parcelable { * @return a 5 or 6 character string (MCC+MNC), null if any field is unknown */ public String getMobileNetworkOperator() { - return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr; + return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr; } /** diff --git a/android/telephony/CellIdentityLte.java b/android/telephony/CellIdentityLte.java index 825dcc33..6ca5daf6 100644 --- a/android/telephony/CellIdentityLte.java +++ b/android/telephony/CellIdentityLte.java @@ -213,7 +213,7 @@ public final class CellIdentityLte implements Parcelable { * @return a 5 or 6 character string (MCC+MNC), null if any field is unknown */ public String getMobileNetworkOperator() { - return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr; + return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr; } /** diff --git a/android/telephony/CellIdentityWcdma.java b/android/telephony/CellIdentityWcdma.java index e74b5701..e4bb4f29 100644 --- a/android/telephony/CellIdentityWcdma.java +++ b/android/telephony/CellIdentityWcdma.java @@ -208,7 +208,7 @@ public final class CellIdentityWcdma implements Parcelable { * @return a 5 or 6 character string (MCC+MNC), null if any field is unknown */ public String getMobileNetworkOperator() { - return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr; + return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr; } /** diff --git a/android/telephony/DisconnectCause.java b/android/telephony/DisconnectCause.java index c3a2ceb1..56e1e640 100644 --- a/android/telephony/DisconnectCause.java +++ b/android/telephony/DisconnectCause.java @@ -280,6 +280,36 @@ public class DisconnectCause { * {@hide} */ public static final int NORMAL_UNSPECIFIED = 65; + + /** + * Stk Call Control modified DIAL request to video DIAL request. + * {@hide} + */ + public static final int DIAL_MODIFIED_TO_DIAL_VIDEO = 66; + + /** + * Stk Call Control modified Video DIAL request to SS request. + * {@hide} + */ + public static final int DIAL_VIDEO_MODIFIED_TO_SS = 67; + + /** + * Stk Call Control modified Video DIAL request to USSD request. + * {@hide} + */ + public static final int DIAL_VIDEO_MODIFIED_TO_USSD = 68; + + /** + * Stk Call Control modified Video DIAL request to DIAL request. + * {@hide} + */ + public static final int DIAL_VIDEO_MODIFIED_TO_DIAL = 69; + + /** + * Stk Call Control modified Video DIAL request to Video DIAL request. + * {@hide} + */ + public static final int DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO = 70; //********************************************************************************************* // When adding a disconnect type: // 1) Update toString() with the newly added disconnect type. @@ -382,6 +412,16 @@ public class DisconnectCause { return "DIAL_MODIFIED_TO_SS"; case DIAL_MODIFIED_TO_DIAL: return "DIAL_MODIFIED_TO_DIAL"; + case DIAL_MODIFIED_TO_DIAL_VIDEO: + return "DIAL_MODIFIED_TO_DIAL_VIDEO"; + case DIAL_VIDEO_MODIFIED_TO_SS: + return "DIAL_VIDEO_MODIFIED_TO_SS"; + case DIAL_VIDEO_MODIFIED_TO_USSD: + return "DIAL_VIDEO_MODIFIED_TO_USSD"; + case DIAL_VIDEO_MODIFIED_TO_DIAL: + return "DIAL_VIDEO_MODIFIED_TO_DIAL"; + case DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO: + return "DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO"; case ERROR_UNSPECIFIED: return "ERROR_UNSPECIFIED"; case OUTGOING_FAILURE: diff --git a/android/telephony/MbmsDownloadSession.java b/android/telephony/MbmsDownloadSession.java index f392570e..059a2d07 100644 --- a/android/telephony/MbmsDownloadSession.java +++ b/android/telephony/MbmsDownloadSession.java @@ -347,6 +347,7 @@ public class MbmsDownloadSession implements AutoCloseable { @Override public void onServiceDisconnected(ComponentName name) { + Log.w(LOG_TAG, "bindAndInitialize: Remote service disconnected"); sIsInitialized.set(false); mService.set(null); } @@ -385,6 +386,7 @@ public class MbmsDownloadSession implements AutoCloseable { } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); + sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } } @@ -438,6 +440,7 @@ public class MbmsDownloadSession implements AutoCloseable { } } catch (RemoteException e) { mService.set(null); + sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } @@ -499,8 +502,10 @@ public class MbmsDownloadSession implements AutoCloseable { * Asynchronous errors through the callback may include any error not specific to the * streaming use-case. * @param request The request that specifies what should be downloaded. + * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, + * and some other error code otherwise. */ - public void download(@NonNull DownloadRequest request) { + public int download(@NonNull DownloadRequest request) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); @@ -516,12 +521,16 @@ public class MbmsDownloadSession implements AutoCloseable { setTempFileRootDirectory(tempRootDirectory); } - writeDownloadRequestToken(request); try { - downloadService.download(request); + int result = downloadService.download(request); + if (result == MbmsErrors.SUCCESS) { + writeDownloadRequestToken(request); + } + return result; } catch (RemoteException e) { mService.set(null); - sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + sIsInitialized.set(false); + return MbmsErrors.ERROR_MIDDLEWARE_LOST; } } @@ -542,6 +551,7 @@ public class MbmsDownloadSession implements AutoCloseable { return downloadService.listPendingDownloads(mSubscriptionId); } catch (RemoteException e) { mService.set(null); + sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return Collections.emptyList(); } @@ -560,8 +570,10 @@ public class MbmsDownloadSession implements AutoCloseable { * @param callback The callback that should be called when the middleware has information to * share on the download. * @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on. + * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, + * and some other error code otherwise. */ - public void registerStateCallback(@NonNull DownloadRequest request, + public int registerStateCallback(@NonNull DownloadRequest request, @NonNull DownloadStateCallback callback, @NonNull Handler handler) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { @@ -578,15 +590,15 @@ public class MbmsDownloadSession implements AutoCloseable { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } - sendErrorToApp(result, null); - return; + return result; } } catch (RemoteException e) { mService.set(null); - sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); - return; + sIsInitialized.set(false); + return MbmsErrors.ERROR_MIDDLEWARE_LOST; } mInternalDownloadCallbacks.put(callback, internalCallback); + return MbmsErrors.SUCCESS; } /** @@ -600,8 +612,10 @@ public class MbmsDownloadSession implements AutoCloseable { * * @param request The {@link DownloadRequest} provided during registration * @param callback The callback provided during registration. + * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, + * and some other error code otherwise. */ - public void unregisterStateCallback(@NonNull DownloadRequest request, + public int unregisterStateCallback(@NonNull DownloadRequest request, @NonNull DownloadStateCallback callback) { try { IMbmsDownloadService downloadService = mService.get(); @@ -611,6 +625,9 @@ public class MbmsDownloadSession implements AutoCloseable { InternalDownloadStateCallback internalCallback = mInternalDownloadCallbacks.get(callback); + if (internalCallback == null) { + throw new IllegalArgumentException("Provided callback was never registered"); + } try { int result = downloadService.unregisterStateCallback(request, internalCallback); @@ -618,11 +635,12 @@ public class MbmsDownloadSession implements AutoCloseable { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } - sendErrorToApp(result, null); + return result; } } catch (RemoteException e) { mService.set(null); - sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); + sIsInitialized.set(false); + return MbmsErrors.ERROR_MIDDLEWARE_LOST; } } finally { InternalDownloadStateCallback internalCallback = @@ -631,6 +649,7 @@ public class MbmsDownloadSession implements AutoCloseable { internalCallback.stop(); } } + return MbmsErrors.SUCCESS; } /** @@ -640,8 +659,10 @@ public class MbmsDownloadSession implements AutoCloseable { * this method will throw an {@link IllegalArgumentException}. * * @param downloadRequest The download request that you wish to cancel. + * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, + * and some other error code otherwise. */ - public void cancelDownload(@NonNull DownloadRequest downloadRequest) { + public int cancelDownload(@NonNull DownloadRequest downloadRequest) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); @@ -653,15 +674,15 @@ public class MbmsDownloadSession implements AutoCloseable { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } - sendErrorToApp(result, null); - return; + } else { + deleteDownloadRequestToken(downloadRequest); } + return result; } catch (RemoteException e) { mService.set(null); - sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); - return; + sIsInitialized.set(false); + return MbmsErrors.ERROR_MIDDLEWARE_LOST; } - deleteDownloadRequestToken(downloadRequest); } /** @@ -686,6 +707,7 @@ public class MbmsDownloadSession implements AutoCloseable { return downloadService.getDownloadStatus(downloadRequest, fileInfo); } catch (RemoteException e) { mService.set(null); + sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return STATUS_UNKNOWN; } @@ -727,6 +749,7 @@ public class MbmsDownloadSession implements AutoCloseable { } } catch (RemoteException e) { mService.set(null); + sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } } diff --git a/android/telephony/NetworkScan.java b/android/telephony/NetworkScan.java index f15fde8f..a2772126 100644 --- a/android/telephony/NetworkScan.java +++ b/android/telephony/NetworkScan.java @@ -19,50 +19,92 @@ package android.telephony; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; +import android.annotation.IntDef; import android.util.Log; import com.android.internal.telephony.ITelephony; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** - * Allows applications to request the system to perform a network scan. - * - * The caller of {@link #requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} will - * receive a NetworkScan which contains the callback method to stop the scan requested. - * @hide + * The caller of + * {@link TelephonyManager#requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} + * will receive an instance of {@link NetworkScan}, which contains a callback method + * {@link #stop()} for stopping the in-progress scan. */ public class NetworkScan { - public static final String TAG = "NetworkScan"; + private static final String TAG = "NetworkScan"; // Below errors are mapped from RadioError which is returned from RIL. We will consolidate // RadioErrors during the mapping if those RadioErrors mean no difference to the users. + + /** + * Defines acceptable values of scan error code. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ERROR_MODEM_ERROR, ERROR_INVALID_SCAN, ERROR_MODEM_UNAVAILABLE, ERROR_UNSUPPORTED, + ERROR_RADIO_INTERFACE_ERROR, ERROR_INVALID_SCANID, ERROR_INTERRUPTED}) + public @interface ScanErrorCode {} + + /** + * The RIL has successfully performed the network scan. + */ public static final int SUCCESS = 0; // RadioError:NONE + + /** + * The scan has failed due to some modem errors. + */ public static final int ERROR_MODEM_ERROR = 1; // RadioError:RADIO_NOT_AVAILABLE // RadioError:NO_MEMORY // RadioError:INTERNAL_ERR // RadioError:MODEM_ERR // RadioError:OPERATION_NOT_ALLOWED + + /** + * The parameters of the scan is invalid. + */ public static final int ERROR_INVALID_SCAN = 2; // RadioError:INVALID_ARGUMENTS - public static final int ERROR_MODEM_BUSY = 3; // RadioError:DEVICE_IN_USE + + /** + * The modem can not perform the scan because it is doing something else. + */ + public static final int ERROR_MODEM_UNAVAILABLE = 3; // RadioError:DEVICE_IN_USE + + /** + * The modem does not support the request scan. + */ public static final int ERROR_UNSUPPORTED = 4; // RadioError:REQUEST_NOT_SUPPORTED + // Below errors are generated at the Telephony. - public static final int ERROR_RIL_ERROR = 10000; // Nothing or only exception is - // returned from RIL. - public static final int ERROR_INVALID_SCANID = 10001; // The scanId is invalid. The user is - // either trying to stop a scan which - // does not exist or started by others. - public static final int ERROR_INTERRUPTED = 10002; // Scan was interrupted by another scan - // with higher priority. + + /** + * The RIL returns nothing or exceptions. + */ + public static final int ERROR_RADIO_INTERFACE_ERROR = 10000; + + /** + * The scan ID is invalid. The user is either trying to stop a scan which does not exist + * or started by others. + */ + public static final int ERROR_INVALID_SCANID = 10001; + + /** + * The scan has been interrupted by another scan with higher priority. + */ + public static final int ERROR_INTERRUPTED = 10002; + private final int mScanId; private final int mSubId; /** * Stops the network scan * - * This is the callback method to stop an ongoing scan. When user requests a new scan, - * a NetworkScan object will be returned, and the user can stop the scan by calling this - * method. + * Use this method to stop an ongoing scan. When user requests a new scan, a {@link NetworkScan} + * object will be returned, and the user can stop the scan by calling this method. */ public void stop() throws RemoteException { try { diff --git a/android/telephony/NetworkScanRequest.java b/android/telephony/NetworkScanRequest.java index 9674c930..ea503c3e 100644 --- a/android/telephony/NetworkScanRequest.java +++ b/android/telephony/NetworkScanRequest.java @@ -16,11 +16,14 @@ package android.telephony; +import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; import java.util.ArrayList; import java.util.Arrays; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines a request to peform a network scan. @@ -28,7 +31,6 @@ import java.util.Arrays; * This class defines whether the network scan will be performed only once or periodically until * cancelled, when the scan is performed periodically, the time interval is not controlled by the * user but defined by the modem vendor. - * @hide */ public final class NetworkScanRequest implements Parcelable { @@ -54,6 +56,14 @@ public final class NetworkScanRequest implements Parcelable { /** @hide */ public static final int MAX_INCREMENTAL_PERIODICITY_SEC = 10; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SCAN_TYPE_ONE_SHOT, + SCAN_TYPE_PERIODIC, + }) + public @interface ScanType {} + /** Performs the scan only once */ public static final int SCAN_TYPE_ONE_SHOT = 0; /** @@ -65,21 +75,21 @@ public final class NetworkScanRequest implements Parcelable { public static final int SCAN_TYPE_PERIODIC = 1; /** Defines the type of the scan. */ - public int scanType; + private int mScanType; /** * Search periodicity (in seconds). * Expected range for the input is [5s - 300s] - * This value must be less than or equal to maxSearchTime + * This value must be less than or equal to mMaxSearchTime */ - public int searchPeriodicity; + private int mSearchPeriodicity; /** * Maximum duration of the periodic search (in seconds). * Expected range for the input is [60s - 3600s] * If the search lasts this long, it will be terminated. */ - public int maxSearchTime; + private int mMaxSearchTime; /** * Indicates whether the modem should report incremental @@ -87,18 +97,18 @@ public final class NetworkScanRequest implements Parcelable { * FALSE – Incremental results are not reported. * TRUE (default) – Incremental results are reported */ - public boolean incrementalResults; + private boolean mIncrementalResults; /** * Indicates the periodicity with which the modem should * report incremental results to the client (in seconds). * Expected range for the input is [1s - 10s] - * This value must be less than or equal to maxSearchTime + * This value must be less than or equal to mMaxSearchTime */ - public int incrementalResultsPeriodicity; + private int mIncrementalResultsPeriodicity; /** Describes the radio access technologies with bands or channels that need to be scanned. */ - public RadioAccessSpecifier[] specifiers; + private RadioAccessSpecifier[] mSpecifiers; /** * Describes the List of PLMN ids (MCC-MNC) @@ -107,20 +117,24 @@ public final class NetworkScanRequest implements Parcelable { * If list not sent, search to be completed till end and all PLMNs found to be reported. * Max size of array is MAX_MCC_MNC_LIST_SIZE */ - public ArrayList mccMncs; + private ArrayList mMccMncs; /** - * Creates a new NetworkScanRequest with scanType and network specifiers + * Creates a new NetworkScanRequest with mScanType and network mSpecifiers * - * @param scanType The type of the scan + * @param scanType The type of the scan, can be either one shot or periodic * @param specifiers the radio network with bands / channels to be scanned - * @param searchPeriodicity Search periodicity (in seconds) - * @param maxSearchTime Maximum duration of the periodic search (in seconds) + * @param searchPeriodicity The modem will restart the scan every searchPeriodicity seconds if + * no network has been found, until it reaches the maxSearchTime. Only + * valid when scan type is periodic scan. + * @param maxSearchTime Maximum duration of the search (in seconds) * @param incrementalResults Indicates whether the modem should report incremental * results of the network scan to the client * @param incrementalResultsPeriodicity Indicates the periodicity with which the modem should - * report incremental results to the client (in seconds) - * @param mccMncs Describes the List of PLMN ids (MCC-MNC) + * report incremental results to the client (in seconds), + * only valid when incrementalResults is true + * @param mccMncs Describes the list of PLMN ids (MCC-MNC), once any network in the list has + * been found, the scan will be terminated by the modem. */ public NetworkScanRequest(int scanType, RadioAccessSpecifier[] specifiers, int searchPeriodicity, @@ -128,19 +142,63 @@ public final class NetworkScanRequest implements Parcelable { boolean incrementalResults, int incrementalResultsPeriodicity, ArrayList mccMncs) { - this.scanType = scanType; - this.specifiers = specifiers; - this.searchPeriodicity = searchPeriodicity; - this.maxSearchTime = maxSearchTime; - this.incrementalResults = incrementalResults; - this.incrementalResultsPeriodicity = incrementalResultsPeriodicity; - if (mccMncs != null) { - this.mccMncs = mccMncs; + this.mScanType = scanType; + this.mSpecifiers = specifiers.clone(); + this.mSearchPeriodicity = searchPeriodicity; + this.mMaxSearchTime = maxSearchTime; + this.mIncrementalResults = incrementalResults; + this.mIncrementalResultsPeriodicity = incrementalResultsPeriodicity; + if (mMccMncs != null) { + this.mMccMncs = (ArrayList) mccMncs.clone(); } else { - this.mccMncs = new ArrayList<>(); + this.mMccMncs = new ArrayList<>(); } } + /** Returns the type of the scan. */ + @ScanType + public int getScanType() { + return mScanType; + } + + /** Returns the search periodicity in seconds. */ + public int getSearchPeriodicity() { + return mSearchPeriodicity; + } + + /** Returns maximum duration of the periodic search in seconds. */ + public int getMaxSearchTime() { + return mMaxSearchTime; + } + + /** + * Returns whether incremental result is enabled. + * FALSE – Incremental results is not enabled. + * TRUE – Incremental results is reported. + */ + public boolean getIncrementalResults() { + return mIncrementalResults; + } + + /** Returns the periodicity in seconds of incremental results. */ + public int getIncrementalResultsPeriodicity() { + return mIncrementalResultsPeriodicity; + } + + /** Returns the radio access technologies with bands or channels that need to be scanned. */ + public RadioAccessSpecifier[] getSpecifiers() { + return mSpecifiers.clone(); + } + + /** + * Returns the List of PLMN ids (MCC-MNC) for early termination of scan. + * If any PLMN of this list is found, search should end at that point and + * results with all PLMN found till that point should be sent as response. + */ + public ArrayList getPlmns() { + return (ArrayList) mMccMncs.clone(); + } + @Override public int describeContents() { return 0; @@ -148,26 +206,26 @@ public final class NetworkScanRequest implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(scanType); - dest.writeParcelableArray(specifiers, flags); - dest.writeInt(searchPeriodicity); - dest.writeInt(maxSearchTime); - dest.writeBoolean(incrementalResults); - dest.writeInt(incrementalResultsPeriodicity); - dest.writeStringList(mccMncs); + dest.writeInt(mScanType); + dest.writeParcelableArray(mSpecifiers, flags); + dest.writeInt(mSearchPeriodicity); + dest.writeInt(mMaxSearchTime); + dest.writeBoolean(mIncrementalResults); + dest.writeInt(mIncrementalResultsPeriodicity); + dest.writeStringList(mMccMncs); } private NetworkScanRequest(Parcel in) { - scanType = in.readInt(); - specifiers = (RadioAccessSpecifier[]) in.readParcelableArray( + mScanType = in.readInt(); + mSpecifiers = (RadioAccessSpecifier[]) in.readParcelableArray( Object.class.getClassLoader(), RadioAccessSpecifier.class); - searchPeriodicity = in.readInt(); - maxSearchTime = in.readInt(); - incrementalResults = in.readBoolean(); - incrementalResultsPeriodicity = in.readInt(); - mccMncs = new ArrayList<>(); - in.readStringList(mccMncs); + mSearchPeriodicity = in.readInt(); + mMaxSearchTime = in.readInt(); + mIncrementalResults = in.readBoolean(); + mIncrementalResultsPeriodicity = in.readInt(); + mMccMncs = new ArrayList<>(); + in.readStringList(mMccMncs); } @Override @@ -184,25 +242,25 @@ public final class NetworkScanRequest implements Parcelable { return false; } - return (scanType == nsr.scanType - && Arrays.equals(specifiers, nsr.specifiers) - && searchPeriodicity == nsr.searchPeriodicity - && maxSearchTime == nsr.maxSearchTime - && incrementalResults == nsr.incrementalResults - && incrementalResultsPeriodicity == nsr.incrementalResultsPeriodicity - && (((mccMncs != null) - && mccMncs.equals(nsr.mccMncs)))); + return (mScanType == nsr.mScanType + && Arrays.equals(mSpecifiers, nsr.mSpecifiers) + && mSearchPeriodicity == nsr.mSearchPeriodicity + && mMaxSearchTime == nsr.mMaxSearchTime + && mIncrementalResults == nsr.mIncrementalResults + && mIncrementalResultsPeriodicity == nsr.mIncrementalResultsPeriodicity + && (((mMccMncs != null) + && mMccMncs.equals(nsr.mMccMncs)))); } @Override public int hashCode () { - return ((scanType * 31) - + (Arrays.hashCode(specifiers)) * 37 - + (searchPeriodicity * 41) - + (maxSearchTime * 43) - + ((incrementalResults == true? 1 : 0) * 47) - + (incrementalResultsPeriodicity * 53) - + (mccMncs.hashCode() * 59)); + return ((mScanType * 31) + + (Arrays.hashCode(mSpecifiers)) * 37 + + (mSearchPeriodicity * 41) + + (mMaxSearchTime * 43) + + ((mIncrementalResults == true? 1 : 0) * 47) + + (mIncrementalResultsPeriodicity * 53) + + (mMccMncs.hashCode() * 59)); } public static final Creator CREATOR = diff --git a/android/telephony/PhoneStateListener.java b/android/telephony/PhoneStateListener.java index 9ccfa942..c7e51310 100644 --- a/android/telephony/PhoneStateListener.java +++ b/android/telephony/PhoneStateListener.java @@ -408,7 +408,7 @@ public class PhoneStateListener { /** * Callback invoked when device call state changes. * @param state call state - * @param incomingNumber incoming call phone number. If application does not have + * @param phoneNumber call phone number. If application does not have * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} permission, an empty * string will be passed as an argument. * @@ -416,7 +416,7 @@ public class PhoneStateListener { * @see TelephonyManager#CALL_STATE_RINGING * @see TelephonyManager#CALL_STATE_OFFHOOK */ - public void onCallStateChanged(int state, String incomingNumber) { + public void onCallStateChanged(int state, String phoneNumber) { // default implementation empty } diff --git a/android/telephony/RadioAccessSpecifier.java b/android/telephony/RadioAccessSpecifier.java index 33ce8b42..5412c617 100644 --- a/android/telephony/RadioAccessSpecifier.java +++ b/android/telephony/RadioAccessSpecifier.java @@ -25,34 +25,40 @@ import java.util.Arrays; * Describes a particular radio access network to be scanned. * * The scan can be performed on either bands or channels for a specific radio access network type. - * @hide */ public final class RadioAccessSpecifier implements Parcelable { /** * The radio access network that needs to be scanned * + * This parameter must be provided or else the scan will be rejected. + * * See {@link RadioNetworkConstants.RadioAccessNetworks} for details. */ - public int radioAccessNetwork; + private int mRadioAccessNetwork; /** * The frequency bands that need to be scanned * - * bands must be used together with radioAccessNetwork + * When no specific bands are specified (empty array or null), all the frequency bands + * supported by the modem will be scanned. * * See {@link RadioNetworkConstants} for details. */ - public int[] bands; + private int[] mBands; /** * The frequency channels that need to be scanned * - * channels must be used together with radioAccessNetwork + * When any specific channels are provided for scan, the corresponding frequency bands that + * contains those channels must also be provided, or else the channels will be ignored. * - * See {@link RadioNetworkConstants.RadioAccessNetworks} for details. + * When no specific channels are specified (empty array or null), all the frequency channels + * supported by the modem will be scanned. + * + * See {@link RadioNetworkConstants} for details. */ - public int[] channels; + private int[] mChannels; /** * Creates a new RadioAccessSpecifier with radio network, bands and channels @@ -65,9 +71,34 @@ public final class RadioAccessSpecifier implements Parcelable { * @param channels the frequency bands to be scanned */ public RadioAccessSpecifier(int ran, int[] bands, int[] channels) { - this.radioAccessNetwork = ran; - this.bands = bands; - this.channels = channels; + this.mRadioAccessNetwork = ran; + this.mBands = bands.clone(); + this.mChannels = channels.clone(); + } + + /** + * Returns the radio access network that needs to be scanned. + * + * The returned value is define in {@link RadioNetworkConstants.RadioAccessNetworks}; + */ + public int getRadioAccessNetwork() { + return mRadioAccessNetwork; + } + + /** + * Returns the frequency bands that need to be scanned. + * + * The returned value is defined in either of {@link RadioNetworkConstants.GeranBands}, + * {@link RadioNetworkConstants.UtranBands} and {@link RadioNetworkConstants.EutranBands}, and + * it depends on the returned value of {@link #getRadioAccessNetwork()}. + */ + public int[] getBands() { + return mBands.clone(); + } + + /** Returns the frequency channels that need to be scanned. */ + public int[] getChannels() { + return mChannels.clone(); } public static final Parcelable.Creator CREATOR = @@ -90,15 +121,15 @@ public final class RadioAccessSpecifier implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(radioAccessNetwork); - dest.writeIntArray(bands); - dest.writeIntArray(channels); + dest.writeInt(mRadioAccessNetwork); + dest.writeIntArray(mBands); + dest.writeIntArray(mChannels); } private RadioAccessSpecifier(Parcel in) { - radioAccessNetwork = in.readInt(); - bands = in.createIntArray(); - channels = in.createIntArray(); + mRadioAccessNetwork = in.readInt(); + mBands = in.createIntArray(); + mChannels = in.createIntArray(); } @Override @@ -115,15 +146,15 @@ public final class RadioAccessSpecifier implements Parcelable { return false; } - return (radioAccessNetwork == ras.radioAccessNetwork - && Arrays.equals(bands, ras.bands) - && Arrays.equals(channels, ras.channels)); + return (mRadioAccessNetwork == ras.mRadioAccessNetwork + && Arrays.equals(mBands, ras.mBands) + && Arrays.equals(mChannels, ras.mChannels)); } @Override public int hashCode () { - return ((radioAccessNetwork * 31) - + (Arrays.hashCode(bands) * 37) - + (Arrays.hashCode(channels)) * 39); + return ((mRadioAccessNetwork * 31) + + (Arrays.hashCode(mBands) * 37) + + (Arrays.hashCode(mChannels)) * 39); } } diff --git a/android/telephony/RadioNetworkConstants.java b/android/telephony/RadioNetworkConstants.java index 1a9072d3..5f5dd82e 100644 --- a/android/telephony/RadioNetworkConstants.java +++ b/android/telephony/RadioNetworkConstants.java @@ -18,7 +18,6 @@ package android.telephony; /** * Contains radio access network related constants. - * @hide */ public final class RadioNetworkConstants { diff --git a/android/telephony/SmsManager.java b/android/telephony/SmsManager.java index fdedf758..5d88cf07 100644 --- a/android/telephony/SmsManager.java +++ b/android/telephony/SmsManager.java @@ -344,6 +344,7 @@ public final class SmsManager { *

    * *

    Requires Permission: + * {@link android.Manifest.permission#SEND_SMS} and * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier * privileges. *

    @@ -351,6 +352,10 @@ public final class SmsManager { * @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent) */ @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.MODIFY_PHONE_STATE, + android.Manifest.permission.SEND_SMS + }) public void sendTextMessageWithoutPersisting( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { @@ -390,112 +395,6 @@ public final class SmsManager { } /** - * Send a text based SMS with messaging options. - * - * @param destinationAddress the address to send the message to - * @param scAddress is the service center address or null to use - * the current default SMSC - * @param text the body of the message to send - * @param sentIntent if not NULL this PendingIntent is - * broadcast when the message is successfully sent, or failed. - * The result code will be Activity.RESULT_OK for success, - * or one of these errors:
    - * RESULT_ERROR_GENERIC_FAILURE
    - * RESULT_ERROR_RADIO_OFF
    - * RESULT_ERROR_NULL_PDU
    - * For RESULT_ERROR_GENERIC_FAILURE the sentIntent may include - * the extra "errorCode" containing a radio technology specific value, - * generally only useful for troubleshooting.
    - * 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 PendingIntent is - * broadcast when the message is delivered to the recipient. The - * raw pdu of the status report is in the extended data ("pdu"). - * @param priority Priority level of the message - * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1 - * --------------------------------- - * PRIORITY | Level of Priority - * --------------------------------- - * '00' | Normal - * '01' | Interactive - * '10' | Urgent - * '11' | Emergency - * ---------------------------------- - * Any Other values included Negative considered as Invalid Priority Indicator of the message. - * @param expectMore is a boolean to indicate the sending messages through same link or not. - * @param validityPeriod Validity Period of the message in mins. - * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1. - * Validity Period(Minimum) -> 5 mins - * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks). - * Any Other values included Negative considered as Invalid Validity Period of the message. - * - * @throws IllegalArgumentException if destinationAddress or text are empty - * {@hide} - */ - public void sendTextMessage( - String destinationAddress, String scAddress, String text, - PendingIntent sentIntent, PendingIntent deliveryIntent, - int priority, boolean expectMore, int validityPeriod) { - sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent, - true /* persistMessage*/, priority, expectMore, validityPeriod); - } - - private void sendTextMessageInternal( - String destinationAddress, String scAddress, String text, - PendingIntent sentIntent, PendingIntent deliveryIntent, boolean persistMessage, - int priority, boolean expectMore, int validityPeriod) { - if (TextUtils.isEmpty(destinationAddress)) { - throw new IllegalArgumentException("Invalid destinationAddress"); - } - - if (TextUtils.isEmpty(text)) { - throw new IllegalArgumentException("Invalid message body"); - } - - if (priority < 0x00 || priority > 0x03) { - throw new IllegalArgumentException("Invalid priority"); - } - - if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) { - throw new IllegalArgumentException("Invalid validity period"); - } - - try { - ISms iccISms = getISmsServiceOrThrow(); - if (iccISms != null) { - iccISms.sendTextForSubscriberWithOptions(getSubscriptionId(), - ActivityThread.currentPackageName(), destinationAddress, scAddress, text, - sentIntent, deliveryIntent, persistMessage, priority, expectMore, - validityPeriod); - } - } catch (RemoteException ex) { - // ignore it - } - } - - /** - * Send a text based SMS without writing it into the SMS Provider. - * - *

    Requires Permission: - * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier - * privileges. - *

    - * - * @see #sendTextMessage(String, String, String, PendingIntent, - * PendingIntent, int, boolean, int) - * @hide - */ - public void sendTextMessageWithoutPersisting( - String destinationAddress, String scAddress, String text, - PendingIntent sentIntent, PendingIntent deliveryIntent, int priority, - boolean expectMore, int validityPeriod) { - sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent, - false /* persistMessage */, priority, expectMore, validityPeriod); - } - - /** - * * Inject an SMS PDU into the android application framework. * *

    Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier @@ -653,140 +552,6 @@ public final class SmsManager { } /** - * Send a multi-part text based SMS with messaging options. The callee should have already - * divided the message into correctly sized parts by calling - * divideMessage. - * - *

    Note: Using this method requires that your app has the - * {@link android.Manifest.permission#SEND_SMS} permission.

    - * - *

    Note: Beginning with Android 4.4 (API level 19), if - * and only if an app is not selected as the default SMS app, the system automatically - * writes messages sent using this method to the SMS Provider (the default SMS app is always - * responsible for writing its sent messages to the SMS Provider). For information about - * how to behave as the default SMS app, see {@link android.provider.Telephony}.

    - * - * @param destinationAddress the address to send the message to - * @param scAddress is the service center address or null to use - * the current default SMSC - * @param parts an ArrayList of strings that, in order, - * comprise the original message - * @param sentIntents if not null, an ArrayList of - * PendingIntents (one for each message part) that is - * broadcast when the corresponding message part has been sent. - * The result code will be Activity.RESULT_OK for success, - * or one of these errors:
    - * RESULT_ERROR_GENERIC_FAILURE
    - * RESULT_ERROR_RADIO_OFF
    - * RESULT_ERROR_NULL_PDU
    - * For RESULT_ERROR_GENERIC_FAILURE each sentIntent may include - * the extra "errorCode" containing a radio technology specific value, - * generally only useful for troubleshooting.
    - * 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 ArrayList of - * PendingIntents (one for each message part) that is - * broadcast when the corresponding message part has been delivered - * to the recipient. The raw pdu of the status report is in the - * extended data ("pdu"). - * @param priority Priority level of the message - * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1 - * --------------------------------- - * PRIORITY | Level of Priority - * --------------------------------- - * '00' | Normal - * '01' | Interactive - * '10' | Urgent - * '11' | Emergency - * ---------------------------------- - * Any Other values included Negative considered as Invalid Priority Indicator of the message. - * @param expectMore is a boolean to indicate the sending messages through same link or not. - * @param validityPeriod Validity Period of the message in mins. - * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1. - * Validity Period(Minimum) -> 5 mins - * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks). - * Any Other values included Negative considered as Invalid Validity Period of the message. - * - * @throws IllegalArgumentException if destinationAddress or data are empty - * {@hide} - */ - public void sendMultipartTextMessage( - String destinationAddress, String scAddress, ArrayList parts, - ArrayList sentIntents, ArrayList deliveryIntents, - int priority, boolean expectMore, int validityPeriod) { - sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents, - deliveryIntents, true /* persistMessage*/); - } - - private void sendMultipartTextMessageInternal( - String destinationAddress, String scAddress, List parts, - List sentIntents, List deliveryIntents, - boolean persistMessage, int priority, boolean expectMore, int validityPeriod) { - if (TextUtils.isEmpty(destinationAddress)) { - throw new IllegalArgumentException("Invalid destinationAddress"); - } - if (parts == null || parts.size() < 1) { - throw new IllegalArgumentException("Invalid message body"); - } - - if (priority < 0x00 || priority > 0x03) { - throw new IllegalArgumentException("Invalid priority"); - } - - if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) { - throw new IllegalArgumentException("Invalid validity period"); - } - - if (parts.size() > 1) { - try { - ISms iccISms = getISmsServiceOrThrow(); - if (iccISms != null) { - iccISms.sendMultipartTextForSubscriberWithOptions(getSubscriptionId(), - ActivityThread.currentPackageName(), destinationAddress, scAddress, - parts, sentIntents, deliveryIntents, persistMessage, priority, - expectMore, validityPeriod); - } - } catch (RemoteException ex) { - // ignore it - } - } else { - PendingIntent sentIntent = null; - PendingIntent deliveryIntent = null; - if (sentIntents != null && sentIntents.size() > 0) { - sentIntent = sentIntents.get(0); - } - if (deliveryIntents != null && deliveryIntents.size() > 0) { - deliveryIntent = deliveryIntents.get(0); - } - sendTextMessageInternal(destinationAddress, scAddress, parts.get(0), - sentIntent, deliveryIntent, persistMessage, priority, expectMore, - validityPeriod); - } - } - - /** - * Send a multi-part text based SMS without writing it into the SMS Provider. - * - *

    Requires Permission: - * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier - * privileges. - *

    - * - * @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, - * ArrayList, int, boolean, int) - * @hide - **/ - public void sendMultipartTextMessageWithoutPersisting( - String destinationAddress, String scAddress, List parts, - List sentIntents, List deliveryIntents, - int priority, boolean expectMore, int validityPeriod) { - sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents, - deliveryIntents, false /* persistMessage*/, priority, expectMore, - validityPeriod); - } - - /** * Send a data based SMS to a specific application port. * *

    Note: Using this method requires that your app has the @@ -1249,7 +1014,7 @@ public final class SmsManager { * getAllMessagesFromIcc * @return ArrayList of SmsMessage objects. */ - private ArrayList createMessageListFromRawRecords(List records) { + private static ArrayList createMessageListFromRawRecords(List records) { ArrayList messages = new ArrayList(); if (records != null) { int count = records.size(); @@ -1257,8 +1022,7 @@ public final class SmsManager { SmsRawData data = records.get(i); // List contains all records, including "free" records (null) if (data != null) { - SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes(), - getSubscriptionId()); + SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes()); if (sms != null) { messages.add(sms); } @@ -1370,6 +1134,8 @@ public final class SmsManager { // SMS send failure result codes + /** No error. {@hide}*/ + static public final int RESULT_ERROR_NONE = 0; /** Generic failure cause */ static public final int RESULT_ERROR_GENERIC_FAILURE = 1; /** Failed because radio was explicitly turned off */ diff --git a/android/telephony/SmsMessage.java b/android/telephony/SmsMessage.java index a5d67c60..a6dbc066 100644 --- a/android/telephony/SmsMessage.java +++ b/android/telephony/SmsMessage.java @@ -19,6 +19,7 @@ package android.telephony; import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; import android.annotation.StringDef; +import android.app.PendingIntent; import android.content.res.Resources; import android.os.Binder; import android.text.TextUtils; @@ -83,17 +84,22 @@ public class SmsMessage { public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153; /** @hide */ - @StringDef({FORMAT_3GPP, FORMAT_3GPP2}) + @StringDef(prefix = { "FORMAT_" }, value = { + FORMAT_3GPP, + FORMAT_3GPP2 + }) @Retention(RetentionPolicy.SOURCE) public @interface Format {} /** * Indicates a 3GPP format SMS message. + * @see SmsManager#injectSmsPdu(byte[], String, PendingIntent) */ public static final String FORMAT_3GPP = "3gpp"; /** * Indicates a 3GPP2 format SMS message. + * @see SmsManager#injectSmsPdu(byte[], String, PendingIntent) */ public static final String FORMAT_3GPP2 = "3gpp2"; @@ -270,31 +276,6 @@ public class SmsMessage { } } - /** - * Create an SmsMessage from an SMS EF record. - * - * @param index Index of SMS record. This should be index in ArrayList - * returned by SmsManager.getAllMessagesFromSim + 1. - * @param data Record data. - * @param subId Subscription Id of the SMS - * @return An SmsMessage representing the record. - * - * @hide - */ - public static SmsMessage createFromEfRecord(int index, byte[] data, int subId) { - SmsMessageBase wrappedMessage; - - if (isCdmaVoice(subId)) { - wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord( - index, data); - } else { - wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromEfRecord( - index, data); - } - - return wrappedMessage != null ? new SmsMessage(wrappedMessage) : null; - } - /** * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the * length in bytes (not hex chars) less the SMSC header @@ -847,7 +828,6 @@ public class SmsMessage { int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(subId); return (PHONE_TYPE_CDMA == activePhone); } - /** * Decide if the carrier supports long SMS. * {@hide} diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java index 81806e52..af5b1908 100644 --- a/android/telephony/TelephonyManager.java +++ b/android/telephony/TelephonyManager.java @@ -29,7 +29,6 @@ import android.annotation.SystemService; import android.annotation.WorkerThread; import android.app.ActivityThread; import android.app.PendingIntent; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; @@ -43,7 +42,6 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.SystemProperties; -import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.service.carrier.CarrierIdentifier; import android.telecom.PhoneAccount; @@ -979,6 +977,63 @@ public class TelephonyManager { */ public static final int CDMA_ROAMING_MODE_ANY = 2; + /** + * An unknown carrier id. It could either be subscription unavailable or the subscription + * carrier cannot be recognized. Unrecognized carriers here means + * {@link #getSimOperator() MCC+MNC} cannot be identified. + */ + public static final int UNKNOWN_CARRIER_ID = -1; + + /** + * Broadcast Action: The subscription carrier identity has changed. + * This intent could be sent on the following events: + *

      + *
    • Subscription absent. Carrier identity could change from a valid id to + * {@link TelephonyManager#UNKNOWN_CARRIER_ID}.
    • + *
    • Subscription loaded. Carrier identity could change from + * {@link TelephonyManager#UNKNOWN_CARRIER_ID} to a valid id.
    • + *
    • The subscription carrier is recognized after a remote update.
    • + *
    + * The intent will have the following extra values: + *
      + *
    • {@link #EXTRA_CARRIER_ID} The up-to-date carrier id of the current subscription id. + *
    • + *
    • {@link #EXTRA_CARRIER_NAME} The up-to-date carrier name of the current subscription. + *
    • + *
    • {@link #EXTRA_SUBSCRIPTION_ID} The subscription id associated with the changed carrier + * identity. + *
    • + *
    + *

    This is a protected intent that can only be sent by the system. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED = + "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED"; + + /** + * An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which indicates + * the updated carrier id {@link TelephonyManager#getSubscriptionCarrierId()} of the current + * subscription. + *

    Will be {@link TelephonyManager#UNKNOWN_CARRIER_ID} if the subscription is unavailable or + * the carrier cannot be identified. + */ + public static final String EXTRA_CARRIER_ID = "android.telephony.extra.CARRIER_ID"; + + /** + * An string extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which + * indicates the updated carrier name of the current subscription. + * {@see TelephonyManager#getSubscriptionCarrierName()} + *

    Carrier name is a user-facing name of the carrier id {@link #EXTRA_CARRIER_ID}, + * usually the brand name of the subsidiary (e.g. T-Mobile). + */ + public static final String EXTRA_CARRIER_NAME = "android.telephony.extra.CARRIER_NAME"; + + /** + * An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} to indicate the + * subscription which has changed. + */ + public static final String EXTRA_SUBSCRIPTION_ID = "android.telephony.extra.SUBSCRIPTION_ID"; + // // // Device Info @@ -1121,12 +1176,14 @@ public class TelephonyManager { } /** - * Returns the NAI. Return null if NAI is not available. - * + * Returns the Network Access Identifier (NAI). Return null if NAI is not available. + *

    + * Requires Permission: + * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} */ - /** {@hide}*/ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getNai() { - return getNai(getSlotIndex()); + return getNaiBySubscriberId(getSubId()); } /** @@ -1137,11 +1194,18 @@ public class TelephonyManager { /** {@hide}*/ public String getNai(int slotIndex) { int[] subId = SubscriptionManager.getSubId(slotIndex); + if (subId == null) { + return null; + } + return getNaiBySubscriberId(subId[0]); + } + + private String getNaiBySubscriberId(int subId) { try { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; - String nai = info.getNaiForSubscriber(subId[0], mContext.getOpPackageName()); + String nai = info.getNaiForSubscriber(subId, mContext.getOpPackageName()); if (Log.isLoggable(TAG, Log.VERBOSE)) { Rlog.v(TAG, "Nai = " + nai); } @@ -3091,6 +3155,7 @@ public class TelephonyManager { * Initial SIM activation state, unknown. Not set by any carrier apps. * @hide */ + @SystemApi public static final int SIM_ACTIVATION_STATE_UNKNOWN = 0; /** @@ -3101,12 +3166,14 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_RESTRICTED * @hide */ + @SystemApi public static final int SIM_ACTIVATION_STATE_ACTIVATING = 1; /** * Indicate SIM has been successfully activated with full service * @hide */ + @SystemApi public static final int SIM_ACTIVATION_STATE_ACTIVATED = 2; /** @@ -3116,6 +3183,7 @@ public class TelephonyManager { * deactivated sim state and set it back to activated after successfully run activation service. * @hide */ + @SystemApi public static final int SIM_ACTIVATION_STATE_DEACTIVATED = 3; /** @@ -3123,14 +3191,47 @@ public class TelephonyManager { * note this is currently available for data activation state. For example out of byte sim. * @hide */ + @SystemApi public static final int SIM_ACTIVATION_STATE_RESTRICTED = 4; + /** @hide */ + @IntDef({ + SIM_ACTIVATION_STATE_UNKNOWN, + SIM_ACTIVATION_STATE_ACTIVATING, + SIM_ACTIVATION_STATE_ACTIVATED, + SIM_ACTIVATION_STATE_DEACTIVATED, + SIM_ACTIVATION_STATE_RESTRICTED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SimActivationState{} + + /** + * Sets the voice activation state + * + *

    Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} + * Or the calling app has carrier privileges. + * + * @param activationState The voice activation state + * @see #SIM_ACTIVATION_STATE_UNKNOWN + * @see #SIM_ACTIVATION_STATE_ACTIVATING + * @see #SIM_ACTIVATION_STATE_ACTIVATED + * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #hasCarrierPrivileges + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setVoiceActivationState(@SimActivationState int activationState) { + setVoiceActivationState(getSubId(), activationState); + } + /** * Sets the voice activation state for the given subscriber. * *

    Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} - * Or the calling app has carrier privileges. @see #hasCarrierPrivileges + * Or the calling app has carrier privileges. * * @param subId The subscription id. * @param activationState The voice activation state of the given subscriber. @@ -3138,24 +3239,48 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATING * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #hasCarrierPrivileges * @hide */ - public void setVoiceActivationState(int subId, int activationState) { + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setVoiceActivationState(int subId, @SimActivationState int activationState) { try { - ITelephony telephony = getITelephony(); - if (telephony != null) - telephony.setVoiceActivationState(subId, activationState); - } catch (RemoteException ex) { - } catch (NullPointerException ex) { - } + ITelephony telephony = getITelephony(); + if (telephony != null) + telephony.setVoiceActivationState(subId, activationState); + } catch (RemoteException ex) { + } catch (NullPointerException ex) { + } + } + + /** + * Sets the data activation state + * + *

    Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} + * Or the calling app has carrier privileges. + * + * @param activationState The data activation state + * @see #SIM_ACTIVATION_STATE_UNKNOWN + * @see #SIM_ACTIVATION_STATE_ACTIVATING + * @see #SIM_ACTIVATION_STATE_ACTIVATED + * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #SIM_ACTIVATION_STATE_RESTRICTED + * @see #hasCarrierPrivileges + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setDataActivationState(@SimActivationState int activationState) { + setDataActivationState(getSubId(), activationState); } /** * Sets the data activation state for the given subscriber. * *

    Requires Permission: - * {@link android.Manifest.permission#MODIFY_PHONE_STATE} - * Or the calling app has carrier privileges. @see #hasCarrierPrivileges + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} + * Or the calling app has carrier privileges. * * @param subId The subscription id. * @param activationState The data activation state of the given subscriber. @@ -3164,9 +3289,11 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED * @see #SIM_ACTIVATION_STATE_RESTRICTED + * @see #hasCarrierPrivileges * @hide */ - public void setDataActivationState(int subId, int activationState) { + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setDataActivationState(int subId, @SimActivationState int activationState) { try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -3176,9 +3303,34 @@ public class TelephonyManager { } } + /** + * Returns the voice activation state + * + *

    Requires Permission: + * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * Or the calling app has carrier privileges. + * + * @return voiceActivationState + * @see #SIM_ACTIVATION_STATE_UNKNOWN + * @see #SIM_ACTIVATION_STATE_ACTIVATING + * @see #SIM_ACTIVATION_STATE_ACTIVATED + * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #hasCarrierPrivileges + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + public @SimActivationState int getVoiceActivationState() { + return getVoiceActivationState(getSubId()); + } + /** * Returns the voice activation state for the given subscriber. * + *

    Requires Permission: + * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * Or the calling app has carrier privileges. + * * @param subId The subscription id. * * @return voiceActivationState for the given subscriber @@ -3186,10 +3338,11 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATING * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #hasCarrierPrivileges * @hide */ - @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - public int getVoiceActivationState(int subId) { + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + public @SimActivationState int getVoiceActivationState(int subId) { try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -3200,9 +3353,35 @@ public class TelephonyManager { return SIM_ACTIVATION_STATE_UNKNOWN; } + /** + * Returns the data activation state + * + *

    Requires Permission: + * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * Or the calling app has carrier privileges. + * + * @return dataActivationState for the given subscriber + * @see #SIM_ACTIVATION_STATE_UNKNOWN + * @see #SIM_ACTIVATION_STATE_ACTIVATING + * @see #SIM_ACTIVATION_STATE_ACTIVATED + * @see #SIM_ACTIVATION_STATE_DEACTIVATED + * @see #SIM_ACTIVATION_STATE_RESTRICTED + * @see #hasCarrierPrivileges + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + public @SimActivationState int getDataActivationState() { + return getDataActivationState(getSubId()); + } + /** * Returns the data activation state for the given subscriber. * + *

    Requires Permission: + * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} + * Or the calling app has carrier privileges. + * * @param subId The subscription id. * * @return dataActivationState for the given subscriber @@ -3211,10 +3390,11 @@ public class TelephonyManager { * @see #SIM_ACTIVATION_STATE_ACTIVATED * @see #SIM_ACTIVATION_STATE_DEACTIVATED * @see #SIM_ACTIVATION_STATE_RESTRICTED + * @see #hasCarrierPrivileges * @hide */ - @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - public int getDataActivationState(int subId) { + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + public @SimActivationState int getDataActivationState(int subId) { try { ITelephony telephony = getITelephony(); if (telephony != null) @@ -4785,15 +4965,14 @@ public class TelephonyManager { * Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * Or the calling app has carrier privileges. @see #hasCarrierPrivileges - * - * @hide - * TODO: Add an overload that takes no args. */ - public void setNetworkSelectionModeAutomatic(int subId) { + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setNetworkSelectionModeAutomatic() { try { ITelephony telephony = getITelephony(); - if (telephony != null) - telephony.setNetworkSelectionModeAutomatic(subId); + if (telephony != null) { + telephony.setNetworkSelectionModeAutomatic(getSubId()); + } } catch (RemoteException ex) { Rlog.e(TAG, "setNetworkSelectionModeAutomatic RemoteException", ex); } catch (NullPointerException ex) { @@ -4841,9 +5020,9 @@ public class TelephonyManager { * * @param request Contains all the RAT with bands/channels that need to be scanned. * @param callback Returns network scan results or errors. - * @return A NetworkScan obj which contains a callback which can stop the scan. - * @hide + * @return A NetworkScan obj which contains a callback which can be used to stop the scan. */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public NetworkScan requestNetworkScan( NetworkScanRequest request, TelephonyScanManager.NetworkScanCallback callback) { synchronized (this) { @@ -4862,15 +5041,20 @@ public class TelephonyManager { * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} * Or the calling app has carrier privileges. @see #hasCarrierPrivileges * - * @hide - * TODO: Add an overload that takes no args. + * @param operatorNumeric the PLMN ID of the network to select. + * @param persistSelection whether the selection will persist until reboot. If true, only allows + * attaching to the selected PLMN until reboot; otherwise, attach to the chosen PLMN and resume + * normal network selection next time. + * @return true on success; false on any failure. */ - public boolean setNetworkSelectionModeManual(int subId, OperatorInfo operator, - boolean persistSelection) { + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public boolean setNetworkSelectionModeManual(String operatorNumeric, boolean persistSelection) { try { ITelephony telephony = getITelephony(); - if (telephony != null) - return telephony.setNetworkSelectionModeManual(subId, operator, persistSelection); + if (telephony != null) { + return telephony.setNetworkSelectionModeManual( + getSubId(), operatorNumeric, persistSelection); + } } catch (RemoteException ex) { Rlog.e(TAG, "setNetworkSelectionModeManual RemoteException", ex); } catch (NullPointerException ex) { @@ -4895,8 +5079,9 @@ public class TelephonyManager { public boolean setPreferredNetworkType(int subId, int networkType) { try { ITelephony telephony = getITelephony(); - if (telephony != null) + if (telephony != null) { return telephony.setPreferredNetworkType(subId, networkType); + } } catch (RemoteException ex) { Rlog.e(TAG, "setPreferredNetworkType RemoteException", ex); } catch (NullPointerException ex) { @@ -5692,39 +5877,38 @@ public class TelephonyManager { * @param enable Whether to enable mobile data. * * @see #hasCarrierPrivileges + * @deprecated use {@link #setUserMobileDataEnabled(boolean)} instead. */ + @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(boolean enable) { - setDataEnabled(getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enable); + setUserMobileDataEnabled(enable); } - /** @hide */ + /** + * @hide + * @deprecated use {@link #setUserMobileDataEnabled(boolean)} instead. + */ @SystemApi + @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int subId, boolean enable) { - try { - Log.d(TAG, "setDataEnabled: enabled=" + enable); - ITelephony telephony = getITelephony(); - if (telephony != null) - telephony.setDataEnabled(subId, enable); - } catch (RemoteException e) { - Log.e(TAG, "Error calling ITelephony#setDataEnabled", e); - } + setUserMobileDataEnabled(subId, enable); } - /** - * @deprecated use {@link #isDataEnabled()} instead. + * @deprecated use {@link #isUserMobileDataEnabled()} instead. * @hide */ @SystemApi @Deprecated public boolean getDataEnabled() { - return isDataEnabled(); + return isUserMobileDataEnabled(); } /** - * Returns whether mobile data is enabled or not. + * Returns whether mobile data is enabled or not per user setting. There are other factors + * that could disable mobile data, but they are not considered here. * * If this object has been created with {@link #createForSubscriptionId}, applies to the given * subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} @@ -5741,28 +5925,21 @@ public class TelephonyManager { * @return true if mobile data is enabled. * * @see #hasCarrierPrivileges + * @deprecated use {@link #isUserMobileDataEnabled()} instead. */ - @SuppressWarnings("deprecation") + @Deprecated public boolean isDataEnabled() { - return getDataEnabled(getSubId(SubscriptionManager.getDefaultDataSubscriptionId())); + return isUserMobileDataEnabled(); } /** - * @deprecated use {@link #isDataEnabled(int)} instead. + * @deprecated use {@link #isUserMobileDataEnabled()} instead. * @hide */ + @Deprecated @SystemApi public boolean getDataEnabled(int subId) { - boolean retVal = false; - try { - ITelephony telephony = getITelephony(); - if (telephony != null) - retVal = telephony.getDataEnabled(subId); - } catch (RemoteException e) { - Log.e(TAG, "Error calling ITelephony#getDataEnabled", e); - } catch (NullPointerException e) { - } - return retVal; + return isUserMobileDataEnabled(subId); } /** @hide */ @@ -6509,6 +6686,9 @@ public class TelephonyManager { * @param uri The URI for the ringtone to play when receiving a voicemail from a specific * PhoneAccount. * @see #hasCarrierPrivileges + * + * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS} + * instead. */ public void setVoicemailRingtoneUri(PhoneAccountHandle phoneAccountHandle, Uri uri) { try { @@ -6551,6 +6731,9 @@ public class TelephonyManager { * @param enabled Whether to enable or disable vibration for voicemail notifications from a * specific PhoneAccount. * @see #hasCarrierPrivileges + * + * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS} + * instead. */ public void setVoicemailVibrationEnabled(PhoneAccountHandle phoneAccountHandle, boolean enabled) { @@ -6565,6 +6748,55 @@ public class TelephonyManager { } } + /** + * Returns carrier id of the current subscription. + *

    To recognize a carrier (including MVNO) as a first class identity, assign each carrier + * with a canonical integer a.k.a carrier id. + * + * @return Carrier id of the current subscription. Return {@link #UNKNOWN_CARRIER_ID} if the + * subscription is unavailable or the carrier cannot be identified. + * @throws IllegalStateException if telephony service is unavailable. + */ + public int getSubscriptionCarrierId() { + try { + ITelephony service = getITelephony(); + return service.getSubscriptionCarrierId(getSubId()); + } catch (RemoteException ex) { + // This could happen if binder process crashes. + ex.rethrowAsRuntimeException(); + } catch (NullPointerException ex) { + // This could happen before phone restarts due to crashing. + throw new IllegalStateException("Telephony service unavailable"); + } + return UNKNOWN_CARRIER_ID; + } + + /** + * Returns carrier name of the current subscription. + *

    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. + *

    The returned carrier name is unlocalized. + * + * @return Carrier name of the current subscription. Return {@code null} if the subscription is + * unavailable or the carrier cannot be identified. + * @throws IllegalStateException if telephony service is unavailable. + */ + public String getSubscriptionCarrierName() { + try { + ITelephony service = getITelephony(); + return service.getSubscriptionCarrierName(getSubId()); + } catch (RemoteException ex) { + // This could happen if binder process crashes. + ex.rethrowAsRuntimeException(); + } catch (NullPointerException ex) { + // This could happen before phone restarts due to crashing. + throw new IllegalStateException("Telephony service unavailable"); + } + return null; + } + /** * Return the application ID for the app type like {@link APPTYPE_CSIM}. * @@ -6641,6 +6873,7 @@ public class TelephonyManager { * @return PRLVersion or null if error. * @hide */ + @SystemApi public String getCdmaPrlVersion() { return getCdmaPrlVersion(getSubId()); } @@ -6866,6 +7099,8 @@ public class TelephonyManager { * @return true if phone is in emergency callback mode * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean getEmergencyCallbackMode() { return getEmergencyCallbackMode(getSubId()); } @@ -6908,4 +7143,101 @@ public class TelephonyManager { } return null; } + + /** + * Turns mobile data on or off. + * If the {@link TelephonyManager} object has been created with + * {@link #createForSubscriptionId}, this API applies to the given subId. + * Otherwise, it applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} + * + *

    Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the + * calling app has carrier privileges. + * + * @param enable Whether to enable mobile data. + * + * @see #hasCarrierPrivileges + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setUserMobileDataEnabled(boolean enable) { + setUserMobileDataEnabled( + getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enable); + } + + /** + * Returns whether mobile data is enabled or not per user setting. There are other factors + * that could disable mobile data, but they are not considered here. + * + * If this object has been created with {@link #createForSubscriptionId}, applies to the given + * subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} + * + *

    Requires one of the following permissions: + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE ACCESS_NETWORK_STATE}, + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}, or that the + * calling app has carrier privileges. + * + *

    Note that this does not take into account any data restrictions that may be present on the + * calling app. Such restrictions may be inspected with + * {@link ConnectivityManager#getRestrictBackgroundStatus}. + * + * @return true if mobile data is enabled. + * + * @see #hasCarrierPrivileges + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.ACCESS_NETWORK_STATE, + android.Manifest.permission.MODIFY_PHONE_STATE + }) + public boolean isUserMobileDataEnabled() { + return isUserMobileDataEnabled( + getSubId(SubscriptionManager.getDefaultDataSubscriptionId())); + } + + /** + * @hide + * Unlike isUserMobileDataEnabled, this API also evaluates carrierDataEnabled, + * policyDataEnabled etc to give a final decision. + */ + public boolean isMobileDataEnabled() { + boolean retVal = false; + try { + int subId = getSubId(SubscriptionManager.getDefaultDataSubscriptionId()); + ITelephony telephony = getITelephony(); + if (telephony != null) + retVal = telephony.isDataEnabled(subId); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelephony#isDataEnabled", e); + } catch (NullPointerException e) { + } + return retVal; + } + + /** + * Utility class of {@link #isUserMobileDataEnabled()}; + */ + private boolean isUserMobileDataEnabled(int subId) { + boolean retVal = false; + try { + ITelephony telephony = getITelephony(); + if (telephony != null) + retVal = telephony.isUserDataEnabled(subId); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelephony#isUserDataEnabled", e); + } catch (NullPointerException e) { + } + return retVal; + } + + /** Utility method of {@link #setUserMobileDataEnabled(boolean)} */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + private void setUserMobileDataEnabled(int subId, boolean enable) { + try { + Log.d(TAG, "setUserMobileDataEnabled: enabled=" + enable); + ITelephony telephony = getITelephony(); + if (telephony != null) + telephony.setUserDataEnabled(subId, enable); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelephony#setUserDataEnabled", e); + } + } } diff --git a/android/telephony/TelephonyScanManager.java b/android/telephony/TelephonyScanManager.java index 7bcdcdcc..c182e349 100644 --- a/android/telephony/TelephonyScanManager.java +++ b/android/telephony/TelephonyScanManager.java @@ -38,7 +38,6 @@ import com.android.internal.telephony.ITelephony; /** * Manages the radio access network scan requests and callbacks. - * @hide */ public final class TelephonyScanManager { @@ -55,7 +54,8 @@ public final class TelephonyScanManager { public static final int CALLBACK_SCAN_COMPLETE = 3; /** - * The caller of {@link #requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} should + * The caller of + * {@link TelephonyManager#requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} should * implement and provide this callback so that the scan results or errors can be returned. */ public static abstract class NetworkScanCallback { @@ -75,8 +75,10 @@ public final class TelephonyScanManager { * * This callback will be called whenever there is any error about the scan, and the scan * will be terminated. onComplete() will NOT be called. + * + * @param error Error code when the scan is failed, as defined in {@link NetworkScan}. */ - public void onError(int error) {} + public void onError(@NetworkScan.ScanErrorCode int error) {} } private static class NetworkScanInfo { diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java new file mode 100644 index 00000000..da51c861 --- /dev/null +++ b/android/telephony/data/DataCallResponse.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2009 Qualcomm Innovation Center, Inc. All Rights Reserved. + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.data; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +/** + * Description of the response of a setup data call connection request. + * + * @hide + */ +@SystemApi +public final class DataCallResponse implements Parcelable { + private final int mStatus; + private final int mSuggestedRetryTime; + private final int mCid; + private final int mActive; + private final String mType; + private final String mIfname; + private final List mAddresses; + private final List mDnses; + private final List mGateways; + private final List mPcscfs; + private final int mMtu; + + /** + * @param status Data call fail cause. 0 indicates no error. + * @param suggestedRetryTime The suggested data retry time in milliseconds. + * @param cid The unique id of the data connection. + * @param active Data connection active status. 0 = inactive, 1 = active/physical link down, + * 2 = active/physical link up. + * @param type The connection protocol, should be one of the PDP_type values in TS 27.007 + * section 10.1.1. For example, "IP", "IPV6", "IPV4V6", or "PPP". + * @param ifname The network interface name. + * @param addresses A list of addresses with optional "/" prefix length, e.g., + * "192.0.1.3" or "192.0.1.11/16 2001:db8::1/64". Typically 1 IPv4 or 1 IPv6 or + * one of each. If the prefix length is absent the addresses are assumed to be + * point to point with IPv4 having a prefix length of 32 and IPv6 128. + * @param dnses A list of DNS server addresses, e.g., "192.0.1.3" or + * "192.0.1.11 2001:db8::1". Null if no dns server addresses returned. + * @param gateways A list of default gateway addresses, e.g., "192.0.1.3" or + * "192.0.1.11 2001:db8::1". When null, the addresses represent point to point + * connections. + * @param pcscfs A list of Proxy Call State Control Function address via PCO(Protocol + * Configuration Option) for IMS client. + * @param mtu MTU (Maximum transmission unit) received from network Value <= 0 means network has + * either not sent a value or sent an invalid value. + */ + public DataCallResponse(int status, int suggestedRetryTime, int cid, int active, + @Nullable String type, @Nullable String ifname, + @Nullable List addresses, + @Nullable List dnses, + @Nullable List gateways, + @Nullable List pcscfs, int mtu) { + mStatus = status; + mSuggestedRetryTime = suggestedRetryTime; + mCid = cid; + mActive = active; + mType = (type == null) ? "" : type; + mIfname = (ifname == null) ? "" : ifname; + mAddresses = (addresses == null) ? new ArrayList<>() : addresses; + mDnses = (dnses == null) ? new ArrayList<>() : dnses; + mGateways = (gateways == null) ? new ArrayList<>() : gateways; + mPcscfs = (pcscfs == null) ? new ArrayList<>() : pcscfs; + mMtu = mtu; + } + + public DataCallResponse(Parcel source) { + mStatus = source.readInt(); + mSuggestedRetryTime = source.readInt(); + mCid = source.readInt(); + mActive = source.readInt(); + mType = source.readString(); + mIfname = source.readString(); + mAddresses = new ArrayList<>(); + source.readList(mAddresses, InterfaceAddress.class.getClassLoader()); + mDnses = new ArrayList<>(); + source.readList(mDnses, InetAddress.class.getClassLoader()); + mGateways = new ArrayList<>(); + source.readList(mGateways, InetAddress.class.getClassLoader()); + mPcscfs = new ArrayList<>(); + source.readList(mPcscfs, InetAddress.class.getClassLoader()); + mMtu = source.readInt(); + } + + /** + * @return Data call fail cause. 0 indicates no error. + */ + public int getStatus() { return mStatus; } + + /** + * @return The suggested data retry time in milliseconds. + */ + public int getSuggestedRetryTime() { return mSuggestedRetryTime; } + + /** + * @return The unique id of the data connection. + */ + public int getCallId() { return mCid; } + + /** + * @return 0 = inactive, 1 = active/physical link down, 2 = active/physical link up. + */ + public int getActive() { return mActive; } + + /** + * @return The connection protocol, should be one of the PDP_type values in TS 27.007 section + * 10.1.1. For example, "IP", "IPV6", "IPV4V6", or "PPP". + */ + @NonNull + public String getType() { return mType; } + + /** + * @return The network interface name. + */ + @NonNull + public String getIfname() { return mIfname; } + + /** + * @return A list of {@link InterfaceAddress} + */ + @NonNull + public List getAddresses() { return mAddresses; } + + /** + * @return A list of DNS server addresses, e.g., "192.0.1.3" or + * "192.0.1.11 2001:db8::1". Empty list if no dns server addresses returned. + */ + @NonNull + public List getDnses() { return mDnses; } + + /** + * @return A list of default gateway addresses, e.g., "192.0.1.3" or + * "192.0.1.11 2001:db8::1". Empty list if the addresses represent point to point connections. + */ + @NonNull + public List getGateways() { return mGateways; } + + /** + * @return A list of Proxy Call State Control Function address via PCO(Protocol Configuration + * Option) for IMS client. + */ + @NonNull + public List getPcscfs() { return mPcscfs; } + + /** + * @return MTU received from network Value <= 0 means network has either not sent a value or + * sent an invalid value + */ + public int getMtu() { return mMtu; } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("DataCallResponse: {") + .append(" status=").append(mStatus) + .append(" retry=").append(mSuggestedRetryTime) + .append(" cid=").append(mCid) + .append(" active=").append(mActive) + .append(" type=").append(mType) + .append(" ifname=").append(mIfname) + .append(" addresses=").append(mAddresses) + .append(" dnses=").append(mDnses) + .append(" gateways=").append(mGateways) + .append(" pcscf=").append(mPcscfs) + .append(" mtu=").append(mMtu) + .append("}"); + return sb.toString(); + } + + @Override + public boolean equals (Object o) { + if (this == o) return true; + + if (o == null || !(o instanceof DataCallResponse)) { + return false; + } + + DataCallResponse other = (DataCallResponse) o; + return this.mStatus == other.mStatus + && this.mSuggestedRetryTime == other.mSuggestedRetryTime + && this.mCid == other.mCid + && this.mActive == other.mActive + && this.mType.equals(other.mType) + && this.mIfname.equals(other.mIfname) + && mAddresses.size() == other.mAddresses.size() + && mAddresses.containsAll(other.mAddresses) + && mDnses.size() == other.mDnses.size() + && mDnses.containsAll(other.mDnses) + && mGateways.size() == other.mGateways.size() + && mGateways.containsAll(other.mGateways) + && mPcscfs.size() == other.mPcscfs.size() + && mPcscfs.containsAll(other.mPcscfs) + && mMtu == other.mMtu; + } + + @Override + public int hashCode() { + return mStatus * 31 + + mSuggestedRetryTime * 37 + + mCid * 41 + + mActive * 43 + + mType.hashCode() * 47 + + mIfname.hashCode() * 53 + + mAddresses.hashCode() * 59 + + mDnses.hashCode() * 61 + + mGateways.hashCode() * 67 + + mPcscfs.hashCode() * 71 + + mMtu * 73; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mStatus); + dest.writeInt(mSuggestedRetryTime); + dest.writeInt(mCid); + dest.writeInt(mActive); + dest.writeString(mType); + dest.writeString(mIfname); + dest.writeList(mAddresses); + dest.writeList(mDnses); + dest.writeList(mGateways); + dest.writeList(mPcscfs); + dest.writeInt(mMtu); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public DataCallResponse createFromParcel(Parcel source) { + return new DataCallResponse(source); + } + + @Override + public DataCallResponse[] newArray(int size) { + return new DataCallResponse[size]; + } + }; +} \ No newline at end of file diff --git a/android/telephony/data/InterfaceAddress.java b/android/telephony/data/InterfaceAddress.java new file mode 100644 index 00000000..00d212a5 --- /dev/null +++ b/android/telephony/data/InterfaceAddress.java @@ -0,0 +1,127 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.data; + +import android.annotation.SystemApi; +import android.net.NetworkUtils; +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * This class represents a Network Interface address. In short it's an IP address, a subnet mask + * when the address is an IPv4 one. An IP address and a network prefix length in the case of IPv6 + * address. + * + * @hide + */ +@SystemApi +public final class InterfaceAddress implements Parcelable { + + private final InetAddress mInetAddress; + + private final int mPrefixLength; + + /** + * @param inetAddress A {@link InetAddress} of the address + * @param prefixLength The network prefix length for this address. + */ + public InterfaceAddress(InetAddress inetAddress, int prefixLength) { + mInetAddress = inetAddress; + mPrefixLength = prefixLength; + } + + /** + * @param address The address in string format + * @param prefixLength The network prefix length for this address. + * @throws UnknownHostException + */ + public InterfaceAddress(String address, int prefixLength) throws UnknownHostException { + InetAddress ia; + try { + ia = NetworkUtils.numericToInetAddress(address); + } catch (IllegalArgumentException e) { + throw new UnknownHostException("Non-numeric ip addr=" + address); + } + mInetAddress = ia; + mPrefixLength = prefixLength; + } + + public InterfaceAddress(Parcel source) { + mInetAddress = (InetAddress) source.readSerializable(); + mPrefixLength = source.readInt(); + } + + /** + * @return an InetAddress for this address. + */ + public InetAddress getAddress() { return mInetAddress; } + + /** + * @return The network prefix length for this address. + */ + public int getNetworkPrefixLength() { return mPrefixLength; } + + @Override + public boolean equals (Object o) { + if (this == o) return true; + + if (o == null || !(o instanceof InterfaceAddress)) { + return false; + } + + InterfaceAddress other = (InterfaceAddress) o; + return this.mInetAddress.equals(other.mInetAddress) + && this.mPrefixLength == other.mPrefixLength; + } + + @Override + public int hashCode() { + return mInetAddress.hashCode() * 31 + mPrefixLength * 37; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return mInetAddress + "/" + mPrefixLength; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeSerializable(mInetAddress); + dest.writeInt(mPrefixLength); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public InterfaceAddress createFromParcel(Parcel source) { + return new InterfaceAddress(source); + } + + @Override + public InterfaceAddress[] newArray(int size) { + return new InterfaceAddress[size]; + } + }; +} diff --git a/android/telephony/euicc/EuiccManager.java b/android/telephony/euicc/EuiccManager.java index a13af5f4..176057dd 100644 --- a/android/telephony/euicc/EuiccManager.java +++ b/android/telephony/euicc/EuiccManager.java @@ -15,8 +15,10 @@ */ package android.telephony.euicc; +import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SdkConstant; +import android.annotation.SystemApi; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; @@ -29,6 +31,9 @@ import android.os.ServiceManager; import com.android.internal.telephony.euicc.IEuiccController; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * EuiccManager is the application interface to eUICCs, or eSIMs/embedded SIMs. * @@ -63,7 +68,7 @@ public class EuiccManager { * embedded SIM. * *

    The activity will immediately finish with {@link android.app.Activity#RESULT_CANCELED} if - * {@link #isEnabled} is false or if the device is already provisioned. + * {@link #isEnabled} is false. * * TODO(b/35851809): Make this a SystemApi. */ @@ -167,13 +172,40 @@ public class EuiccManager { */ public static final String META_DATA_CARRIER_ICON = "android.telephony.euicc.carriericon"; + /** + * Euicc OTA update status which can be got by {@link #getOtaStatus} + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"EUICC_OTA_"}, value = { + EUICC_OTA_IN_PROGRESS, + EUICC_OTA_FAILED, + EUICC_OTA_SUCCEEDED, + EUICC_OTA_NOT_NEEDED, + EUICC_OTA_STATUS_UNAVAILABLE + + }) + public @interface OtaStatus{} + + /** + * An OTA is in progress. During this time, the eUICC is not available and the user may lose + * network access. + */ + public static final int EUICC_OTA_IN_PROGRESS = 1; + /** The OTA update failed. */ + public static final int EUICC_OTA_FAILED = 2; + /** The OTA update finished successfully. */ + public static final int EUICC_OTA_SUCCEEDED = 3; + /** The OTA update not needed since current eUICC OS is latest. */ + public static final int EUICC_OTA_NOT_NEEDED = 4; + /** The OTA status is unavailable since eUICC service is unavailable. */ + public static final int EUICC_OTA_STATUS_UNAVAILABLE = 5; + private final Context mContext; - private final IEuiccController mController; /** @hide */ public EuiccManager(Context context) { mContext = context; - mController = IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller")); } /** @@ -189,7 +221,7 @@ public class EuiccManager { public boolean isEnabled() { // In the future, this may reach out to IEuiccController (if non-null) to check any dynamic // restrictions. - return mController != null; + return getIEuiccController() != null; } /** @@ -206,7 +238,27 @@ public class EuiccManager { return null; } try { - return mController.getEid(); + return getIEuiccController().getEid(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the current status of eUICC OTA. + * + *

    Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. + * + * @return the status of eUICC OTA. If {@link #isEnabled()} is false or the eUICC is not ready, + * {@link OtaStatus#EUICC_OTA_STATUS_UNAVAILABLE} will be returned. + */ + @SystemApi + public int getOtaStatus() { + if (!isEnabled()) { + return EUICC_OTA_STATUS_UNAVAILABLE; + } + try { + return getIEuiccController().getOtaStatus(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -232,7 +284,7 @@ public class EuiccManager { return; } try { - mController.downloadSubscription(subscription, switchAfterDownload, + getIEuiccController().downloadSubscription(subscription, switchAfterDownload, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -296,7 +348,7 @@ public class EuiccManager { return; } try { - mController.continueOperation(resolutionIntent, resolutionExtras); + getIEuiccController().continueOperation(resolutionIntent, resolutionExtras); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -328,7 +380,7 @@ public class EuiccManager { return; } try { - mController.getDownloadableSubscriptionMetadata( + getIEuiccController().getDownloadableSubscriptionMetadata( subscription, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -358,7 +410,7 @@ public class EuiccManager { return; } try { - mController.getDefaultDownloadableSubscriptionList( + getIEuiccController().getDefaultDownloadableSubscriptionList( mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -377,7 +429,7 @@ public class EuiccManager { return null; } try { - return mController.getEuiccInfo(); + return getIEuiccController().getEuiccInfo(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -402,7 +454,7 @@ public class EuiccManager { return; } try { - mController.deleteSubscription( + getIEuiccController().deleteSubscription( subscriptionId, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -429,7 +481,7 @@ public class EuiccManager { return; } try { - mController.switchToSubscription( + getIEuiccController().switchToSubscription( subscriptionId, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -455,7 +507,8 @@ public class EuiccManager { return; } try { - mController.updateSubscriptionNickname(subscriptionId, nickname, callbackIntent); + getIEuiccController().updateSubscriptionNickname( + subscriptionId, nickname, callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -477,7 +530,7 @@ public class EuiccManager { return; } try { - mController.eraseSubscriptions(callbackIntent); + getIEuiccController().eraseSubscriptions(callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -507,7 +560,7 @@ public class EuiccManager { return; } try { - mController.retainSubscriptionsForFactoryReset(callbackIntent); + getIEuiccController().retainSubscriptionsForFactoryReset(callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -520,4 +573,8 @@ public class EuiccManager { // Caller canceled the callback; do nothing. } } + + private static IEuiccController getIEuiccController() { + return IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller")); + } } diff --git a/android/telephony/ims/feature/ImsFeature.java b/android/telephony/ims/feature/ImsFeature.java index 062858d4..ca4a210e 100644 --- a/android/telephony/ims/feature/ImsFeature.java +++ b/android/telephony/ims/feature/ImsFeature.java @@ -187,6 +187,11 @@ public abstract class ImsFeature { mContext.sendBroadcast(intent); } + /** + * Called when the feature is ready to use. + */ + public abstract void onFeatureReady(); + /** * Called when the feature is being removed and must be cleaned up. */ diff --git a/android/telephony/ims/feature/MMTelFeature.java b/android/telephony/ims/feature/MMTelFeature.java index e790d146..4e095e3a 100644 --- a/android/telephony/ims/feature/MMTelFeature.java +++ b/android/telephony/ims/feature/MMTelFeature.java @@ -346,6 +346,11 @@ public class MMTelFeature extends ImsFeature { return null; } + @Override + public void onFeatureReady() { + + } + /** * {@inheritDoc} */ diff --git a/android/telephony/ims/feature/RcsFeature.java b/android/telephony/ims/feature/RcsFeature.java index a82e6086..40c5181d 100644 --- a/android/telephony/ims/feature/RcsFeature.java +++ b/android/telephony/ims/feature/RcsFeature.java @@ -35,6 +35,11 @@ public class RcsFeature extends ImsFeature { super(); } + @Override + public void onFeatureReady() { + + } + @Override public void onFeatureRemoved() { diff --git a/android/telephony/ims/internal/ImsCallSessionListener.java b/android/telephony/ims/internal/ImsCallSessionListener.java new file mode 100644 index 00000000..5d16dd5b --- /dev/null +++ b/android/telephony/ims/internal/ImsCallSessionListener.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal; + +import android.os.RemoteException; +import android.telephony.ims.internal.aidl.IImsCallSessionListener; + +import com.android.ims.ImsCallProfile; +import com.android.ims.ImsConferenceState; +import com.android.ims.ImsReasonInfo; +import com.android.ims.ImsStreamMediaProfile; +import com.android.ims.ImsSuppServiceNotification; +import com.android.ims.internal.ImsCallSession; + +/** + * Proxy class for interfacing with the framework's Call session for an ongoing IMS call. + * + * DO NOT remove or change the existing APIs, only add new ones to this Base implementation or you + * will break other implementations of ImsCallSessionListener maintained by other ImsServices. + * + * @hide + */ +public class ImsCallSessionListener { + + private final IImsCallSessionListener mListener; + + public ImsCallSessionListener(IImsCallSessionListener l) { + mListener = l; + } + + /** + * Called when a request is sent out to initiate a new session + * and 1xx response is received from the network. + */ + public void callSessionProgressing(ImsStreamMediaProfile profile) + throws RemoteException { + mListener.callSessionProgressing(profile); + } + + /** + * Called when the session is initiated. + * + * @param profile the associated {@link ImsCallSession}. + */ + public void callSessionInitiated(ImsCallProfile profile) throws RemoteException { + mListener.callSessionInitiated(profile); + } + + /** + * Called when the session establishment has failed. + * + * @param reasonInfo detailed reason of the session establishment failure + */ + public void callSessionInitiatedFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionInitiatedFailed(reasonInfo); + } + + /** + * Called when the session is terminated. + * + * @param reasonInfo detailed reason of the session termination + */ + public void callSessionTerminated(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionTerminated(reasonInfo); + } + + /** + * Called when the session is on hold. + */ + public void callSessionHeld(ImsCallProfile profile) throws RemoteException { + mListener.callSessionHeld(profile); + } + + /** + * Called when the session hold has failed. + * + * @param reasonInfo detailed reason of the session hold failure + */ + public void callSessionHoldFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionHoldFailed(reasonInfo); + } + + /** + * Called when the session hold is received from the remote user. + */ + public void callSessionHoldReceived(ImsCallProfile profile) throws RemoteException { + mListener.callSessionHoldReceived(profile); + } + + /** + * Called when the session resume is done. + */ + public void callSessionResumed(ImsCallProfile profile) throws RemoteException { + mListener.callSessionResumed(profile); + } + + /** + * Called when the session resume has failed. + * + * @param reasonInfo detailed reason of the session resume failure + */ + public void callSessionResumeFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionResumeFailed(reasonInfo); + } + + /** + * Called when the session resume is received from the remote user. + */ + public void callSessionResumeReceived(ImsCallProfile profile) throws RemoteException { + mListener.callSessionResumeReceived(profile); + } + + /** + * Called when the session merge has been started. At this point, the {@code newSession} + * represents the session which has been initiated to the IMS conference server for the + * new merged conference. + * + * @param newSession the session object that is merged with an active & hold session + */ + public void callSessionMergeStarted(ImsCallSession newSession, ImsCallProfile profile) + throws RemoteException { + mListener.callSessionMergeStarted(newSession != null ? newSession.getSession() : null, + profile); + } + + /** + * Called when the session merge is successful and the merged session is active. + * + * @param newSession the new session object that is used for the conference + */ + public void callSessionMergeComplete(ImsCallSession newSession) throws RemoteException { + mListener.callSessionMergeComplete(newSession != null ? newSession.getSession() : null); + } + + /** + * Called when the session merge has failed. + * + * @param reasonInfo detailed reason of the call merge failure + */ + public void callSessionMergeFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionMergeFailed(reasonInfo); + } + + /** + * Called when the session is updated (except for hold/unhold). + */ + public void callSessionUpdated(ImsCallProfile profile) throws RemoteException { + mListener.callSessionUpdated(profile); + } + + /** + * Called when the session update has failed. + * + * @param reasonInfo detailed reason of the session update failure + */ + public void callSessionUpdateFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionUpdateFailed(reasonInfo); + } + + /** + * Called when the session update is received from the remote user. + */ + public void callSessionUpdateReceived(ImsCallProfile profile) throws RemoteException { + mListener.callSessionUpdateReceived(profile); + } + + /** + * Called when the session has been extended to a conference session. + * + * @param newSession the session object that is extended to the conference + * from the active session + */ + public void callSessionConferenceExtended(ImsCallSession newSession, ImsCallProfile profile) + throws RemoteException { + mListener.callSessionConferenceExtended(newSession != null ? newSession.getSession() : null, + profile); + } + + /** + * Called when the conference extension has failed. + * + * @param reasonInfo detailed reason of the conference extension failure + */ + public void callSessionConferenceExtendFailed(ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionConferenceExtendFailed(reasonInfo); + } + + /** + * Called when the conference extension is received from the remote user. + */ + public void callSessionConferenceExtendReceived(ImsCallSession newSession, + ImsCallProfile profile) throws RemoteException { + mListener.callSessionConferenceExtendReceived(newSession != null + ? newSession.getSession() : null, profile); + } + + /** + * Called when the invitation request of the participants is delivered to the conference + * server. + */ + public void callSessionInviteParticipantsRequestDelivered() throws RemoteException { + mListener.callSessionInviteParticipantsRequestDelivered(); + } + + /** + * Called when the invitation request of the participants has failed. + * + * @param reasonInfo detailed reason of the conference invitation failure + */ + public void callSessionInviteParticipantsRequestFailed(ImsReasonInfo reasonInfo) + throws RemoteException { + mListener.callSessionInviteParticipantsRequestFailed(reasonInfo); + } + + /** + * Called when the removal request of the participants is delivered to the conference + * server. + */ + public void callSessionRemoveParticipantsRequestDelivered() throws RemoteException { + mListener.callSessionRemoveParticipantsRequestDelivered(); + } + + /** + * Called when the removal request of the participants has failed. + * + * @param reasonInfo detailed reason of the conference removal failure + */ + public void callSessionRemoveParticipantsRequestFailed(ImsReasonInfo reasonInfo) + throws RemoteException { + mListener.callSessionInviteParticipantsRequestFailed(reasonInfo); + } + + /** + * Notifies the framework of the updated Call session conference state. + * + * @param state the new {@link ImsConferenceState} associated with the conference. + */ + public void callSessionConferenceStateUpdated(ImsConferenceState state) throws RemoteException { + mListener.callSessionConferenceStateUpdated(state); + } + + /** + * Notifies the incoming USSD message. + */ + public void callSessionUssdMessageReceived(int mode, String ussdMessage) + throws RemoteException { + mListener.callSessionUssdMessageReceived(mode, ussdMessage); + } + + /** + * Notifies of a case where a {@link com.android.ims.internal.ImsCallSession} may potentially + * handover from one radio technology to another. + * + * @param srcAccessTech The source radio access technology; one of the access technology + * constants defined in {@link android.telephony.ServiceState}. For + * example + * {@link android.telephony.ServiceState#RIL_RADIO_TECHNOLOGY_LTE}. + * @param targetAccessTech The target radio access technology; one of the access technology + * constants defined in {@link android.telephony.ServiceState}. For + * example + * {@link android.telephony.ServiceState#RIL_RADIO_TECHNOLOGY_LTE}. + */ + public void callSessionMayHandover(int srcAccessTech, int targetAccessTech) + throws RemoteException { + mListener.callSessionMayHandover(srcAccessTech, targetAccessTech); + } + + /** + * Called when session access technology changes. + * + * @param srcAccessTech original access technology + * @param targetAccessTech new access technology + * @param reasonInfo + */ + public void callSessionHandover(int srcAccessTech, int targetAccessTech, + ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionHandover(srcAccessTech, targetAccessTech, reasonInfo); + } + + /** + * Called when session access technology change fails. + * + * @param srcAccessTech original access technology + * @param targetAccessTech new access technology + * @param reasonInfo handover failure reason + */ + public void callSessionHandoverFailed(int srcAccessTech, int targetAccessTech, + ImsReasonInfo reasonInfo) throws RemoteException { + mListener.callSessionHandoverFailed(srcAccessTech, targetAccessTech, reasonInfo); + } + + /** + * Called when the TTY mode is changed by the remote party. + * + * @param mode one of the following: - + * {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} - + * {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} - + * {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} - + * {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + */ + public void callSessionTtyModeReceived(int mode) throws RemoteException { + mListener.callSessionTtyModeReceived(mode); + } + + /** + * Called when the multiparty state is changed for this {@code ImsCallSession}. + * + * @param isMultiParty {@code true} if the session became multiparty, + * {@code false} otherwise. + */ + + public void callSessionMultipartyStateChanged(boolean isMultiParty) throws RemoteException { + mListener.callSessionMultipartyStateChanged(isMultiParty); + } + + /** + * Called when the supplementary service information is received for the current session. + */ + public void callSessionSuppServiceReceived(ImsSuppServiceNotification suppSrvNotification) + throws RemoteException { + mListener.callSessionSuppServiceReceived(suppSrvNotification); + } + + /** + * Received RTT modify request from the remote party. + * + * @param callProfile ImsCallProfile with updated attributes + */ + public void callSessionRttModifyRequestReceived(ImsCallProfile callProfile) + throws RemoteException { + mListener.callSessionRttModifyRequestReceived(callProfile); + } + + /** + * @param status the received response for RTT modify request. + */ + public void callSessionRttModifyResponseReceived(int status) throws RemoteException { + mListener.callSessionRttModifyResponseReceived(status); + } + + /** + * Device received RTT message from Remote UE. + * + * @param rttMessage RTT message received + */ + public void callSessionRttMessageReceived(String rttMessage) throws RemoteException { + mListener.callSessionRttMessageReceived(rttMessage); + } +} + diff --git a/android/telephony/ims/internal/ImsService.java b/android/telephony/ims/internal/ImsService.java new file mode 100644 index 00000000..b7c8ca0f --- /dev/null +++ b/android/telephony/ims/internal/ImsService.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.telephony.CarrierConfigManager; +import android.telephony.ims.internal.aidl.IImsConfig; +import android.telephony.ims.internal.aidl.IImsMmTelFeature; +import android.telephony.ims.internal.aidl.IImsRcsFeature; +import android.telephony.ims.internal.aidl.IImsRegistration; +import android.telephony.ims.internal.aidl.IImsServiceController; +import android.telephony.ims.internal.aidl.IImsServiceControllerListener; +import android.telephony.ims.internal.feature.ImsFeature; +import android.telephony.ims.internal.feature.MmTelFeature; +import android.telephony.ims.internal.feature.RcsFeature; +import android.telephony.ims.internal.stub.ImsConfigImplBase; +import android.telephony.ims.internal.stub.ImsFeatureConfiguration; +import android.telephony.ims.internal.stub.ImsRegistrationImplBase; +import android.util.Log; +import android.util.SparseArray; + +import com.android.ims.internal.IImsFeatureStatusCallback; +import com.android.internal.annotations.VisibleForTesting; + +/** + * Main ImsService implementation, which binds via the Telephony ImsResolver. Services that extend + * ImsService must register the service in their AndroidManifest to be detected by the framework. + * First, the application must declare that they use the "android.permission.BIND_IMS_SERVICE" + * permission. Then, the ImsService definition in the manifest must follow the following format: + * + * ... + * + * + * + * + * + * + * + * ... + * + * The telephony framework will then bind to the ImsService you have defined in your manifest + * if you are either: + * 1) Defined as the default ImsService for the device in the device overlay using + * "config_ims_package". + * 2) Defined as a Carrier Provided ImsService in the Carrier Configuration using + * {@link CarrierConfigManager#KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING}. + * + * The features that are currently supported in an ImsService are: + * - RCS_FEATURE: This ImsService implements the RcsFeature class. + * - MMTEL_FEATURE: This ImsService implements the MmTelFeature class. + * @hide + */ +public class ImsService extends Service { + + private static final String LOG_TAG = "ImsService"; + + /** + * The intent that must be defined as an intent-filter in the AndroidManifest of the ImsService. + * @hide + */ + public static final String SERVICE_INTERFACE = "android.telephony.ims.ImsService"; + + // A map of slot Id -> map of features (indexed by ImsFeature feature id) corresponding to that + // slot. + // We keep track of this to facilitate cleanup of the IImsFeatureStatusCallback and + // call ImsFeature#onFeatureRemoved. + private final SparseArray> mFeaturesBySlot = new SparseArray<>(); + + private IImsServiceControllerListener mListener; + + + /** + * Listener that notifies the framework of ImsService changes. + */ + public static class Listener extends IImsServiceControllerListener.Stub { + /** + * The IMS features that this ImsService supports has changed. + * @param c a new {@link ImsFeatureConfiguration} containing {@link ImsFeature.FeatureType}s + * that this ImsService supports. This may trigger the addition/removal of feature + * in this service. + */ + public void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c) { + } + } + + /** + * @hide + */ + protected final IBinder mImsServiceController = new IImsServiceController.Stub() { + @Override + public void setListener(IImsServiceControllerListener l) { + mListener = l; + } + + @Override + public IImsMmTelFeature createMmTelFeature(int slotId, IImsFeatureStatusCallback c) { + return createMmTelFeatureInternal(slotId, c); + } + + @Override + public IImsRcsFeature createRcsFeature(int slotId, IImsFeatureStatusCallback c) { + return createRcsFeatureInternal(slotId, c); + } + + @Override + public void removeImsFeature(int slotId, int featureType, IImsFeatureStatusCallback c) + throws RemoteException { + ImsService.this.removeImsFeature(slotId, featureType, c); + } + + @Override + public ImsFeatureConfiguration querySupportedImsFeatures() { + return ImsService.this.querySupportedImsFeatures(); + } + + @Override + public void notifyImsServiceReadyForFeatureCreation() { + ImsService.this.readyForFeatureCreation(); + } + + @Override + public void notifyImsFeatureReady(int slotId, int featureType) + throws RemoteException { + ImsService.this.notifyImsFeatureReady(slotId, featureType); + } + + @Override + public IImsConfig getConfig(int slotId) throws RemoteException { + ImsConfigImplBase c = ImsService.this.getConfig(slotId); + return c != null ? c.getBinder() : null; + } + + @Override + public IImsRegistration getRegistration(int slotId) throws RemoteException { + ImsRegistrationImplBase r = ImsService.this.getRegistration(slotId); + return r != null ? r.getBinder() : null; + } + }; + + /** + * @hide + */ + @Override + public IBinder onBind(Intent intent) { + if(SERVICE_INTERFACE.equals(intent.getAction())) { + Log.i(LOG_TAG, "ImsService Bound."); + return mImsServiceController; + } + return null; + } + + /** + * @hide + */ + @VisibleForTesting + public SparseArray getFeatures(int slotId) { + return mFeaturesBySlot.get(slotId); + } + + private IImsMmTelFeature createMmTelFeatureInternal(int slotId, + IImsFeatureStatusCallback c) { + MmTelFeature f = createMmTelFeature(slotId); + if (f != null) { + setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL, c); + return f.getBinder(); + } else { + Log.e(LOG_TAG, "createMmTelFeatureInternal: null feature returned."); + return null; + } + } + + private IImsRcsFeature createRcsFeatureInternal(int slotId, + IImsFeatureStatusCallback c) { + RcsFeature f = createRcsFeature(slotId); + if (f != null) { + setupFeature(f, slotId, ImsFeature.FEATURE_RCS, c); + return f.getBinder(); + } else { + Log.e(LOG_TAG, "createRcsFeatureInternal: null feature returned."); + return null; + } + } + + private void setupFeature(ImsFeature f, int slotId, int featureType, + IImsFeatureStatusCallback c) { + f.addImsFeatureStatusCallback(c); + f.initialize(this, slotId); + addImsFeature(slotId, featureType, f); + } + + private void addImsFeature(int slotId, int featureType, ImsFeature f) { + synchronized (mFeaturesBySlot) { + // Get SparseArray for Features, by querying slot Id + SparseArray features = mFeaturesBySlot.get(slotId); + if (features == null) { + // Populate new SparseArray of features if it doesn't exist for this slot yet. + features = new SparseArray<>(); + mFeaturesBySlot.put(slotId, features); + } + features.put(featureType, f); + } + } + + private void removeImsFeature(int slotId, int featureType, + IImsFeatureStatusCallback c) { + synchronized (mFeaturesBySlot) { + // get ImsFeature associated with the slot/feature + SparseArray features = mFeaturesBySlot.get(slotId); + if (features == null) { + Log.w(LOG_TAG, "Can not remove ImsFeature. No ImsFeatures exist on slot " + + slotId); + return; + } + ImsFeature f = features.get(featureType); + if (f == null) { + Log.w(LOG_TAG, "Can not remove ImsFeature. No feature with type " + + featureType + " exists on slot " + slotId); + return; + } + f.removeImsFeatureStatusCallback(c); + f.onFeatureRemoved(); + features.remove(featureType); + } + } + + private void notifyImsFeatureReady(int slotId, int featureType) { + synchronized (mFeaturesBySlot) { + // get ImsFeature associated with the slot/feature + SparseArray features = mFeaturesBySlot.get(slotId); + if (features == null) { + Log.w(LOG_TAG, "Can not notify ImsFeature ready. No ImsFeatures exist on " + + "slot " + slotId); + return; + } + ImsFeature f = features.get(featureType); + if (f == null) { + Log.w(LOG_TAG, "Can not notify ImsFeature ready. No feature with type " + + featureType + " exists on slot " + slotId); + return; + } + f.onFeatureReady(); + } + } + + /** + * When called, provide the {@link ImsFeatureConfiguration} that this ImsService currently + * supports. This will trigger the framework to set up the {@link ImsFeature}s that correspond + * to the {@link ImsFeature.FeatureType}s configured here. + * @return an {@link ImsFeatureConfiguration} containing Features this ImsService supports, + * defined in {@link ImsFeature.FeatureType}. + */ + public ImsFeatureConfiguration querySupportedImsFeatures() { + // Return empty for base implementation + return new ImsFeatureConfiguration(); + } + + /** + * Updates the framework with a new {@link ImsFeatureConfiguration} containing the updated + * features, defined in {@link ImsFeature.FeatureType} that this ImsService supports. This may + * trigger the framework to add/remove new ImsFeatures, depending on the configuration. + */ + public final void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c) + throws RemoteException { + if (mListener == null) { + throw new IllegalStateException("Framework is not ready"); + } + mListener.onUpdateSupportedImsFeatures(c); + } + + /** + * The ImsService has been bound and is ready for ImsFeature creation based on the Features that + * the ImsService has registered for with the framework, either in the manifest or via + * The ImsService should use this signal instead of onCreate/onBind or similar to perform + * feature initialization because the framework may bind to this service multiple times to + * query the ImsService's {@link ImsFeatureConfiguration} via + * {@link #querySupportedImsFeatures()}before creating features. + */ + public void readyForFeatureCreation() { + } + + /** + * When called, the framework is requesting that a new MmTelFeature is created for the specified + * slot. + * + * @param slotId The slot ID that the MMTel Feature is being created for. + * @return The newly created MmTelFeature associated with the slot or null if the feature is not + * supported. + */ + public MmTelFeature createMmTelFeature(int slotId) { + return null; + } + + /** + * When called, the framework is requesting that a new RcsFeature is created for the specified + * slot + * + * @param slotId The slot ID that the RCS Feature is being created for. + * @return The newly created RcsFeature associated with the slot or null if the feature is not + * supported. + */ + public RcsFeature createRcsFeature(int slotId) { + return null; + } + + /** + * @param slotId The slot that the IMS configuration is associated with. + * @return ImsConfig implementation that is associated with the specified slot. + */ + public ImsConfigImplBase getConfig(int slotId) { + return new ImsConfigImplBase(); + } + + /** + * @param slotId The slot that is associated with the IMS Registration. + * @return the ImsRegistration implementation associated with the slot. + */ + public ImsRegistrationImplBase getRegistration(int slotId) { + return new ImsRegistrationImplBase(); + } +} diff --git a/android/telephony/ims/internal/SmsImplBase.java b/android/telephony/ims/internal/SmsImplBase.java new file mode 100644 index 00000000..47414cf7 --- /dev/null +++ b/android/telephony/ims/internal/SmsImplBase.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal; + +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.os.RemoteException; +import android.telephony.SmsManager; +import android.telephony.SmsMessage; +import android.telephony.ims.internal.aidl.IImsSmsListener; +import android.telephony.ims.internal.feature.MmTelFeature; +import android.util.Log; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Base implementation for SMS over IMS. + * + * Any service wishing to provide SMS over IMS should extend this class and implement all methods + * that the service supports. + * @hide + */ +public class SmsImplBase { + private static final String LOG_TAG = "SmsImplBase"; + + @IntDef({ + SEND_STATUS_OK, + SEND_STATUS_ERROR, + SEND_STATUS_ERROR_RETRY, + SEND_STATUS_ERROR_FALLBACK + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SendStatusResult {} + /** + * Message was sent successfully. + */ + public static final int SEND_STATUS_OK = 1; + + /** + * IMS provider failed to send the message and platform should not retry falling back to sending + * the message using the radio. + */ + public static final int SEND_STATUS_ERROR = 2; + + /** + * IMS provider failed to send the message and platform should retry again after setting TP-RD bit + * to high. + */ + public static final int SEND_STATUS_ERROR_RETRY = 3; + + /** + * IMS provider failed to send the message and platform should retry falling back to sending + * the message using the radio. + */ + public static final int SEND_STATUS_ERROR_FALLBACK = 4; + + @IntDef({ + DELIVER_STATUS_OK, + DELIVER_STATUS_ERROR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DeliverStatusResult {} + /** + * Message was delivered successfully. + */ + public static final int DELIVER_STATUS_OK = 1; + + /** + * Message was not delivered. + */ + public static final int DELIVER_STATUS_ERROR = 2; + + @IntDef({ + STATUS_REPORT_STATUS_OK, + STATUS_REPORT_STATUS_ERROR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StatusReportResult {} + + /** + * Status Report was set successfully. + */ + public static final int STATUS_REPORT_STATUS_OK = 1; + + /** + * Error while setting status report. + */ + public static final int STATUS_REPORT_STATUS_ERROR = 2; + + + // Lock for feature synchronization + private final Object mLock = new Object(); + private IImsSmsListener mListener; + + /** + * Registers a listener responsible for handling tasks like delivering messages. + * + * @param listener listener to register. + * + * @hide + */ + public final void registerSmsListener(IImsSmsListener listener) { + synchronized (mLock) { + mListener = listener; + } + } + + /** + * This method will be triggered by the platform when the user attempts to send an SMS. This + * method should be implemented by the IMS providers to provide implementation of sending an SMS + * over IMS. + * + * @param smsc the Short Message Service Center address. + * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and + * {@link SmsMessage#FORMAT_3GPP2}. + * @param messageRef the message reference. + * @param isRetry whether it is a retry of an already attempted message or not. + * @param pdu PDUs representing the contents of the message. + */ + public void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) { + // Base implementation returns error. Should be overridden. + try { + onSendSmsResult(messageRef, SEND_STATUS_ERROR, SmsManager.RESULT_ERROR_GENERIC_FAILURE); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Can not send sms: " + e.getMessage()); + } + } + + /** + * This method will be triggered by the platform after {@link #onSmsReceived(String, byte[])} has + * been called to deliver the result to the IMS provider. + * + * @param result result of delivering the message. Valid values are defined in + * {@link DeliverStatusResult} + * @param messageRef the message reference or -1 of unavailable. + */ + public void acknowledgeSms(int messageRef, @DeliverStatusResult int result) { + + } + + /** + * This method will be triggered by the platform after + * {@link #onSmsStatusReportReceived(int, int, byte[])} has been called to provide the result to + * the IMS provider. + * + * @param result result of delivering the message. Valid values are defined in + * {@link StatusReportResult} + * @param messageRef the message reference or -1 of unavailable. + */ + public void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) { + + } + + /** + * This method should be triggered by the IMS providers when there is an incoming message. The + * platform will deliver the message to the messages database and notify the IMS provider of the + * result by calling {@link #acknowledgeSms(int, int)}. + * + * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called. + * + * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and + * {@link SmsMessage#FORMAT_3GPP2}. + * @param pdu PDUs representing the contents of the message. + * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()} + */ + public final void onSmsReceived(String format, byte[] pdu) throws IllegalStateException { + synchronized (mLock) { + if (mListener == null) { + throw new IllegalStateException("Feature not ready."); + } + try { + mListener.onSmsReceived(format, pdu); + acknowledgeSms(-1, DELIVER_STATUS_OK); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage()); + acknowledgeSms(-1, DELIVER_STATUS_ERROR); + } + } + } + + /** + * This method should be triggered by the IMS providers to pass the result of the sent message + * to the platform. + * + * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called. + * + * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040 + * @param status result of sending the SMS. Valid values are defined in {@link SendStatusResult} + * @param reason reason in case status is failure. Valid values are: + * {@link SmsManager#RESULT_ERROR_NONE}, + * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE}, + * {@link SmsManager#RESULT_ERROR_RADIO_OFF}, + * {@link SmsManager#RESULT_ERROR_NULL_PDU}, + * {@link SmsManager#RESULT_ERROR_NO_SERVICE}, + * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED}, + * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED}, + * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED} + * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()} + * @throws RemoteException if the connection to the framework is not available. If this happens + * attempting to send the SMS should be aborted. + */ + public final void onSendSmsResult(int messageRef, @SendStatusResult int status, int reason) + throws IllegalStateException, RemoteException { + synchronized (mLock) { + if (mListener == null) { + throw new IllegalStateException("Feature not ready."); + } + mListener.onSendSmsResult(messageRef, status, reason); + } + } + + /** + * Sets the status report of the sent message. + * + * @param messageRef the message reference. + * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and + * {@link SmsMessage#FORMAT_3GPP2}. + * @param pdu PDUs representing the content of the status report. + * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()} + */ + public final void onSmsStatusReportReceived(int messageRef, String format, byte[] pdu) { + synchronized (mLock) { + if (mListener == null) { + throw new IllegalStateException("Feature not ready."); + } + try { + mListener.onSmsStatusReportReceived(messageRef, format, pdu); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage()); + acknowledgeSmsReport(messageRef, STATUS_REPORT_STATUS_ERROR); + } + } + } + + /** + * Returns the SMS format. Default is {@link SmsMessage#FORMAT_3GPP} unless overridden by IMS + * Provider. + * + * @return the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and + * {@link SmsMessage#FORMAT_3GPP2}. + */ + public String getSmsFormat() { + return SmsMessage.FORMAT_3GPP; + } + +} diff --git a/android/telephony/ims/internal/feature/CapabilityChangeRequest.java b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java new file mode 100644 index 00000000..4d188734 --- /dev/null +++ b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.feature; + +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.ims.internal.stub.ImsRegistrationImplBase; +import android.util.ArraySet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Request to send to IMS provider, which will try to enable/disable capabilities that are added to + * the request. + * {@hide} + */ +public class CapabilityChangeRequest implements Parcelable { + + public static class CapabilityPair { + private final int mCapability; + private final int radioTech; + + public CapabilityPair(int capability, int radioTech) { + this.mCapability = capability; + this.radioTech = radioTech; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CapabilityPair)) return false; + + CapabilityPair that = (CapabilityPair) o; + + if (getCapability() != that.getCapability()) return false; + return getRadioTech() == that.getRadioTech(); + } + + @Override + public int hashCode() { + int result = getCapability(); + result = 31 * result + getRadioTech(); + return result; + } + + public @MmTelFeature.MmTelCapabilities.MmTelCapability int getCapability() { + return mCapability; + } + + public @ImsRegistrationImplBase.ImsRegistrationTech int getRadioTech() { + return radioTech; + } + } + + // Pair contains + private final Set mCapabilitiesToEnable; + // Pair contains + private final Set mCapabilitiesToDisable; + + public CapabilityChangeRequest() { + mCapabilitiesToEnable = new ArraySet<>(); + mCapabilitiesToDisable = new ArraySet<>(); + } + + /** + * Add one or many capabilities to the request to be enabled. + * + * @param capabilities A bitfield of capabilities to enable, valid values are defined in + * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}. + * @param radioTech the radio tech that these capabilities should be enabled for, valid + * values are in {@link ImsRegistrationImplBase.ImsRegistrationTech}. + */ + public void addCapabilitiesToEnableForTech( + @MmTelFeature.MmTelCapabilities.MmTelCapability int capabilities, + @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) { + addAllCapabilities(mCapabilitiesToEnable, capabilities, radioTech); + } + + /** + * Add one or many capabilities to the request to be disabled. + * @param capabilities A bitfield of capabilities to diable, valid values are defined in + * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}. + * @param radioTech the radio tech that these capabilities should be disabled for, valid + * values are in {@link ImsRegistrationImplBase.ImsRegistrationTech}. + */ + public void addCapabilitiesToDisableForTech( + @MmTelFeature.MmTelCapabilities.MmTelCapability int capabilities, + @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) { + addAllCapabilities(mCapabilitiesToDisable, capabilities, radioTech); + } + + /** + * @return a {@link List} of {@link CapabilityPair}s that are requesting to be enabled. + */ + public List getCapabilitiesToEnable() { + return new ArrayList<>(mCapabilitiesToEnable); + } + + /** + * @return a {@link List} of {@link CapabilityPair}s that are requesting to be disabled. + */ + public List getCapabilitiesToDisable() { + return new ArrayList<>(mCapabilitiesToDisable); + } + + // Iterate through capabilities bitfield and add each one as a pair associated with the radio + // technology + private void addAllCapabilities(Set set, int capabilities, int tech) { + long highestCapability = Long.highestOneBit(capabilities); + for (int i = 1; i <= highestCapability; i *= 2) { + if ((i & capabilities) > 0) { + set.add(new CapabilityPair(/*capability*/ i, /*radioTech*/ tech)); + } + } + } + + protected CapabilityChangeRequest(Parcel in) { + int enableSize = in.readInt(); + mCapabilitiesToEnable = new ArraySet<>(enableSize); + for (int i = 0; i < enableSize; i++) { + mCapabilitiesToEnable.add(new CapabilityPair(/*capability*/ in.readInt(), + /*radioTech*/ in.readInt())); + } + int disableSize = in.readInt(); + mCapabilitiesToDisable = new ArraySet<>(disableSize); + for (int i = 0; i < disableSize; i++) { + mCapabilitiesToDisable.add(new CapabilityPair(/*capability*/ in.readInt(), + /*radioTech*/ in.readInt())); + } + } + + public static final Creator CREATOR = + new Creator() { + @Override + public CapabilityChangeRequest createFromParcel(Parcel in) { + return new CapabilityChangeRequest(in); + } + + @Override + public CapabilityChangeRequest[] newArray(int size) { + return new CapabilityChangeRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mCapabilitiesToEnable.size()); + for (CapabilityPair pair : mCapabilitiesToEnable) { + dest.writeInt(pair.getCapability()); + dest.writeInt(pair.getRadioTech()); + } + dest.writeInt(mCapabilitiesToDisable.size()); + for (CapabilityPair pair : mCapabilitiesToDisable) { + dest.writeInt(pair.getCapability()); + dest.writeInt(pair.getRadioTech()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CapabilityChangeRequest)) return false; + + CapabilityChangeRequest that = (CapabilityChangeRequest) o; + + if (!mCapabilitiesToEnable.equals(that.mCapabilitiesToEnable)) return false; + return mCapabilitiesToDisable.equals(that.mCapabilitiesToDisable); + } + + @Override + public int hashCode() { + int result = mCapabilitiesToEnable.hashCode(); + result = 31 * result + mCapabilitiesToDisable.hashCode(); + return result; + } +} diff --git a/android/telephony/ims/internal/feature/ImsFeature.java b/android/telephony/ims/internal/feature/ImsFeature.java new file mode 100644 index 00000000..9f82ad24 --- /dev/null +++ b/android/telephony/ims/internal/feature/ImsFeature.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.feature; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.os.IInterface; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.telephony.SubscriptionManager; +import android.telephony.ims.internal.aidl.IImsCapabilityCallback; +import android.util.Log; + +import com.android.ims.internal.IImsFeatureStatusCallback; +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Base class for all IMS features that are supported by the framework. + * + * @hide + */ +public abstract class ImsFeature { + + private static final String LOG_TAG = "ImsFeature"; + + /** + * Action to broadcast when ImsService is up. + * Internal use only. + * Only defined here separately for compatibility purposes with the old ImsService. + * + * @hide + */ + public static final String ACTION_IMS_SERVICE_UP = + "com.android.ims.IMS_SERVICE_UP"; + + /** + * Action to broadcast when ImsService is down. + * Internal use only. + * Only defined here separately for compatibility purposes with the old ImsService. + * + * @hide + */ + public static final String ACTION_IMS_SERVICE_DOWN = + "com.android.ims.IMS_SERVICE_DOWN"; + + /** + * Part of the ACTION_IMS_SERVICE_UP or _DOWN intents. + * A long value; the phone ID corresponding to the IMS service coming up or down. + * Only defined here separately for compatibility purposes with the old ImsService. + * + * @hide + */ + public static final String EXTRA_PHONE_ID = "android:phone_id"; + + // Invalid feature value + public static final int FEATURE_INVALID = -1; + // ImsFeatures that are defined in the Manifests. Ensure that these values match the previously + // defined values in ImsServiceClass for compatibility purposes. + public static final int FEATURE_EMERGENCY_MMTEL = 0; + public static final int FEATURE_MMTEL = 1; + public static final int FEATURE_RCS = 2; + // Total number of features defined + public static final int FEATURE_MAX = 3; + + // Integer values defining IMS features that are supported in ImsFeature. + @IntDef(flag = true, + value = { + FEATURE_EMERGENCY_MMTEL, + FEATURE_MMTEL, + FEATURE_RCS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FeatureType {} + + // Integer values defining the state of the ImsFeature at any time. + @IntDef(flag = true, + value = { + STATE_UNAVAILABLE, + STATE_INITIALIZING, + STATE_READY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ImsState {} + + public static final int STATE_UNAVAILABLE = 0; + public static final int STATE_INITIALIZING = 1; + public static final int STATE_READY = 2; + + // Integer values defining the result codes that should be returned from + // {@link changeEnabledCapabilities} when the framework tries to set a feature's capability. + @IntDef(flag = true, + value = { + CAPABILITY_ERROR_GENERIC, + CAPABILITY_SUCCESS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ImsCapabilityError {} + + public static final int CAPABILITY_ERROR_GENERIC = -1; + public static final int CAPABILITY_SUCCESS = 0; + + + /** + * The framework implements this callback in order to register for Feature Capability status + * updates, via {@link #onCapabilitiesStatusChanged(Capabilities)}, query Capability + * configurations, via {@link #onQueryCapabilityConfiguration}, as well as to receive error + * callbacks when the ImsService can not change the capability as requested, via + * {@link #onChangeCapabilityConfigurationError}. + */ + public static class CapabilityCallback extends IImsCapabilityCallback.Stub { + + @Override + public final void onCapabilitiesStatusChanged(int config) throws RemoteException { + onCapabilitiesStatusChanged(new Capabilities(config)); + } + + /** + * Returns the result of a query for the capability configuration of a requested capability. + * + * @param capability The capability that was requested. + * @param radioTech The IMS radio technology associated with the capability. + * @param isEnabled true if the capability is enabled, false otherwise. + */ + @Override + public void onQueryCapabilityConfiguration(int capability, int radioTech, + boolean isEnabled) { + + } + + /** + * Called when a change to the capability configuration has returned an error. + * + * @param capability The capability that was requested to be changed. + * @param radioTech The IMS radio technology associated with the capability. + * @param reason error associated with the failure to change configuration. + */ + @Override + public void onChangeCapabilityConfigurationError(int capability, int radioTech, + int reason) { + } + + /** + * The status of the feature's capabilities has changed to either available or unavailable. + * If unavailable, the feature is not able to support the unavailable capability at this + * time. + * + * @param config The new availability of the capabilities. + */ + public void onCapabilitiesStatusChanged(Capabilities config) { + } + } + + /** + * Used by the ImsFeature to call back to the CapabilityCallback that the framework has + * provided. + */ + protected static class CapabilityCallbackProxy { + private final IImsCapabilityCallback mCallback; + + public CapabilityCallbackProxy(IImsCapabilityCallback c) { + mCallback = c; + } + + /** + * This method notifies the provided framework callback that the request to change the + * indicated capability has failed and has not changed. + * + * @param capability The Capability that will be notified to the framework. + * @param radioTech The radio tech that this capability failed for. + * @param reason The reason this capability was unable to be changed. + */ + public void onChangeCapabilityConfigurationError(int capability, int radioTech, + @ImsCapabilityError int reason) { + try { + mCallback.onChangeCapabilityConfigurationError(capability, radioTech, reason); + } catch (RemoteException e) { + Log.e(LOG_TAG, "onChangeCapabilityConfigurationError called on dead binder."); + } + } + + public void onQueryCapabilityConfiguration(int capability, int radioTech, + boolean isEnabled) { + try { + mCallback.onQueryCapabilityConfiguration(capability, radioTech, isEnabled); + } catch (RemoteException e) { + Log.e(LOG_TAG, "onQueryCapabilityConfiguration called on dead binder."); + } + } + } + + /** + * Contains the capabilities defined and supported by an ImsFeature in the form of a bit mask. + */ + public static class Capabilities { + protected int mCapabilities = 0; + + public Capabilities() { + } + + protected Capabilities(int capabilities) { + mCapabilities = capabilities; + } + + /** + * @param capabilities Capabilities to be added to the configuration in the form of a + * bit mask. + */ + public void addCapabilities(int capabilities) { + mCapabilities |= capabilities; + } + + /** + * @param capabilities Capabilities to be removed to the configuration in the form of a + * bit mask. + */ + public void removeCapabilities(int capabilities) { + mCapabilities &= ~capabilities; + } + + /** + * @return true if all of the capabilities specified are capable. + */ + public boolean isCapable(int capabilities) { + return (mCapabilities & capabilities) == capabilities; + } + + public Capabilities copy() { + return new Capabilities(mCapabilities); + } + + /** + * @return a bitmask containing the capability flags directly. + */ + public int getMask() { + return mCapabilities; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Capabilities)) return false; + + Capabilities that = (Capabilities) o; + + return mCapabilities == that.mCapabilities; + } + + @Override + public int hashCode() { + return mCapabilities; + } + } + + private final Set mStatusCallbacks = Collections.newSetFromMap( + new WeakHashMap()); + private @ImsState int mState = STATE_UNAVAILABLE; + private int mSlotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX; + private Context mContext; + private final Object mLock = new Object(); + private final RemoteCallbackList mCapabilityCallbacks + = new RemoteCallbackList<>(); + private Capabilities mCapabilityStatus = new Capabilities(); + + public final void initialize(Context context, int slotId) { + mContext = context; + mSlotId = slotId; + } + + public final int getFeatureState() { + synchronized (mLock) { + return mState; + } + } + + protected final void setFeatureState(@ImsState int state) { + synchronized (mLock) { + if (mState != state) { + mState = state; + notifyFeatureState(state); + } + } + } + + // Not final for testing, but shouldn't be extended! + @VisibleForTesting + public void addImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) { + try { + // If we have just connected, send queued status. + c.notifyImsFeatureStatus(getFeatureState()); + // Add the callback if the callback completes successfully without a RemoteException. + synchronized (mLock) { + mStatusCallbacks.add(c); + } + } catch (RemoteException e) { + Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage()); + } + } + + @VisibleForTesting + // Not final for testing, but should not be extended! + public void removeImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) { + synchronized (mLock) { + mStatusCallbacks.remove(c); + } + } + + /** + * Internal method called by ImsFeature when setFeatureState has changed. + */ + private void notifyFeatureState(@ImsState int state) { + synchronized (mLock) { + for (Iterator iter = mStatusCallbacks.iterator(); + iter.hasNext(); ) { + IImsFeatureStatusCallback callback = iter.next(); + try { + Log.i(LOG_TAG, "notifying ImsFeatureState=" + state); + callback.notifyImsFeatureStatus(state); + } catch (RemoteException e) { + // remove if the callback is no longer alive. + iter.remove(); + Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage()); + } + } + } + sendImsServiceIntent(state); + } + + /** + * Provide backwards compatibility using deprecated service UP/DOWN intents. + */ + private void sendImsServiceIntent(@ImsState int state) { + if (mContext == null || mSlotId == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + return; + } + Intent intent; + switch (state) { + case ImsFeature.STATE_UNAVAILABLE: + case ImsFeature.STATE_INITIALIZING: + intent = new Intent(ACTION_IMS_SERVICE_DOWN); + break; + case ImsFeature.STATE_READY: + intent = new Intent(ACTION_IMS_SERVICE_UP); + break; + default: + intent = new Intent(ACTION_IMS_SERVICE_DOWN); + } + intent.putExtra(EXTRA_PHONE_ID, mSlotId); + mContext.sendBroadcast(intent); + } + + public final void addCapabilityCallback(IImsCapabilityCallback c) { + mCapabilityCallbacks.register(c); + } + + public final void removeCapabilityCallback(IImsCapabilityCallback c) { + mCapabilityCallbacks.unregister(c); + } + + /** + * @return the cached capabilities status for this feature. + */ + @VisibleForTesting + public Capabilities queryCapabilityStatus() { + synchronized (mLock) { + return mCapabilityStatus.copy(); + } + } + + // Called internally to request the change of enabled capabilities. + @VisibleForTesting + public final void requestChangeEnabledCapabilities(CapabilityChangeRequest request, + IImsCapabilityCallback c) throws RemoteException { + if (request == null) { + throw new IllegalArgumentException( + "ImsFeature#requestChangeEnabledCapabilities called with invalid params."); + } + changeEnabledCapabilities(request, new CapabilityCallbackProxy(c)); + } + + /** + * Called by the ImsFeature when the capabilities status has changed. + * + * @param c A {@link Capabilities} containing the new Capabilities status. + */ + protected final void notifyCapabilitiesStatusChanged(Capabilities c) { + synchronized (mLock) { + mCapabilityStatus = c.copy(); + } + int count = mCapabilityCallbacks.beginBroadcast(); + try { + for (int i = 0; i < count; i++) { + try { + mCapabilityCallbacks.getBroadcastItem(i).onCapabilitiesStatusChanged( + c.mCapabilities); + } catch (RemoteException e) { + Log.w(LOG_TAG, e + " " + "notifyCapabilitiesStatusChanged() - Skipping " + + "callback."); + } + } + } finally { + mCapabilityCallbacks.finishBroadcast(); + } + } + + /** + * Features should override this method to receive Capability preference change requests from + * the framework using the provided {@link CapabilityChangeRequest}. If any of the capabilities + * in the {@link CapabilityChangeRequest} are not able to be completed due to an error, + * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError} should be called for + * each failed capability. + * + * @param request A {@link CapabilityChangeRequest} containing requested capabilities to + * enable/disable. + * @param c A {@link CapabilityCallbackProxy}, which will be used to call back to the framework + * setting a subset of these capabilities fail, using + * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError}. + */ + public abstract void changeEnabledCapabilities(CapabilityChangeRequest request, + CapabilityCallbackProxy c); + + /** + * Called when the framework is removing this feature and it needs to be cleaned up. + */ + public abstract void onFeatureRemoved(); + + /** + * Called when the feature has been initialized and communication with the framework is set up. + * Any attempt by this feature to access the framework before this method is called will return + * with an {@link IllegalStateException}. + * The IMS provider should use this method to trigger registration for this feature on the IMS + * network, if needed. + */ + public abstract void onFeatureReady(); + + /** + * @return Binder instance that the framework will use to communicate with this feature. + */ + protected abstract IInterface getBinder(); +} diff --git a/android/telephony/ims/internal/feature/MmTelFeature.java b/android/telephony/ims/internal/feature/MmTelFeature.java new file mode 100644 index 00000000..2f350c86 --- /dev/null +++ b/android/telephony/ims/internal/feature/MmTelFeature.java @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.feature; + +import android.annotation.IntDef; +import android.os.Message; +import android.os.RemoteException; +import android.telecom.TelecomManager; +import android.telephony.ims.internal.ImsCallSessionListener; +import android.telephony.ims.internal.SmsImplBase; +import android.telephony.ims.internal.SmsImplBase.DeliverStatusResult; +import android.telephony.ims.internal.SmsImplBase.StatusReportResult; +import android.telephony.ims.internal.aidl.IImsCallSessionListener; +import android.telephony.ims.internal.aidl.IImsCapabilityCallback; +import android.telephony.ims.internal.aidl.IImsMmTelFeature; +import android.telephony.ims.internal.aidl.IImsMmTelListener; +import android.telephony.ims.internal.stub.ImsRegistrationImplBase; +import android.telephony.ims.internal.aidl.IImsSmsListener; +import android.telephony.ims.stub.ImsEcbmImplBase; +import android.telephony.ims.stub.ImsMultiEndpointImplBase; +import android.telephony.ims.stub.ImsUtImplBase; +import android.util.Log; + +import com.android.ims.ImsCallProfile; +import com.android.ims.internal.IImsCallSession; +import com.android.ims.internal.IImsEcbm; +import com.android.ims.internal.IImsMultiEndpoint; +import com.android.ims.internal.IImsUt; +import com.android.ims.internal.ImsCallSession; +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Base implementation for Voice and SMS (IR-92) and Video (IR-94) IMS support. + * + * Any class wishing to use MmTelFeature should extend this class and implement all methods that the + * service supports. + * @hide + */ + +public class MmTelFeature extends ImsFeature { + + private static final String LOG_TAG = "MmTelFeature"; + + private final IImsMmTelFeature mImsMMTelBinder = new IImsMmTelFeature.Stub() { + + @Override + public void setListener(IImsMmTelListener l) throws RemoteException { + synchronized (mLock) { + MmTelFeature.this.setListener(l); + } + } + + @Override + public void setSmsListener(IImsSmsListener l) throws RemoteException { + MmTelFeature.this.setSmsListener(l); + } + + @Override + public int getFeatureState() throws RemoteException { + synchronized (mLock) { + return MmTelFeature.this.getFeatureState(); + } + } + + + @Override + public ImsCallProfile createCallProfile(int callSessionType, int callType) + throws RemoteException { + synchronized (mLock) { + return MmTelFeature.this.createCallProfile(callSessionType, callType); + } + } + + @Override + public IImsCallSession createCallSession(ImsCallProfile profile, + IImsCallSessionListener listener) throws RemoteException { + synchronized (mLock) { + ImsCallSession s = MmTelFeature.this.createCallSession(profile, + new ImsCallSessionListener(listener)); + return s != null ? s.getSession() : null; + } + } + + @Override + public IImsUt getUtInterface() throws RemoteException { + synchronized (mLock) { + return MmTelFeature.this.getUt(); + } + } + + @Override + public IImsEcbm getEcbmInterface() throws RemoteException { + synchronized (mLock) { + return MmTelFeature.this.getEcbm(); + } + } + + @Override + public void setUiTtyMode(int uiTtyMode, Message onCompleteMessage) throws RemoteException { + synchronized (mLock) { + MmTelFeature.this.setUiTtyMode(uiTtyMode, onCompleteMessage); + } + } + + @Override + public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException { + synchronized (mLock) { + return MmTelFeature.this.getMultiEndpoint(); + } + } + + @Override + public int queryCapabilityStatus() throws RemoteException { + return MmTelFeature.this.queryCapabilityStatus().mCapabilities; + } + + @Override + public void addCapabilityCallback(IImsCapabilityCallback c) { + MmTelFeature.this.addCapabilityCallback(c); + } + + @Override + public void removeCapabilityCallback(IImsCapabilityCallback c) { + MmTelFeature.this.removeCapabilityCallback(c); + } + + @Override + public void changeCapabilitiesConfiguration(CapabilityChangeRequest request, + IImsCapabilityCallback c) throws RemoteException { + MmTelFeature.this.requestChangeEnabledCapabilities(request, c); + } + + @Override + public void queryCapabilityConfiguration(int capability, int radioTech, + IImsCapabilityCallback c) { + queryCapabilityConfigurationInternal(capability, radioTech, c); + } + + @Override + public void sendSms(int messageRef, String format, String smsc, boolean retry, byte[] pdu) { + synchronized (mLock) { + MmTelFeature.this.sendSms(messageRef, format, smsc, retry, pdu); + } + } + + @Override + public void acknowledgeSms(int messageRef, int result) { + synchronized (mLock) { + MmTelFeature.this.acknowledgeSms(messageRef, result); + } + } + + @Override + public void acknowledgeSmsReport(int messageRef, int result) { + synchronized (mLock) { + MmTelFeature.this.acknowledgeSmsReport(messageRef, result); + } + } + + @Override + public String getSmsFormat() { + synchronized (mLock) { + return MmTelFeature.this.getSmsFormat(); + } + } + }; + + /** + * Contains the capabilities defined and supported by a MmTelFeature in the form of a Bitmask. + * The capabilities that are used in MmTelFeature are defined by {@link MmTelCapability}. + * + * The capabilities of this MmTelFeature will be set by the framework and can be queried with + * {@link #queryCapabilityStatus()}. + * + * This MmTelFeature can then return the status of each of these capabilities (enabled or not) + * by sending a {@link #notifyCapabilitiesStatusChanged} callback to the framework. The current + * status can also be queried using {@link #queryCapabilityStatus()}. + */ + public static class MmTelCapabilities extends Capabilities { + + @VisibleForTesting + public MmTelCapabilities() { + super(); + } + + public MmTelCapabilities(Capabilities c) { + mCapabilities = c.mCapabilities; + } + + @IntDef(flag = true, + value = { + CAPABILITY_TYPE_VOICE, + CAPABILITY_TYPE_VIDEO, + CAPABILITY_TYPE_UT, + CAPABILITY_TYPE_SMS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MmTelCapability {} + + /** + * This MmTelFeature supports Voice calling (IR.92) + */ + public static final int CAPABILITY_TYPE_VOICE = 1 << 0; + + /** + * This MmTelFeature supports Video (IR.94) + */ + public static final int CAPABILITY_TYPE_VIDEO = 1 << 1; + + /** + * This MmTelFeature supports XCAP over Ut for supplementary services. (IR.92) + */ + public static final int CAPABILITY_TYPE_UT = 1 << 2; + + /** + * This MmTelFeature supports SMS (IR.92) + */ + public static final int CAPABILITY_TYPE_SMS = 1 << 3; + + @Override + public final void addCapabilities(@MmTelCapability int capabilities) { + super.addCapabilities(capabilities); + } + + @Override + public final void removeCapabilities(@MmTelCapability int capability) { + super.removeCapabilities(capability); + } + + @Override + public final boolean isCapable(@MmTelCapability int capabilities) { + return super.isCapable(capabilities); + } + } + + /** + * Listener that the framework implements for communication from the MmTelFeature. + */ + public static class Listener extends IImsMmTelListener.Stub { + + @Override + public final void onIncomingCall(IImsCallSession c) { + onIncomingCall(new ImsCallSession(c)); + } + + /** + * Called when the IMS provider receives an incoming call. + * @param c The {@link ImsCallSession} associated with the new call. + */ + public void onIncomingCall(ImsCallSession c) { + } + } + + // Lock for feature synchronization + private final Object mLock = new Object(); + private IImsMmTelListener mListener; + + /** + * @param listener A {@link Listener} used when the MmTelFeature receives an incoming call and + * notifies the framework. + */ + private void setListener(IImsMmTelListener listener) { + synchronized (mLock) { + mListener = listener; + } + } + + private void setSmsListener(IImsSmsListener listener) { + getSmsImplementation().registerSmsListener(listener); + } + + private void queryCapabilityConfigurationInternal(int capability, int radioTech, + IImsCapabilityCallback c) { + boolean enabled = queryCapabilityConfiguration(capability, radioTech); + try { + if (c != null) { + c.onQueryCapabilityConfiguration(capability, radioTech, enabled); + } + } catch (RemoteException e) { + Log.e(LOG_TAG, "queryCapabilityConfigurationInternal called on dead binder!"); + } + } + + /** + * The current capability status that this MmTelFeature has defined is available. This + * configuration will be used by the platform to figure out which capabilities are CURRENTLY + * available to be used. + * + * Should be a subset of the capabilities that are enabled by the framework in + * {@link #changeEnabledCapabilities}. + * @return A copy of the current MmTelFeature capability status. + */ + @Override + public final MmTelCapabilities queryCapabilityStatus() { + return new MmTelCapabilities(super.queryCapabilityStatus()); + } + + /** + * Notify the framework that the status of the Capabilities has changed. Even though the + * MmTelFeature capability may be enabled by the framework, the status may be disabled due to + * the feature being unavailable from the network. + * @param c The current capability status of the MmTelFeature. If a capability is disabled, then + * the status of that capability is disabled. This can happen if the network does not currently + * support the capability that is enabled. A capability that is disabled by the framework (via + * {@link #changeEnabledCapabilities}) should also show the status as disabled. + */ + protected final void notifyCapabilitiesStatusChanged(MmTelCapabilities c) { + super.notifyCapabilitiesStatusChanged(c); + } + + /** + * Notify the framework of an incoming call. + * @param c The {@link ImsCallSession} of the new incoming call. + * + * @throws RemoteException if the connection to the framework is not available. If this happens, + * the call should be no longer considered active and should be cleaned up. + * */ + protected final void notifyIncomingCall(ImsCallSession c) throws RemoteException { + synchronized (mLock) { + if (mListener == null) { + throw new IllegalStateException("Session is not available."); + } + mListener.onIncomingCall(c.getSession()); + } + } + + /** + * Provides the MmTelFeature with the ability to return the framework Capability Configuration + * for a provided Capability. If the framework calls {@link #changeEnabledCapabilities} and + * includes a capability A to enable or disable, this method should return the correct enabled + * status for capability A. + * @param capability The capability that we are querying the configuration for. + * @return true if the capability is enabled, false otherwise. + */ + public boolean queryCapabilityConfiguration(@MmTelCapabilities.MmTelCapability int capability, + @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) { + // Base implementation - Override to provide functionality + return false; + } + + /** + * The MmTelFeature should override this method to handle the enabling/disabling of + * MmTel Features, defined in {@link MmTelCapabilities.MmTelCapability}. The framework assumes + * the {@link CapabilityChangeRequest} was processed successfully. If a subset of capabilities + * could not be set to their new values, + * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError} must be called + * individually for each capability whose processing resulted in an error. + * + * Enabling/Disabling a capability here indicates that the capability should be registered or + * deregistered (depending on the capability change) and become available or unavailable to + * the framework. + */ + @Override + public void changeEnabledCapabilities(CapabilityChangeRequest request, + CapabilityCallbackProxy c) { + // Base implementation, no-op + } + + /** + * Creates a {@link ImsCallProfile} from the service capabilities & IMS registration state. + * + * @param callSessionType a service type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#SERVICE_TYPE_NONE} + * {@link ImsCallProfile#SERVICE_TYPE_NORMAL} + * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY} + * @param callType a call type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#CALL_TYPE_VOICE} + * {@link ImsCallProfile#CALL_TYPE_VT} + * {@link ImsCallProfile#CALL_TYPE_VT_TX} + * {@link ImsCallProfile#CALL_TYPE_VT_RX} + * {@link ImsCallProfile#CALL_TYPE_VT_NODIR} + * {@link ImsCallProfile#CALL_TYPE_VS} + * {@link ImsCallProfile#CALL_TYPE_VS_TX} + * {@link ImsCallProfile#CALL_TYPE_VS_RX} + * @return a {@link ImsCallProfile} object + */ + public ImsCallProfile createCallProfile(int callSessionType, int callType) { + // Base Implementation - Should be overridden + return null; + } + + /** + * Creates an {@link ImsCallSession} with the specified call profile. + * Use other methods, if applicable, instead of interacting with + * {@link ImsCallSession} directly. + * + * @param profile a call profile to make the call + * @param listener An implementation of IImsCallSessionListener. + */ + public ImsCallSession createCallSession(ImsCallProfile profile, + ImsCallSessionListener listener) { + // Base Implementation - Should be overridden + return null; + } + + /** + * @return The Ut interface for the supplementary service configuration. + */ + public ImsUtImplBase getUt() { + // Base Implementation - Should be overridden + return null; + } + + /** + * @return The Emergency call-back mode interface for emergency VoLTE calls that support it. + */ + public ImsEcbmImplBase getEcbm() { + // Base Implementation - Should be overridden + return null; + } + + /** + * @return The Emergency call-back mode interface for emergency VoLTE calls that support it. + */ + public ImsMultiEndpointImplBase getMultiEndpoint() { + // Base Implementation - Should be overridden + return null; + } + + /** + * Sets the current UI TTY mode for the MmTelFeature. + * @param mode An integer containing the new UI TTY Mode, can consist of + * {@link TelecomManager#TTY_MODE_OFF}, + * {@link TelecomManager#TTY_MODE_FULL}, + * {@link TelecomManager#TTY_MODE_HCO}, + * {@link TelecomManager#TTY_MODE_VCO} + * @param onCompleteMessage A {@link Message} to be used when the mode has been set. + */ + void setUiTtyMode(int mode, Message onCompleteMessage) { + // Base Implementation - Should be overridden + } + + private void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) { + getSmsImplementation().sendSms(messageRef, format, smsc, isRetry, pdu); + } + + private void acknowledgeSms(int messageRef, @DeliverStatusResult int result) { + getSmsImplementation().acknowledgeSms(messageRef, result); + } + + private void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) { + getSmsImplementation().acknowledgeSmsReport(messageRef, result); + } + + private String getSmsFormat() { + return getSmsImplementation().getSmsFormat(); + } + + /** + * Must be overridden by IMS Provider to be able to support SMS over IMS. Otherwise a default + * non-functional implementation is returned. + * + * @return an instance of {@link SmsImplBase} which should be implemented by the IMS Provider. + */ + protected SmsImplBase getSmsImplementation() { + return new SmsImplBase(); + } + + /**{@inheritDoc}*/ + @Override + public void onFeatureRemoved() { + // Base Implementation - Should be overridden + } + + /**{@inheritDoc}*/ + @Override + public void onFeatureReady() { + // Base Implementation - Should be overridden + } + + /** + * @hide + */ + @Override + public final IImsMmTelFeature getBinder() { + return mImsMMTelBinder; + } +} diff --git a/android/telephony/ims/internal/feature/RcsFeature.java b/android/telephony/ims/internal/feature/RcsFeature.java new file mode 100644 index 00000000..8d1bd9d2 --- /dev/null +++ b/android/telephony/ims/internal/feature/RcsFeature.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.feature; + +import android.telephony.ims.internal.aidl.IImsRcsFeature; + +/** + * Base implementation of the RcsFeature APIs. Any ImsService wishing to support RCS should extend + * this class and provide implementations of the RcsFeature methods that they support. + * @hide + */ + +public class RcsFeature extends ImsFeature { + + private final IImsRcsFeature mImsRcsBinder = new IImsRcsFeature.Stub() { + // Empty Default Implementation. + }; + + + public RcsFeature() { + super(); + } + + @Override + public void changeEnabledCapabilities(CapabilityChangeRequest request, + CapabilityCallbackProxy c) { + // Do nothing for base implementation. + } + + @Override + public void onFeatureRemoved() { + + } + + /**{@inheritDoc}*/ + @Override + public void onFeatureReady() { + + } + + @Override + public final IImsRcsFeature getBinder() { + return mImsRcsBinder; + } +} diff --git a/android/telephony/ims/internal/stub/ImsConfigImplBase.java b/android/telephony/ims/internal/stub/ImsConfigImplBase.java new file mode 100644 index 00000000..33aec5df --- /dev/null +++ b/android/telephony/ims/internal/stub/ImsConfigImplBase.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.stub; + +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.telephony.ims.internal.aidl.IImsConfig; +import android.telephony.ims.internal.aidl.IImsConfigCallback; + +import com.android.ims.ImsConfig; + +/** + * Controls the modification of IMS specific configurations. For more information on the supported + * IMS configuration constants, see {@link ImsConfig}. + * + * @hide + */ + +public class ImsConfigImplBase { + + //TODO: Implement the Binder logic to call base APIs. Need to finish other ImsService Config + // work first. + private final IImsConfig mBinder = new IImsConfig.Stub() { + + @Override + public void addImsConfigCallback(IImsConfigCallback c) throws RemoteException { + ImsConfigImplBase.this.addImsConfigCallback(c); + } + + @Override + public void removeImsConfigCallback(IImsConfigCallback c) throws RemoteException { + ImsConfigImplBase.this.removeImsConfigCallback(c); + } + + @Override + public int getConfigInt(int item) throws RemoteException { + return Integer.MIN_VALUE; + } + + @Override + public String getConfigString(int item) throws RemoteException { + return null; + } + + @Override + public int setConfigInt(int item, int value) throws RemoteException { + return Integer.MIN_VALUE; + } + + @Override + public int setConfigString(int item, String value) throws RemoteException { + return Integer.MIN_VALUE; + } + }; + + public class Callback extends IImsConfigCallback.Stub { + + @Override + public final void onIntConfigChanged(int item, int value) throws RemoteException { + onConfigChanged(item, value); + } + + @Override + public final void onStringConfigChanged(int item, String value) throws RemoteException { + onConfigChanged(item, value); + } + + /** + * Called when the IMS configuration has changed. + * @param item the IMS configuration key constant, as defined in ImsConfig. + * @param value the new integer value of the IMS configuration constant. + */ + public void onConfigChanged(int item, int value) { + // Base Implementation + } + + /** + * Called when the IMS configuration has changed. + * @param item the IMS configuration key constant, as defined in ImsConfig. + * @param value the new String value of the IMS configuration constant. + */ + public void onConfigChanged(int item, String value) { + // Base Implementation + } + } + + private final RemoteCallbackList mCallbacks = new RemoteCallbackList<>(); + + /** + * Adds a {@link Callback} to the list of callbacks notified when a value in the configuration + * changes. + * @param c callback to add. + */ + private void addImsConfigCallback(IImsConfigCallback c) { + mCallbacks.register(c); + } + /** + * Removes a {@link Callback} to the list of callbacks notified when a value in the + * configuration changes. + * + * @param c callback to remove. + */ + private void removeImsConfigCallback(IImsConfigCallback c) { + mCallbacks.unregister(c); + } + + public final IImsConfig getBinder() { + return mBinder; + } + + /** + * Sets the value for IMS service/capabilities parameters by the operator device + * management entity. It sets the config item value in the provisioned storage + * from which the master value is derived. + * + * @param item as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in Integer format. + * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. + */ + public int setConfig(int item, int value) { + // Base Implementation - To be overridden. + return ImsConfig.OperationStatusConstants.FAILED; + } + + /** + * Sets the value for IMS service/capabilities parameters by the operator device + * management entity. It sets the config item value in the provisioned storage + * from which the master value is derived. + * + * @param item as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in String format. + * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. + */ + public int setConfig(int item, String value) { + return ImsConfig.OperationStatusConstants.FAILED; + } + + /** + * Gets the value for ims service/capabilities parameters from the provisioned + * value storage. + * + * @param item as defined in com.android.ims.ImsConfig#ConfigConstants. + * @return value in Integer format. + */ + public int getConfigInt(int item) { + return ImsConfig.OperationStatusConstants.FAILED; + } + + /** + * Gets the value for ims service/capabilities parameters from the provisioned + * value storage. + * + * @param item as defined in com.android.ims.ImsConfig#ConfigConstants. + * @return value in String format. + */ + public String getConfigString(int item) { + return null; + } +} diff --git a/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java b/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java new file mode 100644 index 00000000..244c9578 --- /dev/null +++ b/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.stub; + +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.ims.internal.feature.ImsFeature; +import android.util.ArraySet; + +import java.util.Arrays; +import java.util.Set; + +/** + * Container class for IMS Feature configuration. This class contains the features that the + * ImsService supports, which are defined in {@link ImsFeature.FeatureType}. + * @hide + */ +public class ImsFeatureConfiguration implements Parcelable { + /** + * Features that this ImsService supports. + */ + private final Set mFeatures; + + /** + * Creates an ImsFeatureConfiguration with the features + */ + public static class Builder { + ImsFeatureConfiguration mConfig; + public Builder() { + mConfig = new ImsFeatureConfiguration(); + } + + /** + * @param feature A feature defined in {@link ImsFeature.FeatureType} that this service + * supports. + * @return a {@link Builder} to continue constructing the ImsFeatureConfiguration. + */ + public Builder addFeature(@ImsFeature.FeatureType int feature) { + mConfig.addFeature(feature); + return this; + } + + public ImsFeatureConfiguration build() { + return mConfig; + } + } + + /** + * Creates with all registration features empty. + * + * Consider using the provided {@link Builder} to create this configuration instead. + */ + public ImsFeatureConfiguration() { + mFeatures = new ArraySet<>(); + } + + /** + * Configuration of the ImsService, which describes which features the ImsService supports + * (for registration). + * @param features an array of feature integers defined in {@link ImsFeature} that describe + * which features this ImsService supports. + */ + public ImsFeatureConfiguration(int[] features) { + mFeatures = new ArraySet<>(); + + if (features != null) { + for (int i : features) { + mFeatures.add(i); + } + } + } + + /** + * @return an int[] containing the features that this ImsService supports. + */ + public int[] getServiceFeatures() { + return mFeatures.stream().mapToInt(i->i).toArray(); + } + + void addFeature(int feature) { + mFeatures.add(feature); + } + + protected ImsFeatureConfiguration(Parcel in) { + int[] features = in.createIntArray(); + if (features != null) { + mFeatures = new ArraySet<>(features.length); + for(Integer i : features) { + mFeatures.add(i); + } + } else { + mFeatures = new ArraySet<>(); + } + } + + public static final Creator CREATOR + = new Creator() { + @Override + public ImsFeatureConfiguration createFromParcel(Parcel in) { + return new ImsFeatureConfiguration(in); + } + + @Override + public ImsFeatureConfiguration[] newArray(int size) { + return new ImsFeatureConfiguration[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeIntArray(mFeatures.stream().mapToInt(i->i).toArray()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ImsFeatureConfiguration)) return false; + + ImsFeatureConfiguration that = (ImsFeatureConfiguration) o; + + return mFeatures.equals(that.mFeatures); + } + + @Override + public int hashCode() { + return mFeatures.hashCode(); + } +} diff --git a/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java b/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java new file mode 100644 index 00000000..558b009a --- /dev/null +++ b/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.telephony.ims.internal.stub; + +import android.annotation.IntDef; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.telephony.ims.internal.aidl.IImsRegistration; +import android.telephony.ims.internal.aidl.IImsRegistrationCallback; +import android.util.Log; + +import com.android.ims.ImsReasonInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Controls IMS registration for this ImsService and notifies the framework when the IMS + * registration for this ImsService has changed status. + * @hide + */ + +public class ImsRegistrationImplBase { + + private static final String LOG_TAG = "ImsRegistrationImplBase"; + + // Defines the underlying radio technology type that we have registered for IMS over. + @IntDef(flag = true, + value = { + REGISTRATION_TECH_NONE, + REGISTRATION_TECH_LTE, + REGISTRATION_TECH_IWLAN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ImsRegistrationTech {} + /** + * No registration technology specified, used when we are not registered. + */ + public static final int REGISTRATION_TECH_NONE = -1; + /** + * IMS is registered to IMS via LTE. + */ + public static final int REGISTRATION_TECH_LTE = 0; + /** + * IMS is registered to IMS via IWLAN. + */ + public static final int REGISTRATION_TECH_IWLAN = 1; + + // Registration states, used to notify new ImsRegistrationImplBase#Callbacks of the current + // state. + private static final int REGISTRATION_STATE_NOT_REGISTERED = 0; + private static final int REGISTRATION_STATE_REGISTERING = 1; + private static final int REGISTRATION_STATE_REGISTERED = 2; + + + /** + * Callback class for receiving Registration callback events. + */ + public static class Callback extends IImsRegistrationCallback.Stub { + + /** + * Notifies the framework when the IMS Provider is connected to the IMS network. + * + * @param imsRadioTech the radio access technology. Valid values are defined in + * {@link ImsRegistrationTech}. + */ + @Override + public void onRegistered(@ImsRegistrationTech int imsRadioTech) { + } + + /** + * Notifies the framework when the IMS Provider is trying to connect the IMS network. + * + * @param imsRadioTech the radio access technology. Valid values are defined in + * {@link ImsRegistrationTech}. + */ + @Override + public void onRegistering(@ImsRegistrationTech int imsRadioTech) { + } + + /** + * Notifies the framework when the IMS Provider is disconnected from the IMS network. + * + * @param info the {@link ImsReasonInfo} associated with why registration was disconnected. + */ + @Override + public void onDeregistered(ImsReasonInfo info) { + } + + /** + * A failure has occurred when trying to handover registration to another technology type, + * defined in {@link ImsRegistrationTech} + * + * @param imsRadioTech The {@link ImsRegistrationTech} type that has failed + * @param info A {@link ImsReasonInfo} that identifies the reason for failure. + */ + @Override + public void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech, + ImsReasonInfo info) { + } + } + + private final IImsRegistration mBinder = new IImsRegistration.Stub() { + + @Override + public @ImsRegistrationTech int getRegistrationTechnology() throws RemoteException { + return getConnectionType(); + } + + @Override + public void addRegistrationCallback(IImsRegistrationCallback c) throws RemoteException { + ImsRegistrationImplBase.this.addRegistrationCallback(c); + } + + @Override + public void removeRegistrationCallback(IImsRegistrationCallback c) throws RemoteException { + ImsRegistrationImplBase.this.removeRegistrationCallback(c); + } + }; + + private final RemoteCallbackList mCallbacks + = new RemoteCallbackList<>(); + private final Object mLock = new Object(); + // Locked on mLock + private @ImsRegistrationTech + int mConnectionType = REGISTRATION_TECH_NONE; + // Locked on mLock + private int mRegistrationState = REGISTRATION_STATE_NOT_REGISTERED; + // Locked on mLock + private ImsReasonInfo mLastDisconnectCause; + + public final IImsRegistration getBinder() { + return mBinder; + } + + private void addRegistrationCallback(IImsRegistrationCallback c) throws RemoteException { + mCallbacks.register(c); + updateNewCallbackWithState(c); + } + + private void removeRegistrationCallback(IImsRegistrationCallback c) { + mCallbacks.unregister(c); + } + + /** + * Notify the framework that the device is connected to the IMS network. + * + * @param imsRadioTech the radio access technology. Valid values are defined in + * {@link ImsRegistrationTech}. + */ + public final void onRegistered(@ImsRegistrationTech int imsRadioTech) { + updateToState(imsRadioTech, REGISTRATION_STATE_REGISTERED); + mCallbacks.broadcast((c) -> { + try { + c.onRegistered(imsRadioTech); + } catch (RemoteException e) { + Log.w(LOG_TAG, e + " " + "onRegistrationConnected() - Skipping " + + "callback."); + } + }); + } + + /** + * Notify the framework that the device is trying to connect the IMS network. + * + * @param imsRadioTech the radio access technology. Valid values are defined in + * {@link ImsRegistrationTech}. + */ + public final void onRegistering(@ImsRegistrationTech int imsRadioTech) { + updateToState(imsRadioTech, REGISTRATION_STATE_REGISTERING); + mCallbacks.broadcast((c) -> { + try { + c.onRegistering(imsRadioTech); + } catch (RemoteException e) { + Log.w(LOG_TAG, e + " " + "onRegistrationProcessing() - Skipping " + + "callback."); + } + }); + } + + /** + * Notify the framework that the device is disconnected from the IMS network. + * + * @param info the {@link ImsReasonInfo} associated with why registration was disconnected. + */ + public final void onDeregistered(ImsReasonInfo info) { + updateToDisconnectedState(info); + mCallbacks.broadcast((c) -> { + try { + c.onDeregistered(info); + } catch (RemoteException e) { + Log.w(LOG_TAG, e + " " + "onRegistrationDisconnected() - Skipping " + + "callback."); + } + }); + } + + public final void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech, + ImsReasonInfo info) { + mCallbacks.broadcast((c) -> { + try { + c.onTechnologyChangeFailed(imsRadioTech, info); + } catch (RemoteException e) { + Log.w(LOG_TAG, e + " " + "onRegistrationChangeFailed() - Skipping " + + "callback."); + } + }); + } + + private void updateToState(@ImsRegistrationTech int connType, int newState) { + synchronized (mLock) { + mConnectionType = connType; + mRegistrationState = newState; + mLastDisconnectCause = null; + } + } + + private void updateToDisconnectedState(ImsReasonInfo info) { + synchronized (mLock) { + updateToState(REGISTRATION_TECH_NONE, REGISTRATION_STATE_NOT_REGISTERED); + if (info != null) { + mLastDisconnectCause = info; + } else { + Log.w(LOG_TAG, "updateToDisconnectedState: no ImsReasonInfo provided."); + mLastDisconnectCause = new ImsReasonInfo(); + } + } + } + + private @ImsRegistrationTech int getConnectionType() { + synchronized (mLock) { + return mConnectionType; + } + } + + /** + * @param c the newly registered callback that will be updated with the current registration + * state. + */ + private void updateNewCallbackWithState(IImsRegistrationCallback c) throws RemoteException { + int state; + ImsReasonInfo disconnectInfo; + synchronized (mLock) { + state = mRegistrationState; + disconnectInfo = mLastDisconnectCause; + } + switch (state) { + case REGISTRATION_STATE_NOT_REGISTERED: { + c.onDeregistered(disconnectInfo); + break; + } + case REGISTRATION_STATE_REGISTERING: { + c.onRegistering(getConnectionType()); + break; + } + case REGISTRATION_STATE_REGISTERED: { + c.onRegistered(getConnectionType()); + break; + } + } + } +} diff --git a/android/telephony/ims/stub/ImsConfigImplBase.java b/android/telephony/ims/stub/ImsConfigImplBase.java index 5a4db99e..1670e6b9 100644 --- a/android/telephony/ims/stub/ImsConfigImplBase.java +++ b/android/telephony/ims/stub/ImsConfigImplBase.java @@ -16,15 +16,23 @@ package android.telephony.ims.stub; +import android.content.Context; +import android.content.Intent; import android.os.RemoteException; +import android.util.Log; import com.android.ims.ImsConfig; import com.android.ims.ImsConfigListener; import com.android.ims.internal.IImsConfig; +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + /** - * Base implementation of ImsConfig, which implements stub versions of the methods - * in the IImsConfig AIDL. Override the methods that your implementation of ImsConfig supports. + * Base implementation of ImsConfig. + * Override the methods that your implementation of ImsConfig supports. * * DO NOT remove or change the existing APIs, only add new ones to this Base implementation or you * will break other implementations of ImsConfig maintained by other ImsServices. @@ -34,10 +42,25 @@ import com.android.ims.internal.IImsConfig; * 1) Items provisioned by the operator. * 2) Items configured by user. Mainly service feature class. * + * The inner class {@link ImsConfigStub} implements methods of IImsConfig AIDL interface. + * The IImsConfig AIDL interface is called by ImsConfig, which may exist in many other processes. + * ImsConfigImpl access to the configuration parameters may be arbitrarily slow, especially in + * during initialization, or times when a lot of configuration parameters are being set/get + * (such as during boot up or SIM card change). By providing a cache in ImsConfigStub, we can speed + * up access to these configuration parameters, so a query to the ImsConfigImpl does not have to be + * performed every time. * @hide */ -public class ImsConfigImplBase extends IImsConfig.Stub { +public class ImsConfigImplBase { + + static final private String TAG = "ImsConfigImplBase"; + + ImsConfigStub mImsConfigStub; + + public ImsConfigImplBase(Context context) { + mImsConfigStub = new ImsConfigStub(this, context); + } /** * Gets the value for ims service/capabilities parameters from the provisioned @@ -46,7 +69,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. * @return value in Integer format. */ - @Override public int getProvisionedValue(int item) throws RemoteException { return -1; } @@ -58,7 +80,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. * @return value in String format. */ - @Override public String getProvisionedStringValue(int item) throws RemoteException { return null; } @@ -72,7 +93,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param value in Integer format. * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. */ - @Override public int setProvisionedValue(int item, int value) throws RemoteException { return ImsConfig.OperationStatusConstants.FAILED; } @@ -86,7 +106,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param value in String format. * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. */ - @Override public int setProvisionedStringValue(int item, String value) throws RemoteException { return ImsConfig.OperationStatusConstants.FAILED; } @@ -100,7 +119,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param network as defined in android.telephony.TelephonyManager#NETWORK_TYPE_XXX. * @param listener feature value returned asynchronously through listener. */ - @Override public void getFeatureValue(int feature, int network, ImsConfigListener listener) throws RemoteException { } @@ -115,7 +133,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param value as defined in com.android.ims.ImsConfig#FeatureValueConstants. * @param listener, provided if caller needs to be notified for set result. */ - @Override public void setFeatureValue(int feature, int network, int value, ImsConfigListener listener) throws RemoteException { } @@ -124,7 +141,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * Gets the value for IMS VoLTE provisioned. * This should be the same as the operator provisioned value if applies. */ - @Override public boolean getVolteProvisioned() throws RemoteException { return false; } @@ -134,7 +150,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * * @param listener Video quality value returned asynchronously through listener. */ - @Override public void getVideoQuality(ImsConfigListener listener) throws RemoteException { } @@ -144,7 +159,233 @@ public class ImsConfigImplBase extends IImsConfig.Stub { * @param quality, defines the value of video quality. * @param listener, provided if caller needs to be notified for set result. */ - @Override public void setVideoQuality(int quality, ImsConfigListener listener) throws RemoteException { } + + public IImsConfig getIImsConfig() { return mImsConfigStub; } + + /** + * Updates provisioning value and notifies the framework of the change. + * Doesn't call #setProvisionedValue and assumes the result succeeded. + * This should only be used by modem when they implicitly changed provisioned values. + * + * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in Integer format. + */ + public final void notifyProvisionedValueChanged(int item, int value) { + mImsConfigStub.updateCachedValue(item, value, true); + } + + /** + * Updates provisioning value and notifies the framework of the change. + * Doesn't call #setProvisionedValue and assumes the result succeeded. + * This should only be used by modem when they implicitly changed provisioned values. + * + * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in String format. + */ + public final void notifyProvisionedValueChanged(int item, String value) { + mImsConfigStub.updateCachedValue(item, value, true); + } + + /** + * Implements the IImsConfig AIDL interface, which is called by potentially many processes + * in order to get/set configuration parameters. + * + * It holds an object of ImsConfigImplBase class which is usually extended by ImsConfigImpl + * with actual implementations from vendors. This class caches provisioned values from + * ImsConfigImpl layer because queries through ImsConfigImpl can be slow. When query goes in, + * it first checks cache layer. If missed, it will call the vendor implementation of + * ImsConfigImplBase API. + * and cache the return value if the set succeeds. + * + * Provides APIs to get/set the IMS service feature/capability/parameters. + * The config items include: + * 1) Items provisioned by the operator. + * 2) Items configured by user. Mainly service feature class. + * + * @hide + */ + @VisibleForTesting + static public class ImsConfigStub extends IImsConfig.Stub { + Context mContext; + WeakReference mImsConfigImplBaseWeakReference; + private HashMap mProvisionedIntValue = new HashMap<>(); + private HashMap mProvisionedStringValue = new HashMap<>(); + + @VisibleForTesting + public ImsConfigStub(ImsConfigImplBase imsConfigImplBase, Context context) { + mContext = context; + mImsConfigImplBaseWeakReference = + new WeakReference(imsConfigImplBase); + } + + /** + * Gets the value for ims service/capabilities parameters. It first checks its local cache, + * if missed, it will call ImsConfigImplBase.getProvisionedValue. + * Synchronous blocking call. + * + * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. + * @return value in Integer format. + */ + @Override + public synchronized int getProvisionedValue(int item) throws RemoteException { + if (mProvisionedIntValue.containsKey(item)) { + return mProvisionedIntValue.get(item); + } else { + int retVal = getImsConfigImpl().getProvisionedValue(item); + if (retVal != ImsConfig.OperationStatusConstants.UNKNOWN) { + updateCachedValue(item, retVal, false); + } + return retVal; + } + } + + /** + * Gets the value for ims service/capabilities parameters. It first checks its local cache, + * if missed, it will call #ImsConfigImplBase.getProvisionedValue. + * Synchronous blocking call. + * + * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. + * @return value in String format. + */ + @Override + public synchronized String getProvisionedStringValue(int item) throws RemoteException { + if (mProvisionedIntValue.containsKey(item)) { + return mProvisionedStringValue.get(item); + } else { + String retVal = getImsConfigImpl().getProvisionedStringValue(item); + if (retVal != null) { + updateCachedValue(item, retVal, false); + } + return retVal; + } + } + + /** + * Sets the value for IMS service/capabilities parameters by the operator device + * management entity. It sets the config item value in the provisioned storage + * from which the master value is derived, and write it into local cache. + * Synchronous blocking call. + * + * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in Integer format. + * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. + */ + @Override + public synchronized int setProvisionedValue(int item, int value) throws RemoteException { + mProvisionedIntValue.remove(item); + int retVal = getImsConfigImpl().setProvisionedValue(item, value); + if (retVal == ImsConfig.OperationStatusConstants.SUCCESS) { + updateCachedValue(item, retVal, true); + } else { + Log.d(TAG, "Set provision value of " + item + + " to " + value + " failed with error code " + retVal); + } + + return retVal; + } + + /** + * Sets the value for IMS service/capabilities parameters by the operator device + * management entity. It sets the config item value in the provisioned storage + * from which the master value is derived, and write it into local cache. + * Synchronous blocking call. + * + * @param item as defined in com.android.ims.ImsConfig#ConfigConstants. + * @param value in String format. + * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants. + */ + @Override + public synchronized int setProvisionedStringValue(int item, String value) + throws RemoteException { + mProvisionedStringValue.remove(item); + int retVal = getImsConfigImpl().setProvisionedStringValue(item, value); + if (retVal == ImsConfig.OperationStatusConstants.SUCCESS) { + updateCachedValue(item, retVal, true); + } + + return retVal; + } + + /** + * Wrapper function to call ImsConfigImplBase.getFeatureValue. + */ + @Override + public void getFeatureValue(int feature, int network, ImsConfigListener listener) + throws RemoteException { + getImsConfigImpl().getFeatureValue(feature, network, listener); + } + + /** + * Wrapper function to call ImsConfigImplBase.setFeatureValue. + */ + @Override + public void setFeatureValue(int feature, int network, int value, ImsConfigListener listener) + throws RemoteException { + getImsConfigImpl().setFeatureValue(feature, network, value, listener); + } + + /** + * Wrapper function to call ImsConfigImplBase.getVolteProvisioned. + */ + @Override + public boolean getVolteProvisioned() throws RemoteException { + return getImsConfigImpl().getVolteProvisioned(); + } + + /** + * Wrapper function to call ImsConfigImplBase.getVideoQuality. + */ + @Override + public void getVideoQuality(ImsConfigListener listener) throws RemoteException { + getImsConfigImpl().getVideoQuality(listener); + } + + /** + * Wrapper function to call ImsConfigImplBase.setVideoQuality. + */ + @Override + public void setVideoQuality(int quality, ImsConfigListener listener) + throws RemoteException { + getImsConfigImpl().setVideoQuality(quality, listener); + } + + private ImsConfigImplBase getImsConfigImpl() throws RemoteException { + ImsConfigImplBase ref = mImsConfigImplBaseWeakReference.get(); + if (ref == null) { + throw new RemoteException("Fail to get ImsConfigImpl"); + } else { + return ref; + } + } + + private void sendImsConfigChangedIntent(int item, int value) { + sendImsConfigChangedIntent(item, Integer.toString(value)); + } + + private void sendImsConfigChangedIntent(int item, String value) { + Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED); + configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item); + configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value); + if (mContext != null) { + mContext.sendBroadcast(configChangedIntent); + } + } + + protected synchronized void updateCachedValue(int item, int value, boolean notifyChange) { + mProvisionedIntValue.put(item, value); + if (notifyChange) { + sendImsConfigChangedIntent(item, value); + } + } + + protected synchronized void updateCachedValue( + int item, String value, boolean notifyChange) { + mProvisionedStringValue.put(item, value); + if (notifyChange) { + sendImsConfigChangedIntent(item, value); + } + } + } } diff --git a/android/telephony/ims/stub/ImsUtImplBase.java b/android/telephony/ims/stub/ImsUtImplBase.java index dc74094d..054a8b22 100644 --- a/android/telephony/ims/stub/ImsUtImplBase.java +++ b/android/telephony/ims/stub/ImsUtImplBase.java @@ -52,6 +52,15 @@ public class ImsUtImplBase extends IImsUt.Stub { return -1; } + /** + * Retrieves the configuration of the call barring for specified service class. + */ + @Override + public int queryCallBarringForServiceClass(int cbType, int serviceClass) + throws RemoteException { + return -1; + } + /** * Retrieves the configuration of the call forward. */ @@ -116,6 +125,15 @@ public class ImsUtImplBase extends IImsUt.Stub { return -1; } + /** + * Updates the configuration of the call barring for specified service class. + */ + @Override + public int updateCallBarringForServiceClass(int cbType, int action, String[] barrList, + int serviceClass) throws RemoteException { + return -1; + } + /** * Updates the configuration of the call forward. */ diff --git a/android/telephony/ims/stub/ImsUtListenerImplBase.java b/android/telephony/ims/stub/ImsUtListenerImplBase.java index b371efb6..daa74c8f 100644 --- a/android/telephony/ims/stub/ImsUtListenerImplBase.java +++ b/android/telephony/ims/stub/ImsUtListenerImplBase.java @@ -21,6 +21,7 @@ import android.os.RemoteException; import com.android.ims.ImsCallForwardInfo; import com.android.ims.ImsReasonInfo; +import com.android.ims.ImsSsData; import com.android.ims.ImsSsInfo; import com.android.ims.internal.IImsUt; import com.android.ims.internal.IImsUtListener; @@ -85,4 +86,10 @@ public class ImsUtListenerImplBase extends IImsUtListener.Stub { public void utConfigurationCallWaitingQueried(IImsUt ut, int id, ImsSsInfo[] cwInfo) throws RemoteException { } + + /** + * Notifies client when Supplementary Service indication is received + */ + @Override + public void onSupplementaryServiceIndication(ImsSsData ssData) {} } diff --git a/android/telephony/mbms/ServiceInfo.java b/android/telephony/mbms/ServiceInfo.java index 8529f525..f78e7a6e 100644 --- a/android/telephony/mbms/ServiceInfo.java +++ b/android/telephony/mbms/ServiceInfo.java @@ -51,8 +51,8 @@ public class ServiceInfo { /** @hide */ public ServiceInfo(Map newNames, String newClassName, List newLocales, String newServiceId, Date start, Date end) { - if (newNames == null || newNames.isEmpty() || TextUtils.isEmpty(newClassName) - || newLocales == null || newLocales.isEmpty() || TextUtils.isEmpty(newServiceId) + if (newNames == null || newClassName == null + || newLocales == null || newServiceId == null || start == null || end == null) { throw new IllegalArgumentException("Bad ServiceInfo construction"); } diff --git a/android/test/mock/MockContext.java b/android/test/mock/MockContext.java index 5e5ba462..4dfd0507 100644 --- a/android/test/mock/MockContext.java +++ b/android/test/mock/MockContext.java @@ -19,13 +19,12 @@ package android.test.mock; import android.annotation.SystemApi; import android.app.IApplicationThread; import android.app.IServiceConnection; -import android.app.Notification; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.BroadcastReceiver; import android.content.IntentSender; import android.content.ServiceConnection; import android.content.SharedPreferences; @@ -44,8 +43,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; -import android.view.DisplayAdjustments; import android.view.Display; +import android.view.DisplayAdjustments; import java.io.File; import java.io.FileInputStream; @@ -53,6 +52,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.Executor; /** * A mock {@link android.content.Context} class. All methods are non-functional and throw @@ -86,6 +86,11 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } + @Override + public Executor getMainExecutor() { + throw new UnsupportedOperationException(); + } + @Override public Context getApplicationContext() { throw new UnsupportedOperationException(); diff --git a/android/test/mock/MockPackageManager.java b/android/test/mock/MockPackageManager.java index 0c562e65..ce8019f8 100644 --- a/android/test/mock/MockPackageManager.java +++ b/android/test/mock/MockPackageManager.java @@ -46,6 +46,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; +import android.content.pm.dex.ArtManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Rect; @@ -1174,4 +1175,12 @@ public class MockPackageManager extends PackageManager { @Nullable DexModuleRegisterCallback callback) { throw new UnsupportedOperationException(); } + + /** + * @hide + */ + @Override + public ArtManager getArtManager() { + throw new UnsupportedOperationException(); + } } diff --git a/android/text/AutoGrowArray.java b/android/text/AutoGrowArray.java new file mode 100644 index 00000000..e428377a --- /dev/null +++ b/android/text/AutoGrowArray.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; + +import com.android.internal.util.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Implements a growing array of int primitives. + * + * These arrays are NOT thread safe. + * + * @hide + */ +public final class AutoGrowArray { + private static final int MIN_CAPACITY_INCREMENT = 12; + private static final int MAX_CAPACITY_TO_BE_KEPT = 10000; + + /** + * Returns next capacity size. + * + * The returned capacity is larger than requested capacity. + */ + private static int computeNewCapacity(int currentSize, int requested) { + final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) + ? MIN_CAPACITY_INCREMENT : currentSize >> 1); + return targetCapacity > requested ? targetCapacity : requested; + } + + /** + * An auto growing byte array. + */ + public static class ByteArray { + + private @NonNull byte[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty ByteArray with the default initial capacity. + */ + public ByteArray() { + this(10); + } + + /** + * Creates an empty ByteArray with the specified initial capacity. + */ + public ByteArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.BYTE; + } else { + mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(byte value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(@IntRange int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.BYTE; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public byte get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, byte value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull byte[] getRawArray() { + return mValues; + } + } + + /** + * An auto growing int array. + */ + public static class IntArray { + + private @NonNull int[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty IntArray with the default initial capacity. + */ + public IntArray() { + this(10); + } + + /** + * Creates an empty IntArray with the specified initial capacity. + */ + public IntArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.INT; + } else { + mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this IntArray. If this IntArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(int value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(@IntRange(from = 0) int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.INT; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public int get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, int value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull int[] getRawArray() { + return mValues; + } + } + + /** + * An auto growing float array. + */ + public static class FloatArray { + + private @NonNull float[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty FloatArray with the default initial capacity. + */ + public FloatArray() { + this(10); + } + + /** + * Creates an empty FloatArray with the specified initial capacity. + */ + public FloatArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.FLOAT; + } else { + mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(float value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.FLOAT; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public float get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, float value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull float[] getRawArray() { + return mValues; + } + } +} diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java index fba358cf..6bca37af 100644 --- a/android/text/DynamicLayout.java +++ b/android/text/DynamicLayout.java @@ -42,8 +42,7 @@ import java.lang.ref.WeakReference; * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) * Canvas.drawText()} directly.

    */ -public class DynamicLayout extends Layout -{ +public class DynamicLayout extends Layout { private static final int PRIORITY = 128; private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; @@ -303,8 +302,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the specified text that will be updated as the text is changed. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -315,9 +315,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the transformed text (password transformation being the primary example of - * a transformation) that will be updated as the base text is changed. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -328,10 +328,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the transformed text (password transformation being the primary example of - * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, - * the Layout will ellipsize the text down to ellipsizedWidth. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -351,7 +350,9 @@ public class DynamicLayout extends Layout * the Layout will ellipsize the text down to ellipsizedWidth. * * @hide + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @@ -492,7 +493,9 @@ public class DynamicLayout extends Layout } } - private void reflow(CharSequence s, int where, int before, int after) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void reflow(CharSequence s, int where, int before, int after) { if (s != mBase) return; @@ -805,8 +808,8 @@ public class DynamicLayout extends Layout return; } - int firstBlock = -1; - int lastBlock = -1; + /*final*/ int firstBlock = -1; + /*final*/ int lastBlock = -1; for (int i = 0; i < mNumberOfBlocks; i++) { if (mBlockEndLines[i] >= startLine) { firstBlock = i; @@ -821,10 +824,10 @@ public class DynamicLayout extends Layout } final int lastBlockEndLine = mBlockEndLines[lastBlock]; - boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : + final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : mBlockEndLines[firstBlock - 1] + 1); - boolean createBlock = newLineCount > 0; - boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; + final boolean createBlock = newLineCount > 0; + final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; int numAddedBlocks = 0; if (createBlockBefore) numAddedBlocks++; @@ -863,12 +866,18 @@ public class DynamicLayout extends Layout if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) { final ArraySet set = new ArraySet<>(); + final int changedBlockCount = numAddedBlocks - numRemovedBlocks; for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) { Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i); - if (block > firstBlock) { - block += numAddedBlocks - numRemovedBlocks; + if (block < firstBlock) { + // block index is before firstBlock add it since it did not change + set.add(block); + } + if (block > lastBlock) { + // block index is after lastBlock, the index reduced to += changedBlockCount + block += changedBlockCount; + set.add(block); } - set.add(block); } mBlocksAlwaysNeedToBeRedrawn = set; } diff --git a/android/text/FontConfig.java b/android/text/FontConfig.java index 4654e83c..7386e3e8 100644 --- a/android/text/FontConfig.java +++ b/android/text/FontConfig.java @@ -179,7 +179,11 @@ public final class FontConfig { /** @hide */ @Retention(SOURCE) - @IntDef({VARIANT_DEFAULT, VARIANT_COMPACT, VARIANT_ELEGANT}) + @IntDef(prefix = { "VARIANT_" }, value = { + VARIANT_DEFAULT, + VARIANT_COMPACT, + VARIANT_ELEGANT + }) public @interface Variant {} /** diff --git a/android/text/Layout.java b/android/text/Layout.java index 4d2a9629..bf4b6ac5 100644 --- a/android/text/Layout.java +++ b/android/text/Layout.java @@ -48,7 +48,11 @@ import java.util.Arrays; */ public abstract class Layout { /** @hide */ - @IntDef({BREAK_STRATEGY_SIMPLE, BREAK_STRATEGY_HIGH_QUALITY, BREAK_STRATEGY_BALANCED}) + @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { + BREAK_STRATEGY_SIMPLE, + BREAK_STRATEGY_HIGH_QUALITY, + BREAK_STRATEGY_BALANCED + }) @Retention(RetentionPolicy.SOURCE) public @interface BreakStrategy {} @@ -73,8 +77,11 @@ public abstract class Layout { public static final int BREAK_STRATEGY_BALANCED = 2; /** @hide */ - @IntDef({HYPHENATION_FREQUENCY_NORMAL, HYPHENATION_FREQUENCY_FULL, - HYPHENATION_FREQUENCY_NONE}) + @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = { + HYPHENATION_FREQUENCY_NORMAL, + HYPHENATION_FREQUENCY_FULL, + HYPHENATION_FREQUENCY_NONE + }) @Retention(RetentionPolicy.SOURCE) public @interface HyphenationFrequency {} @@ -105,7 +112,10 @@ public abstract class Layout { ArrayUtils.emptyArray(ParagraphStyle.class); /** @hide */ - @IntDef({JUSTIFICATION_MODE_NONE, JUSTIFICATION_MODE_INTER_WORD}) + @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { + JUSTIFICATION_MODE_NONE, + JUSTIFICATION_MODE_INTER_WORD + }) @Retention(RetentionPolicy.SOURCE) public @interface JustificationMode {} @@ -1907,22 +1917,14 @@ public abstract class Layout { private static float measurePara(TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - MeasuredText mt = MeasuredText.obtain(); + MeasuredText mt = null; TextLine tl = TextLine.obtain(); try { - mt.setPara(text, start, end, textDir); - Directions directions; - int dir; - if (mt.mEasy) { - directions = DIRS_ALL_LEFT_TO_RIGHT; - dir = Layout.DIR_LEFT_TO_RIGHT; - } else { - directions = AndroidBidi.directions(mt.mDir, mt.mLevels, - 0, mt.mChars, 0, mt.mLen); - dir = mt.mDir; - } - char[] chars = mt.mChars; - int len = mt.mLen; + mt = MeasuredText.buildForBidi(text, start, end, textDir, mt); + final char[] chars = mt.getChars(); + final int len = chars.length; + final Directions directions = mt.getDirections(0, len); + final int dir = mt.getParagraphDir(); boolean hasTabs = false; TabStops tabStops = null; // leading margins should be taken into account when measuring a paragraph @@ -1955,7 +1957,9 @@ public abstract class Layout { return margin + Math.abs(tl.metrics(null)); } finally { TextLine.recycle(tl); - MeasuredText.recycle(mt); + if (mt != null) { + mt.recycle(); + } } } @@ -2272,6 +2276,14 @@ public abstract class Layout { private SpanSet mLineBackgroundSpans; private int mJustificationMode; + /** @hide */ + @IntDef(prefix = { "DIR_" }, value = { + DIR_LEFT_TO_RIGHT, + DIR_RIGHT_TO_LEFT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Direction {} + public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; @@ -2309,7 +2321,10 @@ public abstract class Layout { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT}) + @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = { + TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, + TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT + }) public @interface TextSelectionLayout {} /** @hide */ diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java index 3d9fba71..14d6f9e8 100644 --- a/android/text/MeasuredText.java +++ b/android/text/MeasuredText.java @@ -16,125 +16,436 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Paint; +import android.text.AutoGrowArray.ByteArray; +import android.text.AutoGrowArray.FloatArray; +import android.text.AutoGrowArray.IntArray; +import android.text.Layout.Directions; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; -import android.util.Log; +import android.util.Pools.SynchronizedPool; -import com.android.internal.util.ArrayUtils; +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +import java.util.Arrays; /** + * MeasuredText provides text information for rendering purpose. + * + * The first motivation of this class is identify the text directions and retrieving individual + * character widths. However retrieving character widths is slower than identifying text directions. + * Thus, this class provides several builder methods for specific purposes. + * + * - buildForBidi: + * Compute only text directions. + * - buildForMeasurement: + * Compute text direction and all character widths. + * - buildForStaticLayout: + * This is bit special. StaticLayout also needs to know text direction and character widths for + * line breaking, but all things are done in native code. Similarly, text measurement is done + * in native code. So instead of storing result to Java array, this keeps the result in native + * code since there is no good reason to move the results to Java layer. + * + * In addition to the character widths, some additional information is computed for each purposes, + * e.g. whole text length for measurement or font metrics for static layout. + * + * MeasuredText is NOT a thread safe object. * @hide */ -class MeasuredText { - private static final boolean localLOGV = false; - CharSequence mText; - int mTextStart; - float[] mWidths; - char[] mChars; - byte[] mLevels; - int mDir; - boolean mEasy; - int mLen; - - private int mPos; - private TextPaint mWorkPaint; - - private MeasuredText() { - mWorkPaint = new TextPaint(); +public class MeasuredText { + private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; + + private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024); + + private MeasuredText() {} // Use build static functions instead. + + private static final SynchronizedPool sPool = new SynchronizedPool<>(1); + + private static @NonNull MeasuredText obtain() { // Use build static functions instead. + final MeasuredText mt = sPool.acquire(); + return mt != null ? mt : new MeasuredText(); } - private static final Object[] sLock = new Object[0]; - private static final MeasuredText[] sCached = new MeasuredText[3]; - - static MeasuredText obtain() { - MeasuredText mt; - synchronized (sLock) { - for (int i = sCached.length; --i >= 0;) { - if (sCached[i] != null) { - mt = sCached[i]; - sCached[i] = null; - return mt; - } - } + /** + * Recycle the MeasuredText. + * + * Do not call any methods after you call this method. + */ + public void recycle() { + release(); + sPool.release(this); + } + + // The casted original text. + // + // This may be null if the passed text is not a Spanned. + private @Nullable Spanned mSpanned; + + // The start offset of the target range in the original text (mSpanned); + private @IntRange(from = 0) int mTextStart; + + // The length of the target range in the original text. + private @IntRange(from = 0) int mTextLength; + + // The copied character buffer for measuring text. + // + // The length of this array is mTextLength. + private @Nullable char[] mCopiedBuffer; + + // The whole paragraph direction. + private @Layout.Direction int mParaDir; + + // True if the text is LTR direction and doesn't contain any bidi characters. + private boolean mLtrWithoutBidi; + + // The bidi level for individual characters. + // + // This is empty if mLtrWithoutBidi is true. + private @NonNull ByteArray mLevels = new ByteArray(); + + // The whole width of the text. + // See getWholeWidth comments. + private @FloatRange(from = 0.0f) float mWholeWidth; + + // Individual characters' widths. + // See getWidths comments. + private @Nullable FloatArray mWidths = new FloatArray(); + + // The span end positions. + // See getSpanEndCache comments. + private @Nullable IntArray mSpanEndCache = new IntArray(4); + + // The font metrics. + // See getFontMetrics comments. + private @Nullable IntArray mFontMetrics = new IntArray(4 * 4); + + // The native MeasuredText. + // See getNativePtr comments. + // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead. + private /* Maybe Zero */ long mNativePtr = 0; + private @Nullable Runnable mNativeObjectCleaner; + + // Associate the native object to this Java object. + private void bindNativeObject(/* Non Zero*/ long nativePtr) { + mNativePtr = nativePtr; + mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr); + } + + // Decouple the native object from this Java object and release the native object. + private void unbindNativeObject() { + if (mNativePtr != 0) { + mNativeObjectCleaner.run(); + mNativePtr = 0; } - mt = new MeasuredText(); - if (localLOGV) { - Log.v("MEAS", "new: " + mt); + } + + // Following two objects are for avoiding object allocation. + private @NonNull TextPaint mCachedPaint = new TextPaint(); + private @Nullable Paint.FontMetricsInt mCachedFm; + + /** + * Releases internal buffers. + */ + public void release() { + reset(); + mLevels.clearWithReleasingLargeArray(); + mWidths.clearWithReleasingLargeArray(); + mFontMetrics.clearWithReleasingLargeArray(); + mSpanEndCache.clearWithReleasingLargeArray(); + } + + /** + * Resets the internal state for starting new text. + */ + private void reset() { + mSpanned = null; + mCopiedBuffer = null; + mWholeWidth = 0; + mLevels.clear(); + mWidths.clear(); + mFontMetrics.clear(); + mSpanEndCache.clear(); + unbindNativeObject(); + } + + /** + * Returns the characters to be measured. + * + * This is always available. + */ + public @NonNull char[] getChars() { + return mCopiedBuffer; + } + + /** + * Returns the paragraph direction. + * + * This is always available. + */ + public @Layout.Direction int getParagraphDir() { + return mParaDir; + } + + /** + * Returns the directions. + * + * This is always available. + */ + public Directions getDirections(@IntRange(from = 0) int start, // inclusive + @IntRange(from = 0) int end) { // exclusive + if (mLtrWithoutBidi) { + return Layout.DIRS_ALL_LEFT_TO_RIGHT; } + + final int length = end - start; + return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start, + length); + } + + /** + * Returns the whole text width. + * + * This is available only if the MeasureText is computed with computeForMeasurement. + * Returns 0 in other cases. + */ + public @FloatRange(from = 0.0f) float getWholeWidth() { + return mWholeWidth; + } + + /** + * Returns the individual character's width. + * + * This is available only if the MeasureText is computed with computeForMeasurement. + * Returns empty array in other cases. + */ + public @NonNull FloatArray getWidths() { + return mWidths; + } + + /** + * Returns the MetricsAffectingSpan end indices. + * + * If the input text is not a spanned string, this has one value that is the length of the text. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns empty array in other cases. + */ + public @NonNull IntArray getSpanEndCache() { + return mSpanEndCache; + } + + /** + * Returns the int array which holds FontMetrics. + * + * This array holds the repeat of top, bottom, ascent, descent of font metrics value. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns empty array in other cases. + */ + public @NonNull IntArray getFontMetrics() { + return mFontMetrics; + } + + /** + * Returns the native ptr of the MeasuredText. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns 0 in other cases. + */ + public /* Maybe Zero */ long getNativePtr() { + return mNativePtr; + } + + /** + * Generates new MeasuredText for Bidi computation. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); return mt; } - static MeasuredText recycle(MeasuredText mt) { - mt.finish(); - synchronized(sLock) { - for (int i = 0; i < sCached.length; ++i) { - if (sCached[i] == null) { - sCached[i] = mt; - mt.mText = null; - break; - } + /** + * Generates new MeasuredText for measuring texts. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param paint the paint to be used for rendering the text. + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint, + @NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); + + mt.mWidths.resize(mt.mTextLength); + if (mt.mTextLength == 0) { + return mt; + } + + if (mt.mSpanned == null) { + // No style change by MetricsAffectingSpan. Just measure all text. + mt.applyMetricsAffectingSpan( + paint, null /* spans */, start, end, 0 /* native static layout ptr */); + } else { + // There may be a MetricsAffectingSpan. Split into span transitions and apply styles. + int spanEnd; + for (int spanStart = start; spanStart < end; spanStart = spanEnd) { + spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class); + MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, + MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); + mt.applyMetricsAffectingSpan( + paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */); } } - return null; + return mt; } - void finish() { - mText = null; - if (mLen > 1000) { - mWidths = null; - mChars = null; - mLevels = null; + /** + * Generates new MeasuredText for StaticLayout. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param paint the paint to be used for rendering the text. + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForStaticLayout( + @NonNull TextPaint paint, + @NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); + if (mt.mTextLength == 0) { + // Need to build empty native measured text for StaticLayout. + // TODO: Stop creating empty measured text for empty lines. + long nativeBuilderPtr = nInitBuilder(); + try { + mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer)); + } finally { + nFreeBuilder(nativeBuilderPtr); + } + return mt; + } + + long nativeBuilderPtr = nInitBuilder(); + try { + if (mt.mSpanned == null) { + // No style change by MetricsAffectingSpan. Just measure all text. + mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr); + mt.mSpanEndCache.append(end); + } else { + // There may be a MetricsAffectingSpan. Split into span transitions and apply + // styles. + int spanEnd; + for (int spanStart = start; spanStart < end; spanStart = spanEnd) { + spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, + MetricAffectingSpan.class); + MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, + MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, + MetricAffectingSpan.class); + mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, + nativeBuilderPtr); + mt.mSpanEndCache.append(spanEnd); + } + } + mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer)); + } finally { + nFreeBuilder(nativeBuilderPtr); } + + return mt; } /** - * Analyzes text for bidirectional runs. Allocates working buffers. + * Reset internal state and analyzes text for bidirectional runs. + * + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction */ - void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - mText = text; + private void resetAndAnalyzeBidi(@NonNull CharSequence text, + @IntRange(from = 0) int start, // inclusive + @IntRange(from = 0) int end, // exclusive + @NonNull TextDirectionHeuristic textDir) { + reset(); + mSpanned = text instanceof Spanned ? (Spanned) text : null; mTextStart = start; + mTextLength = end - start; - int len = end - start; - mLen = len; - mPos = 0; - - if (mWidths == null || mWidths.length < len) { - mWidths = ArrayUtils.newUnpaddedFloatArray(len); - } - if (mChars == null || mChars.length != len) { - mChars = new char[len]; + if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) { + mCopiedBuffer = new char[mTextLength]; } - TextUtils.getChars(text, start, end, mChars, 0); + TextUtils.getChars(text, start, end, mCopiedBuffer, 0); - if (text instanceof Spanned) { - Spanned spanned = (Spanned) text; - ReplacementSpan[] spans = spanned.getSpans(start, end, - ReplacementSpan.class); + // Replace characters associated with ReplacementSpan to U+FFFC. + if (mSpanned != null) { + ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class); for (int i = 0; i < spans.length; i++) { - int startInPara = spanned.getSpanStart(spans[i]) - start; - int endInPara = spanned.getSpanEnd(spans[i]) - start; - // The span interval may be larger and must be restricted to [start, end[ + int startInPara = mSpanned.getSpanStart(spans[i]) - start; + int endInPara = mSpanned.getSpanEnd(spans[i]) - start; + // The span interval may be larger and must be restricted to [start, end) if (startInPara < 0) startInPara = 0; - if (endInPara > len) endInPara = len; - for (int j = startInPara; j < endInPara; j++) { - mChars[j] = '\uFFFC'; // object replacement character - } + if (endInPara > mTextLength) endInPara = mTextLength; + Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER); } } if ((textDir == TextDirectionHeuristics.LTR || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR || textDir == TextDirectionHeuristics.ANYRTL_LTR) && - TextUtils.doesNotNeedBidi(mChars, 0, len)) { - mDir = Layout.DIR_LEFT_TO_RIGHT; - mEasy = true; + TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) { + mLevels.clear(); + mParaDir = Layout.DIR_LEFT_TO_RIGHT; + mLtrWithoutBidi = true; } else { - if (mLevels == null || mLevels.length < len) { - mLevels = ArrayUtils.newUnpaddedByteArray(len); - } - int bidiRequest; + final int bidiRequest; if (textDir == TextDirectionHeuristics.LTR) { bidiRequest = Layout.DIR_REQUEST_LTR; } else if (textDir == TextDirectionHeuristics.RTL) { @@ -144,122 +455,147 @@ class MeasuredText { } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; } else { - boolean isRtl = textDir.isRtl(mChars, 0, len); + final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength); bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; } - mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels); - mEasy = false; + mLevels.resize(mTextLength); + mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray()); + mLtrWithoutBidi = false; } } - /** - * Apply the style. - * - * If nativeStaticLayoutPtr is 0, this method measures the styled text width. - * If nativeStaticLayoutPtr is not 0, this method just passes the style information to native - * code by calling StaticLayout.addstyleRun() and returns 0. - */ - float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm, - long nativeStaticLayoutPtr) { - if (fm != null) { - paint.getFontMetricsInt(fm); + private void applyReplacementRun(@NonNull ReplacementSpan replacement, + @IntRange(from = 0) int start, // inclusive, in copied buffer + @IntRange(from = 0) int end, // exclusive, in copied buffer + /* Maybe Zero */ long nativeBuilderPtr) { + // Use original text. Shouldn't matter. + // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for + // backward compatibility? or Should we initialize them for getFontMetricsInt? + final float width = replacement.getSize( + mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); + if (nativeBuilderPtr == 0) { + // Assigns all width to the first character. This is the same behavior as minikin. + mWidths.set(start, width); + if (end > start + 1) { + Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f); + } + mWholeWidth += width; + } else { + nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end, + width); } + } - final int p = mPos; - mPos = p + len; + private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer + @IntRange(from = 0) int end, // exclusive, in copied buffer + /* Maybe Zero */ long nativeBuilderPtr) { + if (nativeBuilderPtr != 0) { + mCachedPaint.getFontMetricsInt(mCachedFm); + } - if (mEasy) { - final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; - if (nativeStaticLayoutPtr == 0) { - return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p); + if (mLtrWithoutBidi) { + // If the whole text is LTR direction, just apply whole region. + if (nativeBuilderPtr == 0) { + mWholeWidth += mCachedPaint.getTextRunAdvances( + mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, + mWidths.getRawArray(), start); } else { - StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl); - return 0.0f; // Builder.addStyleRun doesn't return the width. + nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end, + false /* isRtl */); } - } - - float totalAdvance = 0; - int level = mLevels[p]; - for (int q = p, i = p + 1, e = p + len;; ++i) { - if (i == e || mLevels[i] != level) { - final boolean isRtl = (level & 0x1) != 0; - if (nativeStaticLayoutPtr == 0) { - totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q); - } else { - // Builder.addStyleRun doesn't return the width. - StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl); - } - if (i == e) { - break; + } else { + // If there is multiple bidi levels, split into individual bidi level and apply style. + byte level = mLevels.get(start); + // Note that the empty text or empty range won't reach this method. + // Safe to search from start + 1. + for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) { + if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point + final boolean isRtl = (level & 0x1) != 0; + if (nativeBuilderPtr == 0) { + final int levelLength = levelEnd - levelStart; + mWholeWidth += mCachedPaint.getTextRunAdvances( + mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, + isRtl, mWidths.getRawArray(), levelStart); + } else { + nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart, + levelEnd, isRtl); + } + if (levelEnd == end) { + break; + } + levelStart = levelEnd; + level = mLevels.get(levelEnd); } - q = i; - level = mLevels[i]; } } - return totalAdvance; // If nativeStaticLayoutPtr is 0, the result is zero. } - float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { - return addStyleRun(paint, len, fm, 0 /* native ptr */); - } + private void applyMetricsAffectingSpan( + @NonNull TextPaint paint, + @Nullable MetricAffectingSpan[] spans, + @IntRange(from = 0) int start, // inclusive, in original text buffer + @IntRange(from = 0) int end, // exclusive, in original text buffer + /* Maybe Zero */ long nativeBuilderPtr) { + mCachedPaint.set(paint); + // XXX paint should not have a baseline shift, but... + mCachedPaint.baselineShift = 0; - float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, - Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) { + final boolean needFontMetrics = nativeBuilderPtr != 0; - TextPaint workPaint = mWorkPaint; - workPaint.set(paint); - // XXX paint should not have a baseline shift, but... - workPaint.baselineShift = 0; + if (needFontMetrics && mCachedFm == null) { + mCachedFm = new Paint.FontMetricsInt(); + } ReplacementSpan replacement = null; - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } else { - span.updateMeasureState(workPaint); + if (spans != null) { + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + // The last ReplacementSpan is effective for backward compatibility reasons. + replacement = (ReplacementSpan) span; + } else { + // TODO: No need to call updateMeasureState for ReplacementSpan as well? + span.updateMeasureState(mCachedPaint); + } } } - float wid; - if (replacement == null) { - wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr); + final int startInCopiedBuffer = start - mTextStart; + final int endInCopiedBuffer = end - mTextStart; + + if (replacement != null) { + applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, + nativeBuilderPtr); } else { - // Use original text. Shouldn't matter. - wid = replacement.getSize(workPaint, mText, mTextStart + mPos, - mTextStart + mPos + len, fm); - if (nativeStaticLayoutPtr == 0) { - float[] w = mWidths; - w[mPos] = wid; - for (int i = mPos + 1, e = mPos + len; i < e; i++) - w[i] = 0; - } else { - StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid); - } - mPos += len; + applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr); } - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; + if (needFontMetrics) { + if (mCachedPaint.baselineShift < 0) { + mCachedFm.ascent += mCachedPaint.baselineShift; + mCachedFm.top += mCachedPaint.baselineShift; } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; + mCachedFm.descent += mCachedPaint.baselineShift; + mCachedFm.bottom += mCachedPaint.baselineShift; } - } - - return wid; - } - float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, - Paint.FontMetricsInt fm) { - return addStyleRun(paint, spans, len, fm, 0 /* native ptr */); + mFontMetrics.append(mCachedFm.top); + mFontMetrics.append(mCachedFm.bottom); + mFontMetrics.append(mCachedFm.ascent); + mFontMetrics.append(mCachedFm.descent); + } } - int breakText(int limit, boolean forwards, float width) { - float[] w = mWidths; + /** + * Returns the maximum index that the accumulated width not exceeds the width. + * + * If forward=false is passed, returns the minimum index from the end instead. + * + * This only works if the MeasuredText is computed with computeForMeasurement. + * Undefined behavior in other case. + */ + @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) { + float[] w = mWidths.getRawArray(); if (forwards) { int i = 0; while (i < limit) { @@ -267,7 +603,7 @@ class MeasuredText { if (width < 0.0f) break; i++; } - while (i > 0 && mChars[i - 1] == ' ') i--; + while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--; return i; } else { int i = limit - 1; @@ -276,19 +612,65 @@ class MeasuredText { if (width < 0.0f) break; i--; } - while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) { + while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) { i++; } return limit - i - 1; } } - float measure(int start, int limit) { + /** + * Returns the length of the substring. + * + * This only works if the MeasuredText is computed with computeForMeasurement. + * Undefined behavior in other case. + */ + @FloatRange(from = 0.0f) float measure(int start, int limit) { float width = 0; - float[] w = mWidths; + float[] w = mWidths.getRawArray(); for (int i = start; i < limit; ++i) { width += w[i]; } return width; } + + private static native /* Non Zero */ long nInitBuilder(); + + /** + * Apply style to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param isRtl True if the text is RTL. + */ + private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + boolean isRtl); + + /** + * Apply ReplacementRun to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param width The width of the replacement. + */ + private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @FloatRange(from = 0) float width); + + private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr, + @NonNull char[] text); + + private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr); + + @CriticalNative + private static native /* Non Zero */ long nGetReleaseFunc(); } diff --git a/android/text/MeasuredText_Delegate.java b/android/text/MeasuredText_Delegate.java new file mode 100644 index 00000000..adcc774c --- /dev/null +++ b/android/text/MeasuredText_Delegate.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.annotation.NonNull; +import android.graphics.BidiRenderer; +import android.graphics.Paint; +import android.graphics.Paint_Delegate; +import android.graphics.RectF; +import android.text.StaticLayout_Delegate.Builder; +import android.text.StaticLayout_Delegate.Run; + +import java.util.ArrayList; +import java.util.Arrays; + +import libcore.util.NativeAllocationRegistry_Delegate; + +/** + * Delegate that provides implementation for native methods in {@link android.text.MeasuredText} + *

    + * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class MeasuredText_Delegate { + + // ---- Builder delegate manager ---- + private static final DelegateManager sBuilderManager = + new DelegateManager<>(MeasuredTextBuilder.class); + private static final DelegateManager sManager = + new DelegateManager<>(MeasuredText_Delegate.class); + private static long sFinalizer = -1; + + private long mNativeBuilderPtr; + + @LayoutlibDelegate + /*package*/ static long nInitBuilder() { + return sBuilderManager.addNewDelegate(new MeasuredTextBuilder()); + } + + /** + * Apply style to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param isRtl True if the text is RTL. + */ + @LayoutlibDelegate + /*package*/ static void nAddStyleRun(long nativeBuilderPtr, long paintPtr, int start, + int end, boolean isRtl) { + MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr); + if (builder == null) { + return; + } + builder.mRuns.add(new StyleRun(paintPtr, start, end, isRtl)); + } + + /** + * Apply ReplacementRun to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param width The width of the replacement. + */ + @LayoutlibDelegate + /*package*/ static void nAddReplacementRun(long nativeBuilderPtr, long paintPtr, int start, + int end, float width) { + MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr); + if (builder == null) { + return; + } + builder.mRuns.add(new ReplacementRun(start, end, width)); + } + + @LayoutlibDelegate + /*package*/ static long nBuildNativeMeasuredText(long nativeBuilderPtr, @NonNull char[] text) { + MeasuredText_Delegate delegate = new MeasuredText_Delegate(); + delegate.mNativeBuilderPtr = nativeBuilderPtr; + return sManager.addNewDelegate(delegate); + } + + @LayoutlibDelegate + /*package*/ static void nFreeBuilder(long nativeBuilderPtr) { + sBuilderManager.removeJavaReferenceFor(nativeBuilderPtr); + } + + @LayoutlibDelegate + /*package*/ static long nGetReleaseFunc() { + synchronized (MeasuredText_Delegate.class) { + if (sFinalizer == -1) { + sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( + sManager::removeJavaReferenceFor); + } + } + return sFinalizer; + } + + private static float measureText(long nativePaint, char[] text, int index, int count, + float[] widths, int bidiFlags) { + Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint); + RectF bounds = + new BidiRenderer(null, paint, text).renderText(index, index + count, bidiFlags, + widths, 0, false); + return bounds.right - bounds.left; + } + + public static void computeRuns(long measuredTextPtr, Builder staticLayoutBuilder) { + MeasuredText_Delegate delegate = sManager.getDelegate(measuredTextPtr); + if (delegate == null) { + return; + } + MeasuredTextBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr); + if (builder == null) { + return; + } + for (Run run: builder.mRuns) { + run.addTo(staticLayoutBuilder); + } + } + + private static class StyleRun extends Run { + private final long mNativePaint; + private final boolean mIsRtl; + + private StyleRun(long nativePaint, int start, int end, boolean isRtl) { + super(start, end); + mNativePaint = nativePaint; + mIsRtl = isRtl; + } + + @Override + void addTo(Builder builder) { + int bidiFlags = mIsRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR; + measureText(mNativePaint, builder.mText, mStart, mEnd - mStart, builder.mWidths, + bidiFlags); + } + } + + private static class ReplacementRun extends Run { + private final float mWidth; + + private ReplacementRun(int start, int end, float width) { + super(start, end); + mWidth = width; + } + + @Override + void addTo(Builder builder) { + builder.mWidths[mStart] = mWidth; + Arrays.fill(builder.mWidths, mStart + 1, mEnd, 0.0f); + } + } + + private static class MeasuredTextBuilder { + private final ArrayList mRuns = new ArrayList<>(); + } +} diff --git a/android/text/PremeasuredText.java b/android/text/PremeasuredText.java new file mode 100644 index 00000000..465314dd --- /dev/null +++ b/android/text/PremeasuredText.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.util.IntArray; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; + +/** + * A text which has already been measured. + * + * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc. + */ +public class PremeasuredText implements Spanned { + private static final char LINE_FEED = '\n'; + + // The original text. + private final @NonNull CharSequence mText; + + // The inclusive start offset of the measuring target. + private final @IntRange(from = 0) int mStart; + + // The exclusive end offset of the measuring target. + private final @IntRange(from = 0) int mEnd; + + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private final @NonNull TextDirectionHeuristic mTextDir; + + // The measured paragraph texts. + private final @NonNull MeasuredText[] mMeasuredTexts; + + // The sorted paragraph end offsets. + private final @NonNull int[] mParagraphBreakPoints; + + /** + * Build PremeasuredText from the text. + * + * @param text The text to be measured. + * @param paint The paint to be used for drawing. + * @param textDir The text direction. + * @return The measured text. + */ + public static @NonNull PremeasuredText build(@NonNull CharSequence text, + @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir) { + return PremeasuredText.build(text, paint, textDir, 0, text.length()); + } + + /** + * Build PremeasuredText from the specific range of the text.. + * + * @param text The text to be measured. + * @param paint The paint to be used for drawing. + * @param textDir The text direction. + * @param start The inclusive start offset of the text. + * @param end The exclusive start offset of the text. + * @return The measured text. + */ + public static @NonNull PremeasuredText build(@NonNull CharSequence text, + @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(paint); + Preconditions.checkNotNull(textDir); + Preconditions.checkArgumentInRange(start, 0, text.length(), "start"); + Preconditions.checkArgumentInRange(end, 0, text.length(), "end"); + + final IntArray paragraphEnds = new IntArray(); + final ArrayList 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[] getSpans(int start, int end, Class type) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpans(start, end, type); + } else { + return ArrayUtils.emptyArray(type); + } + } + + @Override + public int getSpanStart(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanStart(tag); + } else { + return -1; + } + } + + @Override + public int getSpanEnd(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanEnd(tag); + } else { + return -1; + } + } + + @Override + public int getSpanFlags(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanFlags(tag); + } else { + return 0; + } + } + + @Override + public int nextSpanTransition(int start, int limit, Class type) { + if (mText instanceof Spanned) { + return ((Spanned) mText).nextSpanTransition(start, limit, type); + } else { + return mText.length(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CharSequence overrides. + // + // Just proxy for underlying mText. + + @Override + public int length() { + return mText.length(); + } + + @Override + public char charAt(int index) { + // TODO: Should this be index + mStart ? + return mText.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + // TODO: return PremeasuredText. + // TODO: Should this be index + mStart, end + mStart ? + return mText.subSequence(start, end); + } + + @Override + public String toString() { + return mText.toString(); + } +} diff --git a/android/text/StaticLayout.java b/android/text/StaticLayout.java index c0fc44fd..d69b1190 100644 --- a/android/text/StaticLayout.java +++ b/android/text/StaticLayout.java @@ -21,10 +21,10 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Paint; +import android.text.AutoGrowArray.FloatArray; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.text.style.LineHeightSpan; -import android.text.style.MetricAffectingSpan; import android.text.style.TabStopSpan; import android.util.Log; import android.util.Pools.SynchronizedPool; @@ -48,6 +48,18 @@ import java.util.Arrays; * Canvas.drawText()} directly.

    */ public class StaticLayout extends Layout { + /* + * The break iteration is done in native code. The protocol for using the native code is as + * follows. + * + * First, call nInit to setup native line breaker object. Then, for each paragraph, do the + * following: + * + * - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native. + * - Run nComputeLineBreaks() to obtain line breaks for the paragraph. + * + * After all paragraphs, call finish() to release expensive buffers. + */ static final String TAG = "StaticLayout"; @@ -99,8 +111,6 @@ public class StaticLayout extends Layout { b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; - - b.mMeasuredText = MeasuredText.obtain(); return b; } @@ -111,8 +121,6 @@ public class StaticLayout extends Layout { private static void recycle(@NonNull Builder b) { b.mPaint = null; b.mText = null; - MeasuredText.recycle(b.mMeasuredText); - b.mMeasuredText = null; b.mLeftIndents = null; b.mRightIndents = null; b.mLeftPaddings = null; @@ -128,7 +136,6 @@ public class StaticLayout extends Layout { mRightIndents = null; mLeftPaddings = null; mRightPaddings = null; - mMeasuredText.finish(); } public Builder setText(CharSequence source) { @@ -444,12 +451,13 @@ public class StaticLayout extends Layout { private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); - // This will go away and be subsumed by native builder code - private MeasuredText mMeasuredText; - private static final SynchronizedPool sPool = new SynchronizedPool<>(3); } + /** + * @deprecated Use {@link Builder} instead. + */ + @Deprecated public StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, @@ -459,16 +467,9 @@ public class StaticLayout extends Layout { } /** - * @hide + * @deprecated Use {@link Builder} instead. */ - public StaticLayout(CharSequence source, TextPaint paint, - int width, Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad) { - this(source, 0, source.length(), paint, width, align, textDir, - spacingmult, spacingadd, includepad); - } - + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, @@ -479,17 +480,9 @@ public class StaticLayout extends Layout { } /** - * @hide + * @deprecated Use {@link Builder} instead. */ - public StaticLayout(CharSequence source, int bufstart, int bufend, - TextPaint paint, int outerwidth, - Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad) { - this(source, bufstart, bufend, paint, outerwidth, align, textDir, - spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE); -} - + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, @@ -503,7 +496,9 @@ public class StaticLayout extends Layout { /** * @hide + * @deprecated Use {@link Builder} instead. */ + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, @@ -561,6 +556,9 @@ public class StaticLayout extends Layout { Builder.recycle(b); } + /** + * Used by DynamicLayout. + */ /* package */ StaticLayout(@Nullable CharSequence text) { super(text, null, 0, null, 0, 0); @@ -606,8 +604,8 @@ public class StaticLayout extends Layout { /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { final CharSequence source = b.mText; - int bufStart = b.mStart; - int bufEnd = b.mEnd; + final int bufStart = b.mStart; + final int bufEnd = b.mEnd; TextPaint paint = b.mPaint; int outerWidth = b.mWidth; TextDirectionHeuristic textDir = b.mTextDir; @@ -618,11 +616,7 @@ public class StaticLayout extends Layout { TextUtils.TruncateAt ellipsize = b.mEllipsize; final boolean addLastLineSpacing = b.mAddLastLineLineSpacing; LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs - // store span end locations - int[] spanEndCache = new int[4]; - // store fontMetrics per span range - // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) - int[] fmCache = new int[4 * 4]; + FloatArray widths = new FloatArray(); mLineCount = 0; mEllipsized = false; @@ -634,12 +628,6 @@ public class StaticLayout extends Layout { Paint.FontMetricsInt fm = b.mFontMetricsInt; int[] chooseHtv = null; - MeasuredText measured = b.mMeasuredText; - - Spanned spanned = null; - if (source instanceof Spanned) - spanned = (Spanned) source; - final int[] indents; if (mLeftIndents != null || mRightIndents != null) { final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; @@ -662,15 +650,34 @@ public class StaticLayout extends Layout { b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, indents, mLeftPaddings, mRightPaddings); + PremeasuredText premeasured = null; + final Spanned spanned; + if (source instanceof PremeasuredText) { + premeasured = (PremeasuredText) source; + + final CharSequence original = premeasured.getText(); + spanned = (original instanceof Spanned) ? (Spanned) original : null; + + if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) { + // The buffer position has changed. Re-measure here. + premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd); + } else { + // We can use premeasured information. + + // Overwrite with the one when premeasured. + // TODO: Give an option for developer not to overwrite and measure again here? + textDir = premeasured.getTextDir(); + paint = premeasured.getPaint(); + } + } else { + premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd); + spanned = (source instanceof Spanned) ? (Spanned) source : null; + } + try { - int paraEnd; - for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { - paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); - if (paraEnd < 0) { - paraEnd = bufEnd; - } else { - paraEnd++; - } + for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) { + final int paraStart = premeasured.getParagraphStart(paraIndex); + final int paraEnd = premeasured.getParagraphEnd(paraIndex); int firstWidthLineCount = 1; int firstWidth = outerWidth; @@ -721,13 +728,6 @@ public class StaticLayout extends Layout { } } - measured.setPara(source, paraStart, paraEnd, textDir); - char[] chs = measured.mChars; - float[] widths = measured.mWidths; - byte[] chdirs = measured.mLevels; - int dir = measured.mDir; - boolean easy = measured.mEasy; - // tab stop locations int[] variableTabStops = null; if (spanned != null) { @@ -743,56 +743,23 @@ public class StaticLayout extends Layout { } } + final MeasuredText measured = premeasured.getMeasuredText(paraIndex); + final char[] chs = measured.getChars(); + final int[] spanEndCache = measured.getSpanEndCache().getRawArray(); + final int[] fmCache = measured.getFontMetrics().getRawArray(); + // TODO: Stop keeping duplicated width copy in native and Java. + widths.resize(chs.length); + // measurement has to be done before performing line breaking // but we don't want to recompute fontmetrics or span ranges the // second time, so we cache those and then use those stored values - int fmCacheCount = 0; - int spanEndCacheCount = 0; - for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { - if (fmCacheCount * 4 >= fmCache.length) { - int[] grow = new int[fmCacheCount * 4 * 2]; - System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4); - fmCache = grow; - } - - if (spanEndCacheCount >= spanEndCache.length) { - int[] grow = new int[spanEndCacheCount * 2]; - System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount); - spanEndCache = grow; - } - - if (spanned == null) { - spanEnd = paraEnd; - int spanLen = spanEnd - spanStart; - measured.addStyleRun(paint, spanLen, fm, nativePtr); - } else { - spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, - MetricAffectingSpan.class); - int spanLen = spanEnd - spanStart; - MetricAffectingSpan[] spans = - spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); - spans = TextUtils.removeEmptySpans(spans, spanned, - MetricAffectingSpan.class); - measured.addStyleRun(paint, spans, spanLen, fm, nativePtr); - } - - // the order of storage here (top, bottom, ascent, descent) has to match the - // code below where these values are retrieved - fmCache[fmCacheCount * 4 + 0] = fm.top; - fmCache[fmCacheCount * 4 + 1] = fm.bottom; - fmCache[fmCacheCount * 4 + 2] = fm.ascent; - fmCache[fmCacheCount * 4 + 3] = fm.descent; - fmCacheCount++; - - spanEndCache[spanEndCacheCount] = spanEnd; - spanEndCacheCount++; - } int breakCount = nComputeLineBreaks( nativePtr, // Inputs chs, + measured.getNativePtr(), paraEnd - paraStart, firstWidth, firstWidthLineCount, @@ -809,7 +776,7 @@ public class StaticLayout extends Layout { lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags, - widths); + widths.getRawArray()); final int[] breaks = lineBreaks.breaks; final float[] lineWidths = lineBreaks.widths; @@ -832,7 +799,7 @@ public class StaticLayout extends Layout { width += lineWidths[i]; } else { for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) { - width += widths[j]; + width += widths.get(j); } } flag |= flags[i] & TAB_MASK; @@ -896,10 +863,10 @@ public class StaticLayout extends Layout { v = out(source, here, endPos, ascent, descent, fmTop, fmBottom, v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, - flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd, - includepad, trackpad, addLastLineSpacing, chs, widths, paraStart, - ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint, - moreChars); + flags[breakIndex], needMultiply, measured, bufEnd, + includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(), + paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex], + paint, moreChars); if (endPos < spanEnd) { // preserve metrics for current span @@ -927,17 +894,16 @@ public class StaticLayout extends Layout { if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && mLineCount < mMaximumVisibleLineCount) { - measured.setPara(source, bufEnd, bufEnd, textDir); - + final MeasuredText measured = + MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null); paint.getFontMetricsInt(fm); - v = out(source, bufEnd, bufEnd, fm.ascent, fm.descent, fm.top, fm.bottom, v, spacingmult, spacingadd, null, null, fm, 0, - needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, + needMultiply, measured, bufEnd, includepad, trackpad, addLastLineSpacing, null, null, bufStart, ellipsize, ellipsizedWidth, 0, paint, false); @@ -952,8 +918,8 @@ public class StaticLayout extends Layout { private int out(final CharSequence text, final int start, final int end, int above, int below, int top, int bottom, int v, final float spacingmult, final float spacingadd, final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm, - final int flags, final boolean needMultiply, final byte[] chdirs, final int dir, - final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad, + final int flags, final boolean needMultiply, @NonNull final MeasuredText measured, + final int bufEnd, final boolean includePad, final boolean trackPad, final boolean addLastLineLineSpacing, final char[] chs, final float[] widths, final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth, final float textWidth, final TextPaint paint, final boolean moreChars) { @@ -961,6 +927,7 @@ public class StaticLayout extends Layout { final int off = j * mColumns; final int want = off + mColumns + TOP; int[] lines = mLines; + final int dir = measured.getParagraphDir(); if (want >= lines.length) { final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want)); @@ -986,17 +953,8 @@ public class StaticLayout extends Layout { // one bit for start field lines[off + TAB] |= flags & TAB_MASK; lines[off + HYPHEN] = flags; - lines[off + DIR] |= dir << DIR_SHIFT; - // easy means all chars < the first RTL, so no emoji, no nothing - // XXX a run with no text or all spaces is easy but might be an empty - // RTL paragraph. Make sure easy is false if this is the case. - if (easy) { - mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT; - } else { - mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, - start - widthStart, end - start); - } + mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); final boolean firstLine = (j == 0); final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); @@ -1473,33 +1431,6 @@ public class StaticLayout extends Layout { mMaxLineHeight : super.getHeight(); } - /** - * Measurement and break iteration is done in native code. The protocol for using - * the native code is as follows. - * - * First, call nInit to setup native line breaker object. Then, for each paragraph, do the - * following: - * - * - Call one of the following methods for each run within the paragraph depending on the type - * of run: - * + addStyleRun (a text run, to be measured in native code) - * + addReplacementRun (a replacement run, width is given) - * - * - Run nComputeLineBreaks() to obtain line breaks for the paragraph. - * - * After all paragraphs, call finish() to release expensive buffers. - */ - - /* package */ static void addStyleRun(long nativePtr, TextPaint paint, int start, int end, - boolean isRtl) { - nAddStyleRun(nativePtr, paint.getNativeInstance(), start, end, isRtl); - } - - /* package */ static void addReplacementRun(long nativePtr, TextPaint paint, int start, int end, - float width) { - nAddReplacementRun(nativePtr, paint.getNativeInstance(), start, end, width); - } - @FastNative private static native long nInit( @BreakStrategy int breakStrategy, @@ -1512,17 +1443,6 @@ public class StaticLayout extends Layout { @CriticalNative private static native void nFinish(long nativePtr); - @CriticalNative - private static native void nAddStyleRun( - /* non-zero */ long nativePtr, /* non-zero */ long nativePaint, - @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl); - - @CriticalNative - private static native void nAddReplacementRun( - /* non-zero */ long nativePtr, /* non-zero */ long nativePaint, - @IntRange(from = 0) int start, @IntRange(from = 0) int end, - @FloatRange(from = 0.0f) float width); - // populates LineBreaks and returns the number of breaks found // // the arrays inside the LineBreaks objects are passed in as well @@ -1535,6 +1455,7 @@ public class StaticLayout extends Layout { // Inputs @NonNull char[] text, + /* Non Zero */ long measuredTextPtr, @IntRange(from = 0) int length, @FloatRange(from = 0.0f) float firstWidth, @IntRange(from = 0) int firstWidthLineCount, diff --git a/android/text/StaticLayoutPerfTest.java b/android/text/StaticLayoutPerfTest.java index 57a61ec8..5653a039 100644 --- a/android/text/StaticLayoutPerfTest.java +++ b/android/text/StaticLayoutPerfTest.java @@ -16,12 +16,19 @@ package android.text; +import static android.text.TextDirectionHeuristics.LTR; + import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.text.Layout; +import android.text.style.TextAppearanceSpan; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,40 +40,35 @@ import java.util.Random; @RunWith(AndroidJUnit4.class) public class StaticLayoutPerfTest { - public StaticLayoutPerfTest() { - } + public StaticLayoutPerfTest() {} @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - private static final String FIXED_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing " - + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad " - + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " - + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse " - + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " - + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; - private static final int FIXED_TEXT_LENGTH = FIXED_TEXT.length(); + private static final int WORD_LENGTH = 9; // Random word has 9 characters. + private static final int WORDS_IN_LINE = 8; // Roughly, 8 words in a line. + private static final int PARA_LENGTH = 500; // Number of characters in a paragraph. - private static TextPaint PAINT = new TextPaint(); - private static final int TEXT_WIDTH = 20 * (int) PAINT.getTextSize(); + private static final boolean NO_STYLE_TEXT = false; + private static final boolean STYLE_TEXT = true; - @Test - public void testCreate() { - final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - while (state.keepRunning()) { - StaticLayout.Builder.obtain(FIXED_TEXT, 0, FIXED_TEXT_LENGTH, PAINT, TEXT_WIDTH) - .build(); - } - } + private final Random mRandom = new Random(31415926535L); private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int ALPHABET_LENGTH = ALPHABET.length(); - private static final int PARA_LENGTH = 500; + private static final ColorStateList TEXT_COLOR = ColorStateList.valueOf(0x00000000); + private static final String[] FAMILIES = { "sans-serif", "serif", "monospace" }; + private static final int[] STYLES = { + Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC + }; + private final char[] mBuffer = new char[PARA_LENGTH]; - private final Random mRandom = new Random(31415926535L); - private CharSequence generateRandomParagraph(int wordLen) { + private static TextPaint PAINT = new TextPaint(); + private static final int TEXT_WIDTH = WORDS_IN_LINE * WORD_LENGTH * (int) PAINT.getTextSize(); + + private CharSequence generateRandomParagraph(int wordLen, boolean applyRandomStyle) { for (int i = 0; i < PARA_LENGTH; i++) { if (i % (wordLen + 1) == wordLen) { mBuffer[i] = ' '; @@ -74,29 +76,192 @@ public class StaticLayoutPerfTest { mBuffer[i] = ALPHABET.charAt(mRandom.nextInt(ALPHABET_LENGTH)); } } - return CharBuffer.wrap(mBuffer); + + CharSequence cs = CharBuffer.wrap(mBuffer); + if (!applyRandomStyle) { + return cs; + } + + SpannableStringBuilder ssb = new SpannableStringBuilder(cs); + for (int i = 0; i < ssb.length(); i += WORD_LENGTH) { + final int spanStart = i; + final int spanEnd = (i + WORD_LENGTH) > ssb.length() ? ssb.length() : i + WORD_LENGTH; + + final TextAppearanceSpan span = new TextAppearanceSpan( + FAMILIES[mRandom.nextInt(FAMILIES.length)], + STYLES[mRandom.nextInt(STYLES.length)], + 24 + mRandom.nextInt(32), // text size. min 24 max 56 + TEXT_COLOR, TEXT_COLOR); + + ssb.setSpan(span, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + return ssb; + } + + @Test + public void testCreate_FixedText_NoStyle_Greedy_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + while (state.keepRunning()) { + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_RandomText_NoStyled_Greedy_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_RandomText_NoStyled_Greedy_Hyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_RandomText_NoStyled_Balanced_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .build(); + } } - // This tries to simulate the case where the cache hit rate is low, and most of the text is - // new text. @Test - public void testCreateRandom() { + public void testCreate_RandomText_NoStyled_Balanced_Hyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - final CharSequence text = generateRandomParagraph(9); + state.pauseTiming(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + state.resumeTiming(); + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .build(); } } @Test - public void testCreateRandom_breakBalanced() { + public void testCreate_RandomText_Styled_Greedy_NoHyphenation() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - final CharSequence text = generateRandomParagraph(9); + state.pauseTiming(); + final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT); + state.resumeTiming(); + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_MeasuredText_NoStyled_Greedy_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final PremeasuredText text = PremeasuredText.build( + generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_MeasuredText_NoStyled_Greedy_Hyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final PremeasuredText text = PremeasuredText.build( + generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } + + @Test + public void testCreate_MeasuredText_NoStyled_Balanced_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final PremeasuredText text = PremeasuredText.build( + generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .build(); + } + } + + @Test + public void testCreate_MeasuredText_NoStyled_Balanced_Hyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final PremeasuredText text = PremeasuredText.build( + generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) .build(); } } + + @Test + public void testCreate_MeasuredText_Styled_Greedy_NoHyphenation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + final PremeasuredText text = PremeasuredText.build( + generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR); + state.resumeTiming(); + + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + .build(); + } + } } diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java index ca8743c7..d524954e 100644 --- a/android/text/StaticLayout_Delegate.java +++ b/android/text/StaticLayout_Delegate.java @@ -5,10 +5,6 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.annotation.NonNull; import android.annotation.Nullable; -import android.graphics.BidiRenderer; -import android.graphics.Paint; -import android.graphics.Paint_Delegate; -import android.graphics.RectF; import android.icu.text.BreakIterator; import android.text.Layout.BreakStrategy; import android.text.Layout.HyphenationFrequency; @@ -17,7 +13,6 @@ import android.text.StaticLayout.LineBreaks; import java.text.CharacterIterator; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import javax.swing.text.Segment; @@ -58,32 +53,13 @@ public class StaticLayout_Delegate { sBuilderManager.removeJavaReferenceFor(nativePtr); } - @LayoutlibDelegate - /*package*/ static void nAddStyleRun(long nativeBuilder, long nativePaint, int start, - int end, boolean isRtl) { - Builder builder = sBuilderManager.getDelegate(nativeBuilder); - if (builder == null) { - return; - } - builder.mRuns.add(new StyleRun(nativePaint, start, end, isRtl)); - } - - @LayoutlibDelegate - /*package*/ static void nAddReplacementRun(long nativeBuilder, long nativePaint, int start, - int end, float width) { - Builder builder = sBuilderManager.getDelegate(nativeBuilder); - if (builder == null) { - return; - } - builder.mRuns.add(new ReplacementRun(start, end, width)); - } - @LayoutlibDelegate /*package*/ static int nComputeLineBreaks( /* non zero */ long nativePtr, // Inputs @NonNull char[] text, + long measuredTextPtr, int length, float firstWidth, int firstWidthLineCount, @@ -111,9 +87,7 @@ public class StaticLayout_Delegate { builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth); builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop); - for (Run run: builder.mRuns) { - run.addTo(builder); - } + MeasuredText_Delegate.computeRuns(measuredTextPtr, builder); // compute all possible breakpoints. BreakIterator it = BreakIterator.getLineInstance(); @@ -192,29 +166,20 @@ public class StaticLayout_Delegate { return primitives; } - private static float measureText(long nativePaint, char []text, int index, int count, - float[] widths, int bidiFlags) { - Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint); - RectF bounds = new BidiRenderer(null, paint, text) - .renderText(index, index + count, bidiFlags, widths, 0, false); - return bounds.right - bounds.left; - } - // TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker. /** * Java representation of the native Builder class. */ - private static class Builder { + public static class Builder { char[] mText; float[] mWidths; private LineBreaker mLineBreaker; private int mBreakStrategy; private LineWidth mLineWidth; private TabStops mTabStopCalculator; - private ArrayList mRuns = new ArrayList<>(); } - private abstract static class Run { + public abstract static class Run { int mStart; int mEnd; @@ -225,37 +190,4 @@ public class StaticLayout_Delegate { abstract void addTo(Builder builder); } - - private static class StyleRun extends Run { - private long mNativePaint; - private boolean mIsRtl; - - private StyleRun(long nativePaint, int start, int end, boolean isRtl) { - super(start, end); - mNativePaint = nativePaint; - mIsRtl = isRtl; - } - - @Override - void addTo(Builder builder) { - int bidiFlags = mIsRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR; - measureText(mNativePaint, builder.mText, mStart, mEnd - mStart, builder.mWidths, - bidiFlags); - } - } - - private static class ReplacementRun extends Run { - private final float mWidth; - - private ReplacementRun(int start, int end, float width) { - super(start, end); - mWidth = width; - } - - @Override - void addTo(Builder builder) { - builder.mWidths[mStart] = mWidth; - Arrays.fill(builder.mWidths, mStart + 1, mEnd, 0.0f); - } - } } diff --git a/android/text/TextUtils.java b/android/text/TextUtils.java index cbdaa69b..9c9fbf23 100644 --- a/android/text/TextUtils.java +++ b/android/text/TextUtils.java @@ -42,7 +42,6 @@ import android.text.style.EasyEditSpan; import android.text.style.ForegroundColorSpan; import android.text.style.LeadingMarginSpan; import android.text.style.LocaleSpan; -import android.text.style.MetricAffectingSpan; import android.text.style.ParagraphStyle; import android.text.style.QuoteSpan; import android.text.style.RelativeSizeSpan; @@ -1251,10 +1250,11 @@ public class TextUtils { @NonNull String ellipsis) { final int len = text.length(); - final MeasuredText mt = MeasuredText.obtain(); + MeasuredText mt = null; MeasuredText resultMt = null; try { - float width = setPara(mt, paint, text, 0, text.length(), textDir); + mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); + float width = mt.getWholeWidth(); if (width <= avail) { if (callback != null) { @@ -1263,7 +1263,6 @@ public class TextUtils { return text; } - resultMt = MeasuredText.obtain(); // First estimate of effective width of ellipsis. float ellipsisWidth = paint.measureText(ellipsis); int numberOfTries = 0; @@ -1290,7 +1289,7 @@ public class TextUtils { } } - final char[] buf = mt.mChars; + final char[] buf = mt.getChars(); final Spanned sp = text instanceof Spanned ? (Spanned) text : null; final int removed = end - start; @@ -1333,7 +1332,9 @@ public class TextUtils { if (remaining == 0) { // All text is gone. textFits = true; } else { - width = setPara(resultMt, paint, result, 0, result.length(), textDir); + resultMt = MeasuredText.buildForMeasurement( + paint, result, 0, result.length(), textDir, resultMt); + width = resultMt.getWholeWidth(); if (width <= avail) { textFits = true; } else { @@ -1357,9 +1358,11 @@ public class TextUtils { } return result; } finally { - MeasuredText.recycle(mt); + if (mt != null) { + mt.recycle(); + } if (resultMt != null) { - MeasuredText.recycle(resultMt); + resultMt.recycle(); } } } @@ -1476,15 +1479,17 @@ public class TextUtils { public static CharSequence commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir) { - MeasuredText mt = MeasuredText.obtain(); + MeasuredText mt = null; + MeasuredText tempMt = null; try { int len = text.length(); - float width = setPara(mt, p, text, 0, len, textDir); + mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt); + final float width = mt.getWholeWidth(); if (width <= avail) { return text; } - char[] buf = mt.mChars; + char[] buf = mt.getChars(); int commaCount = 0; for (int i = 0; i < len; i++) { @@ -1500,9 +1505,8 @@ public class TextUtils { int w = 0; int count = 0; - float[] widths = mt.mWidths; + float[] widths = mt.getWidths().getRawArray(); - MeasuredText tempMt = MeasuredText.obtain(); for (int i = 0; i < len; i++) { w += widths[i]; @@ -1519,8 +1523,9 @@ public class TextUtils { } // XXX this is probably ok, but need to look at it more - tempMt.setPara(format, 0, format.length(), textDir); - float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); + tempMt = MeasuredText.buildForMeasurement( + p, format, 0, format.length(), textDir, tempMt); + float moreWid = tempMt.getWholeWidth(); if (w + moreWid <= avail) { ok = i + 1; @@ -1528,40 +1533,18 @@ public class TextUtils { } } } - MeasuredText.recycle(tempMt); SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; } finally { - MeasuredText.recycle(mt); - } - } - - private static float setPara(MeasuredText mt, TextPaint paint, - CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - - mt.setPara(text, start, end, textDir); - - float width; - Spanned sp = text instanceof Spanned ? (Spanned) text : null; - int len = end - start; - if (sp == null) { - width = mt.addStyleRun(paint, len, null); - } else { - width = 0; - int spanEnd; - for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { - spanEnd = sp.nextSpanTransition(spanStart, len, - MetricAffectingSpan.class); - MetricAffectingSpan[] spans = sp.getSpans( - spanStart, spanEnd, MetricAffectingSpan.class); - spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); - width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); + if (mt != null) { + mt.recycle(); + } + if (tempMt != null) { + tempMt.recycle(); } } - - return width; } // Returns true if the character's presence could affect RTL layout. diff --git a/android/transition/Visibility.java b/android/transition/Visibility.java index 5b851caa..f0838a16 100644 --- a/android/transition/Visibility.java +++ b/android/transition/Visibility.java @@ -52,7 +52,10 @@ public abstract class Visibility extends Transition { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag=true, value={MODE_IN, MODE_OUT}) + @IntDef(flag = true, prefix = { "MODE_" }, value = { + MODE_IN, + MODE_OUT + }) @interface VisibilityMode {} /** diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java index 29baea17..bfb51309 100644 --- a/android/util/FeatureFlagUtils.java +++ b/android/util/FeatureFlagUtils.java @@ -21,6 +21,7 @@ import android.os.SystemProperties; import android.provider.Settings; import android.text.TextUtils; +import java.util.HashMap; import java.util.Map; /** @@ -33,6 +34,19 @@ public class FeatureFlagUtils { public static final String FFLAG_PREFIX = "sys.fflag."; public static final String FFLAG_OVERRIDE_PREFIX = FFLAG_PREFIX + "override."; + private static final Map DEFAULT_FLAGS; + static { + DEFAULT_FLAGS = new HashMap<>(); + DEFAULT_FLAGS.put("device_info_v2", "true"); + DEFAULT_FLAGS.put("new_settings_suggestion", "true"); + DEFAULT_FLAGS.put("settings_search_v2", "true"); + DEFAULT_FLAGS.put("settings_app_info_v2", "false"); + DEFAULT_FLAGS.put("settings_connected_device_v2", "true"); + DEFAULT_FLAGS.put("settings_battery_v2", "false"); + DEFAULT_FLAGS.put("settings_battery_display_app_list", "false"); + DEFAULT_FLAGS.put("settings_security_settings_v2", "false"); + } + /** * Whether or not a flag is enabled. * @@ -41,7 +55,7 @@ public class FeatureFlagUtils { */ public static boolean isEnabled(Context context, String feature) { // Override precedence: - // Settings.Global -> sys.fflag.override.* -> sys.fflag.* + // Settings.Global -> sys.fflag.override.* -> static list // Step 1: check if feature flag is set in Settings.Global. String value; @@ -57,8 +71,8 @@ public class FeatureFlagUtils { if (!TextUtils.isEmpty(value)) { return Boolean.parseBoolean(value); } - // Step 3: check if feature flag has any default value. Flag name: sys.fflag. - value = SystemProperties.get(FFLAG_PREFIX + feature); + // Step 3: check if feature flag has any default value. + value = getAllFeatureFlags().get(feature); return Boolean.parseBoolean(value); } @@ -73,6 +87,6 @@ public class FeatureFlagUtils { * Returns all feature flags in their raw form. */ public static Map getAllFeatureFlags() { - return null; + return DEFAULT_FLAGS; } } diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java index d50395e2..0a00794a 100644 --- a/android/util/KeyValueListParser.java +++ b/android/util/KeyValueListParser.java @@ -148,6 +148,34 @@ public class KeyValueListParser { return def; } + /** + * Get the value for key as an integer array. + * + * The value should be encoded as "0:1:2:3:4" + * + * @param key The key to lookup. + * @param def The value to return if the key was not found. + * @return the int[] value associated with the key. + */ + public int[] getIntArray(String key, int[] def) { + String value = mValues.get(key); + if (value != null) { + try { + String[] parts = value.split(":"); + if (parts.length > 0) { + int[] ret = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + ret[i] = Integer.parseInt(parts[i]); + } + return ret; + } + } catch (NumberFormatException e) { + // fallthrough + } + } + return def; + } + /** * @return the number of keys. */ diff --git a/android/util/Log.java b/android/util/Log.java index 02998653..b94e48b3 100644 --- a/android/util/Log.java +++ b/android/util/Log.java @@ -16,45 +16,12 @@ package android.util; -import android.os.DeadSystemException; - -import com.android.internal.os.RuntimeInit; -import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.LineBreakBufferedWriter; - import java.io.PrintWriter; import java.io.StringWriter; -import java.io.Writer; import java.net.UnknownHostException; /** - * API for sending log output. - * - *

    Generally, you should use the {@link #v Log.v()}, {@link #d Log.d()}, - * {@link #i Log.i()}, {@link #w Log.w()}, and {@link #e Log.e()} methods to write logs. - * You can then view the logs in logcat. - * - *

    The order in terms of verbosity, from least to most is - * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled - * into an application except during development. Debug logs are compiled - * in but stripped at runtime. Error, warning and info logs are always kept. - * - *

    Tip: A good convention is to declare a TAG constant - * in your class: - * - *

    private static final String TAG = "MyActivity";
    - * - * and use that in subsequent calls to the log methods. - *

    - * - *

    Tip: Don't forget that when you make a call like - *

    Log.v(TAG, "index=" + i);
    - * that when you're building the string to pass into Log.d, the compiler uses a - * StringBuilder and at least three allocations occur: the StringBuilder - * itself, the buffer, and the String object. Realistically, there is also - * another buffer allocation and copy, and even more pressure on the gc. - * That means that if your log message is filtered out, you might be doing - * significant work and incurring significant overhead. + * Mock Log implementation for testing on non android host. */ public final class Log { @@ -88,29 +55,6 @@ public final class Log { */ public static final int ASSERT = 7; - /** - * Exception class used to capture a stack trace in {@link #wtf}. - * @hide - */ - public static class TerribleFailure extends Exception { - TerribleFailure(String msg, Throwable cause) { super(msg, cause); } - } - - /** - * Interface to handle terrible failures from {@link #wtf}. - * - * @hide - */ - public interface TerribleFailureHandler { - void onTerribleFailure(String tag, TerribleFailure what, boolean system); - } - - private static TerribleFailureHandler sWtfHandler = new TerribleFailureHandler() { - public void onTerribleFailure(String tag, TerribleFailure what, boolean system) { - RuntimeInit.wtf(tag, what, system); - } - }; - private Log() { } @@ -121,7 +65,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int v(String tag, String msg) { - return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); + return println(LOG_ID_MAIN, VERBOSE, tag, msg); } /** @@ -132,7 +76,7 @@ public final class Log { * @param tr An exception to log */ public static int v(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr); + return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -142,7 +86,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int d(String tag, String msg) { - return println_native(LOG_ID_MAIN, DEBUG, tag, msg); + return println(LOG_ID_MAIN, DEBUG, tag, msg); } /** @@ -153,7 +97,7 @@ public final class Log { * @param tr An exception to log */ public static int d(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr); + return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -163,7 +107,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int i(String tag, String msg) { - return println_native(LOG_ID_MAIN, INFO, tag, msg); + return println(LOG_ID_MAIN, INFO, tag, msg); } /** @@ -174,7 +118,7 @@ public final class Log { * @param tr An exception to log */ public static int i(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, INFO, tag, msg, tr); + return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -184,7 +128,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int w(String tag, String msg) { - return println_native(LOG_ID_MAIN, WARN, tag, msg); + return println(LOG_ID_MAIN, WARN, tag, msg); } /** @@ -195,31 +139,9 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, msg, tr); + return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr)); } - /** - * Checks to see whether or not a log for the specified tag is loggable at the specified level. - * - * The default level of any tag is set to INFO. This means that any level above and including - * INFO will be logged. Before you make any calls to a logging method you should check to see - * if your tag should be logged. You can change the default level by setting a system property: - * 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' - * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will - * turn off all logging for your tag. You can also create a local.prop file that with the - * following in it: - * 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' - * and place that in /data/local.prop. - * - * @param tag The tag to check. - * @param level The level to check. - * @return Whether or not that this is allowed to be logged. - * @throws IllegalArgumentException is thrown if the tag.length() > 23 - * for Nougat (7.0) releases (API <= 23) and prior, there is no - * tag limit of concern after this API level. - */ - public static native boolean isLoggable(String tag, int level); - /* * Send a {@link #WARN} log message and log the exception. * @param tag Used to identify the source of a log message. It usually identifies @@ -227,7 +149,7 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, "", tr); + return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr)); } /** @@ -237,7 +159,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int e(String tag, String msg) { - return println_native(LOG_ID_MAIN, ERROR, tag, msg); + return println(LOG_ID_MAIN, ERROR, tag, msg); } /** @@ -248,82 +170,7 @@ public final class Log { * @param tr An exception to log */ public static int e(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr); - } - - /** - * What a Terrible Failure: Report a condition that should never happen. - * The error will always be logged at level ASSERT with the call stack. - * Depending on system configuration, a report may be added to the - * {@link android.os.DropBoxManager} and/or the process may be terminated - * immediately with an error dialog. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - */ - public static int wtf(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, false, false); - } - - /** - * Like {@link #wtf(String, String)}, but also writes to the log the full - * call stack. - * @hide - */ - public static int wtfStack(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, true, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, String)}, with an exception to log. - * @param tag Used to identify the source of a log message. - * @param tr An exception to log. - */ - public static int wtf(String tag, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, Throwable)}, with a message as well. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - * @param tr An exception to log. May be null. - */ - public static int wtf(String tag, String msg, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, msg, tr, false, false); - } - - static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack, - boolean system) { - TerribleFailure what = new TerribleFailure(msg, tr); - // Only mark this as ERROR, do not use ASSERT since that should be - // reserved for cases where the system is guaranteed to abort. - // The onTerribleFailure call does not always cause a crash. - int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr); - sWtfHandler.onTerribleFailure(tag, what, system); - return bytes; - } - - static void wtfQuiet(int logId, String tag, String msg, boolean system) { - TerribleFailure what = new TerribleFailure(msg, null); - sWtfHandler.onTerribleFailure(tag, what, system); - } - - /** - * Sets the terrible failure handler, for testing. - * - * @return the old handler - * - * @hide - */ - public static TerribleFailureHandler setWtfHandler(TerribleFailureHandler handler) { - if (handler == null) { - throw new NullPointerException("handler == null"); - } - TerribleFailureHandler oldHandler = sWtfHandler; - sWtfHandler = handler; - return oldHandler; + return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -346,7 +193,7 @@ public final class Log { } StringWriter sw = new StringWriter(); - PrintWriter pw = new FastPrintWriter(sw, false, 256); + PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); pw.flush(); return sw.toString(); @@ -361,7 +208,7 @@ public final class Log { * @return The number of bytes written. */ public static int println(int priority, String tag, String msg) { - return println_native(LOG_ID_MAIN, priority, tag, msg); + return println(LOG_ID_MAIN, priority, tag, msg); } /** @hide */ public static final int LOG_ID_MAIN = 0; @@ -370,115 +217,9 @@ public final class Log { /** @hide */ public static final int LOG_ID_SYSTEM = 3; /** @hide */ public static final int LOG_ID_CRASH = 4; - /** @hide */ public static native int println_native(int bufID, - int priority, String tag, String msg); - - /** - * Return the maximum payload the log daemon accepts without truncation. - * @return LOGGER_ENTRY_MAX_PAYLOAD. - */ - private static native int logger_entry_max_payload_native(); - - /** - * Helper function for long messages. Uses the LineBreakBufferedWriter to break - * up long messages and stacktraces along newlines, but tries to write in large - * chunks. This is to avoid truncation. - * @hide - */ - public static int printlns(int bufID, int priority, String tag, String msg, - Throwable tr) { - ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag); - // Acceptable buffer size. Get the native buffer size, subtract two zero terminators, - // and the length of the tag. - // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It - // is too expensive to compute that ahead of time. - int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. - - 2 // Two terminators. - - (tag != null ? tag.length() : 0) // Tag length. - - 32; // Some slack. - // At least assume you can print *some* characters (tag is not too large). - bufferSize = Math.max(bufferSize, 100); - - LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize); - - lbbw.println(msg); - - if (tr != null) { - // This is to reduce the amount of log spew that apps do in the non-error - // condition of the network being unavailable. - Throwable t = tr; - while (t != null) { - if (t instanceof UnknownHostException) { - break; - } - if (t instanceof DeadSystemException) { - lbbw.println("DeadSystemException: The system died; " - + "earlier logs will point to the root cause"); - break; - } - t = t.getCause(); - } - if (t == null) { - tr.printStackTrace(lbbw); - } - } - - lbbw.flush(); - - return logWriter.getWritten(); - } - - /** - * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid - * a JNI call during logging. - */ - static class PreloadHolder { - public final static int LOGGER_ENTRY_MAX_PAYLOAD = - logger_entry_max_payload_native(); - } - - /** - * Helper class to write to the logcat. Different from LogWriter, this writes - * the whole given buffer and does not break along newlines. - */ - private static class ImmediateLogWriter extends Writer { - - private int bufID; - private int priority; - private String tag; - - private int written = 0; - - /** - * Create a writer that immediately writes to the log, using the given - * parameters. - */ - public ImmediateLogWriter(int bufID, int priority, String tag) { - this.bufID = bufID; - this.priority = priority; - this.tag = tag; - } - - public int getWritten() { - return written; - } - - @Override - public void write(char[] cbuf, int off, int len) { - // Note: using String here has a bit of overhead as a Java object is created, - // but using the char[] directly is not easier, as it needs to be translated - // to a C char[] for logging. - written += println_native(bufID, priority, tag, new String(cbuf, off, len)); - } - - @Override - public void flush() { - // Ignored. - } - - @Override - public void close() { - // Ignored. - } + /** @hide */ @SuppressWarnings("unused") + public static int println(int bufID, + int priority, String tag, String msg) { + return 0; } } diff --git a/android/util/LruCache.java b/android/util/LruCache.java index 40154880..52086065 100644 --- a/android/util/LruCache.java +++ b/android/util/LruCache.java @@ -20,6 +20,10 @@ import java.util.LinkedHashMap; import java.util.Map; /** + * BEGIN LAYOUTLIB CHANGE + * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. + * END LAYOUTLIB CHANGE + * * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may @@ -87,8 +91,9 @@ public class LruCache { /** * Sets the size of the cache. - * * @param maxSize The new maximum size. + * + * @hide */ public void resize(int maxSize) { if (maxSize <= 0) { @@ -185,13 +190,10 @@ public class LruCache { } /** - * Remove the eldest entries until the total of remaining entries is at or - * below the requested size. - * * @param maxSize the maximum size of the cache before returning. May be -1 - * to evict even 0-sized elements. + * to evict even 0-sized elements. */ - public void trimToSize(int maxSize) { + private void trimToSize(int maxSize) { while (true) { K key; V value; @@ -205,7 +207,16 @@ public class LruCache { break; } - Map.Entry toEvict = map.eldest(); + // BEGIN LAYOUTLIB CHANGE + // get the last item in the linked list. + // This is not efficient, the goal here is to minimize the changes + // compared to the platform version. + Map.Entry toEvict = null; + for (Map.Entry entry : map.entrySet()) { + toEvict = entry; + } + // END LAYOUTLIB CHANGE + if (toEvict == null) { break; } diff --git a/android/util/Pools.java b/android/util/Pools.java index 70581be8..f0b7e01d 100644 --- a/android/util/Pools.java +++ b/android/util/Pools.java @@ -130,22 +130,29 @@ public final class Pools { } /** - * Synchronized) pool of objects. + * Synchronized pool of objects. * * @param The pooled type. */ public static class SynchronizedPool extends SimplePool { - private final Object mLock = new Object(); + private final Object mLock; /** * Creates a new instance. * * @param maxPoolSize The max pool size. + * @param lock an optional custom object to synchronize on * * @throws IllegalArgumentException If the max pool size is less than zero. */ - public SynchronizedPool(int maxPoolSize) { + public SynchronizedPool(int maxPoolSize, Object lock) { super(maxPoolSize); + mLock = lock; + } + + /** @see #SynchronizedPool(int, Object) */ + public SynchronizedPool(int maxPoolSize) { + this(maxPoolSize, new Object()); } @Override diff --git a/android/util/SparseBooleanArray.java b/android/util/SparseBooleanArray.java index 4f76463a..68d347c9 100644 --- a/android/util/SparseBooleanArray.java +++ b/android/util/SparseBooleanArray.java @@ -117,7 +117,11 @@ public class SparseBooleanArray implements Cloneable { } } - /** @hide */ + /** + * Removes the mapping at the specified index. + *

    + * For indices outside of the range {@code 0...size()-1}, the behavior is undefined. + */ public void removeAt(int index) { System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); diff --git a/android/util/StatsLog.java b/android/util/StatsLog.java new file mode 100644 index 00000000..48053183 --- /dev/null +++ b/android/util/StatsLog.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * StatsLog provides an API for developers to send events to statsd. The events can be used to + * define custom metrics inside statsd. We will rate-limit how often the calls can be made inside + * statsd. + */ +public final class StatsLog extends StatsLogInternal { + private static final String TAG = "StatsManager"; + + private StatsLog() {} + + /** + * Logs a start event. + * + * @param label developer-chosen label that is from [0, 16). + * @return True if the log request was sent to statsd. + */ + public static boolean logStart(int label) { + if (label >= 0 && label < 16) { + StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__START); + return true; + } + return false; + } + + /** + * Logs a stop event. + * + * @param label developer-chosen label that is from [0, 16). + * @return True if the log request was sent to statsd. + */ + public static boolean logStop(int label) { + if (label >= 0 && label < 16) { + StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__STOP); + return true; + } + return false; + } + + /** + * Logs an event that does not represent a start or stop boundary. + * + * @param label developer-chosen label that is from [0, 16). + * @return True if the log request was sent to statsd. + */ + public static boolean logEvent(int label) { + if (label >= 0 && label < 16) { + StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__UNSPECIFIED); + return true; + } + return false; + } +} diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java index 2bcd863c..26a3c361 100644 --- a/android/util/StatsManager.java +++ b/android/util/StatsManager.java @@ -93,10 +93,11 @@ public final class StatsManager { } /** - * Clients can request data with a binder call. + * Clients can request data with a binder call. This getter is destructive and also clears + * the retrieved metrics from statsd memory. * * @param configKey Configuration key to retrieve data from. - * @return Serialized ConfigMetricsReport proto. Returns null on failure. + * @return Serialized ConfigMetricsReportList proto. Returns null on failure. */ @RequiresPermission(Manifest.permission.DUMP) public byte[] getData(String configKey) { @@ -115,6 +116,30 @@ public final class StatsManager { } } + /** + * Clients can request metadata for statsd. Will contain stats across all configurations but not + * the actual metrics themselves (metrics must be collected via {@link #getData(String)}. + * This getter is not destructive and will not reset any metrics/counters. + * + * @return Serialized StatsdStatsReport proto. Returns null on failure. + */ + @RequiresPermission(Manifest.permission.DUMP) + public byte[] getMetadata() { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + Slog.d(TAG, "Failed to find statsd when getting metadata"); + return null; + } + return service.getMetadata(); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connecto statsd when getting metadata"); + return null; + } + } + } + private class StatsdDeathRecipient implements IBinder.DeathRecipient { @Override public void binderDied() { diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java index 18081234..530937e7 100644 --- a/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -16,6 +16,21 @@ package android.util.apk; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; + import android.util.ArrayMap; import android.util.Pair; @@ -23,56 +38,47 @@ import java.io.ByteArrayInputStream; import java.io.FileDescriptor; import java.io.IOException; import java.io.RandomAccessFile; -import java.math.BigInteger; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Principal; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; -import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; -import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.List; import java.util.Map; -import java.util.Set; /** * APK Signature Scheme v2 verifier. * + *

    APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single + * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + * @see APK Signature Scheme v2 + * * @hide for internal use only. */ public class ApkSignatureSchemeV2Verifier { /** - * {@code .SF} file header section attribute indicating that the APK is signed not just with - * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute - * facilitates v2 signature stripping detection. - * - *

    The attribute contains a comma-separated set of signature scheme IDs. + * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing. */ - public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed"; public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2; + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + /** * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature. * @@ -97,8 +103,27 @@ public class ApkSignatureSchemeV2Verifier { */ public static X509Certificate[][] verify(String apkFile) throws SignatureNotFoundException, SecurityException, IOException { + return verify(apkFile, true); + } + + /** + * Returns the certificates associated with each signer for the given APK without verification. + * This method is dangerous and should not be used, unless the caller is absolutely certain the + * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v2 + * Block while gathering signer information. The APK contents are not verified. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + public static X509Certificate[][] plsCertsNoVerifyOnlyCerts(String apkFile) + throws SignatureNotFoundException, SecurityException, IOException { + return verify(apkFile, false); + } + + private static X509Certificate[][] verify(String apkFile, boolean verifyIntegrity) + throws SignatureNotFoundException, SecurityException, IOException { try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { - return verify(apk); + return verify(apk, verifyIntegrity); } } @@ -111,10 +136,10 @@ public class ApkSignatureSchemeV2Verifier { * verify. * @throws IOException if an I/O error occurs while reading the APK file. */ - private static X509Certificate[][] verify(RandomAccessFile apk) + private static X509Certificate[][] verify(RandomAccessFile apk, boolean verifyIntegrity) throws SignatureNotFoundException, SecurityException, IOException { SignatureInfo signatureInfo = findSignature(apk); - return verify(apk.getFD(), signatureInfo); + return verify(apk.getFD(), signatureInfo, verifyIntegrity); } /** @@ -126,30 +151,7 @@ public class ApkSignatureSchemeV2Verifier { */ private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { - // Find the ZIP End of Central Directory (EoCD) record. - Pair eocdAndOffsetInFile = getEocd(apk); - ByteBuffer eocd = eocdAndOffsetInFile.first; - long eocdOffset = eocdAndOffsetInFile.second; - if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { - throw new SignatureNotFoundException("ZIP64 APK not supported"); - } - - // Find the APK Signing Block. The block immediately precedes the Central Directory. - long centralDirOffset = getCentralDirOffset(eocd, eocdOffset); - Pair apkSigningBlockAndOffsetInFile = - findApkSigningBlock(apk, centralDirOffset); - ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first; - long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second; - - // Find the APK Signature Scheme v2 Block inside the APK Signing Block. - ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock); - - return new SignatureInfo( - apkSignatureSchemeV2Block, - apkSigningBlockOffset, - centralDirOffset, - eocdOffset, - eocd); + return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); } /** @@ -161,7 +163,8 @@ public class ApkSignatureSchemeV2Verifier { */ private static X509Certificate[][] verify( FileDescriptor apkFileDescriptor, - SignatureInfo signatureInfo) throws SecurityException { + SignatureInfo signatureInfo, + boolean doVerifyIntegrity) throws SecurityException { int signerCount = 0; Map contentDigests = new ArrayMap<>(); List signerCerts = new ArrayList<>(); @@ -198,13 +201,15 @@ public class ApkSignatureSchemeV2Verifier { throw new SecurityException("No content digests found"); } - verifyIntegrity( - contentDigests, - apkFileDescriptor, - signatureInfo.apkSigningBlockOffset, - signatureInfo.centralDirOffset, - signatureInfo.eocdOffset, - signatureInfo.eocd); + if (doVerifyIntegrity) { + ApkSigningBlockUtils.verifyIntegrity( + contentDigests, + apkFileDescriptor, + signatureInfo.apkSigningBlockOffset, + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset, + signatureInfo.eocd); + } return signerCerts.toArray(new X509Certificate[signerCerts.size()][]); } @@ -328,7 +333,8 @@ public class ApkSignatureSchemeV2Verifier { } catch (CertificateException e) { throw new SecurityException("Failed to decode certificate #" + certificateCount, e); } - certificate = new VerbatimX509Certificate(certificate, encodedCert); + certificate = new VerbatimX509Certificate( + certificate, encodedCert); certs.add(certificate); } @@ -342,235 +348,44 @@ public class ApkSignatureSchemeV2Verifier { "Public key mismatch between certificate and signature record"); } - return certs.toArray(new X509Certificate[certs.size()]); - } - - private static void verifyIntegrity( - Map expectedDigests, - FileDescriptor apkFileDescriptor, - long apkSigningBlockOffset, - long centralDirOffset, - long eocdOffset, - ByteBuffer eocdBuf) throws SecurityException { - - if (expectedDigests.isEmpty()) { - throw new SecurityException("No digests provided"); - } - - // We need to verify the integrity of the following three sections of the file: - // 1. Everything up to the start of the APK Signing Block. - // 2. ZIP Central Directory. - // 3. ZIP End of Central Directory (EoCD). - // Each of these sections is represented as a separate DataSource instance below. - - // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to - // avoid wasting physical memory. In most APK verification scenarios, the contents of the - // APK are already there in the OS's page cache and thus mmap does not use additional - // physical memory. - DataSource beforeApkSigningBlock = - new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset); - DataSource centralDir = - new MemoryMappedFileDataSource( - apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset); + ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData); + verifyAdditionalAttributes(additionalAttrs); - // For the purposes of integrity verification, ZIP End of Central Directory's field Start of - // Central Directory must be considered to point to the offset of the APK Signing Block. - eocdBuf = eocdBuf.duplicate(); - eocdBuf.order(ByteOrder.LITTLE_ENDIAN); - ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset); - DataSource eocd = new ByteBufferDataSource(eocdBuf); - - int[] digestAlgorithms = new int[expectedDigests.size()]; - int digestAlgorithmCount = 0; - for (int digestAlgorithm : expectedDigests.keySet()) { - digestAlgorithms[digestAlgorithmCount] = digestAlgorithm; - digestAlgorithmCount++; - } - byte[][] actualDigests; - try { - actualDigests = - computeContentDigests( - digestAlgorithms, - new DataSource[] {beforeApkSigningBlock, centralDir, eocd}); - } catch (DigestException e) { - throw new SecurityException("Failed to compute digest(s) of contents", e); - } - for (int i = 0; i < digestAlgorithms.length; i++) { - int digestAlgorithm = digestAlgorithms[i]; - byte[] expectedDigest = expectedDigests.get(digestAlgorithm); - byte[] actualDigest = actualDigests[i]; - if (!MessageDigest.isEqual(expectedDigest, actualDigest)) { - throw new SecurityException( - getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) - + " digest of contents did not verify"); - } - } + return certs.toArray(new X509Certificate[certs.size()]); } - private static byte[][] computeContentDigests( - int[] digestAlgorithms, - DataSource[] contents) throws DigestException { - // For each digest algorithm the result is computed as follows: - // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. - // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. - // No chunks are produced for empty (zero length) segments. - // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's - // length in bytes (uint32 little-endian) and the chunk's contents. - // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of - // chunks (uint32 little-endian) and the concatenation of digests of chunks of all - // segments in-order. - - long totalChunkCountLong = 0; - for (DataSource input : contents) { - totalChunkCountLong += getChunkCount(input.size()); - } - if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) { - throw new DigestException("Too many chunks: " + totalChunkCountLong); - } - int totalChunkCount = (int) totalChunkCountLong; - - byte[][] digestsOfChunks = new byte[digestAlgorithms.length][]; - for (int i = 0; i < digestAlgorithms.length; i++) { - int digestAlgorithm = digestAlgorithms[i]; - int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); - byte[] concatenationOfChunkCountAndChunkDigests = - new byte[5 + totalChunkCount * digestOutputSizeBytes]; - concatenationOfChunkCountAndChunkDigests[0] = 0x5a; - setUnsignedInt32LittleEndian( - totalChunkCount, - concatenationOfChunkCountAndChunkDigests, - 1); - digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; - } + // Attribute to check whether a newer APK Signature Scheme signature was stripped + private static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; - byte[] chunkContentPrefix = new byte[5]; - chunkContentPrefix[0] = (byte) 0xa5; - int chunkIndex = 0; - MessageDigest[] mds = new MessageDigest[digestAlgorithms.length]; - for (int i = 0; i < digestAlgorithms.length; i++) { - String jcaAlgorithmName = - getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]); - try { - mds[i] = MessageDigest.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); + private static void verifyAdditionalAttributes(ByteBuffer attrs) + throws SecurityException, IOException { + while (attrs.hasRemaining()) { + ByteBuffer attr = getLengthPrefixedSlice(attrs); + if (attr.remaining() < 4) { + throw new IOException("Remaining buffer too short to contain additional attribute " + + "ID. Remaining: " + attr.remaining()); } - } - // TODO: Compute digests of chunks in parallel when beneficial. This requires some research - // into how to parallelize (if at all) based on the capabilities of the hardware on which - // this code is running and based on the size of input. - DataDigester digester = new MultipleDigestDataDigester(mds); - int dataSourceIndex = 0; - for (DataSource input : contents) { - long inputOffset = 0; - long inputRemaining = input.size(); - while (inputRemaining > 0) { - int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES); - setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); - for (int i = 0; i < mds.length; i++) { - mds[i].update(chunkContentPrefix); - } - try { - input.feedIntoDataDigester(digester, inputOffset, chunkSize); - } catch (IOException e) { - throw new DigestException( - "Failed to digest chunk #" + chunkIndex + " of section #" - + dataSourceIndex, - e); - } - for (int i = 0; i < digestAlgorithms.length; i++) { - int digestAlgorithm = digestAlgorithms[i]; - byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; - int expectedDigestSizeBytes = - getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); - MessageDigest md = mds[i]; - int actualDigestSizeBytes = - md.digest( - concatenationOfChunkCountAndChunkDigests, - 5 + chunkIndex * expectedDigestSizeBytes, - expectedDigestSizeBytes); - if (actualDigestSizeBytes != expectedDigestSizeBytes) { - throw new RuntimeException( - "Unexpected output size of " + md.getAlgorithm() + " digest: " - + actualDigestSizeBytes); + int id = attr.getInt(); + switch (id) { + case STRIPPING_PROTECTION_ATTR_ID: + if (attr.remaining() < 4) { + throw new IOException("V2 Signature Scheme Stripping Protection Attribute " + + " value too small. Expected 4 bytes, but found " + + attr.remaining()); } - } - inputOffset += chunkSize; - inputRemaining -= chunkSize; - chunkIndex++; - } - dataSourceIndex++; - } - - byte[][] result = new byte[digestAlgorithms.length][]; - for (int i = 0; i < digestAlgorithms.length; i++) { - int digestAlgorithm = digestAlgorithms[i]; - byte[] input = digestsOfChunks[i]; - String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); - MessageDigest md; - try { - md = MessageDigest.getInstance(jcaAlgorithmName); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); + int vers = attr.getInt(); + if (vers == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) { + throw new SecurityException("V2 signature indicates APK is signed using APK" + + " Signature Scheme v3, but none was found. Signature stripped?"); + } + break; + default: + // not the droid we're looking for, move along, move along. + break; } - byte[] output = md.digest(input); - result[i] = output; - } - return result; - } - - /** - * Returns the ZIP End of Central Directory (EoCD) and its offset in the file. - * - * @throws IOException if an I/O error occurs while reading the file. - * @throws SignatureNotFoundException if the EoCD could not be found. - */ - private static Pair getEocd(RandomAccessFile apk) - throws IOException, SignatureNotFoundException { - Pair eocdAndOffsetInFile = - ZipUtils.findZipEndOfCentralDirectoryRecord(apk); - if (eocdAndOffsetInFile == null) { - throw new SignatureNotFoundException( - "Not an APK file: ZIP End of Central Directory record not found"); - } - return eocdAndOffsetInFile; - } - - private static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset) - throws SignatureNotFoundException { - // Look up the offset of ZIP Central Directory. - long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); - if (centralDirOffset > eocdOffset) { - throw new SignatureNotFoundException( - "ZIP Central Directory offset out of range: " + centralDirOffset - + ". ZIP End of Central Directory offset: " + eocdOffset); } - long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd); - if (centralDirOffset + centralDirSize != eocdOffset) { - throw new SignatureNotFoundException( - "ZIP Central Directory is not immediately followed by End of Central" - + " Directory"); - } - return centralDirOffset; - } - - private static final long getChunkCount(long inputSizeBytes) { - return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES; + return; } - - private static final int CHUNK_SIZE_BYTES = 1024 * 1024; - - private static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; - private static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; - private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; - private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; - private static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; - private static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; - private static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; - - private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; - private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; - private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) { switch (sigAlgorithm) { case SIGNATURE_RSA_PSS_WITH_SHA256: @@ -585,531 +400,4 @@ public class ApkSignatureSchemeV2Verifier { return false; } } - - private static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) { - int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1); - int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2); - return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2); - } - - private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) { - switch (digestAlgorithm1) { - case CONTENT_DIGEST_CHUNKED_SHA256: - switch (digestAlgorithm2) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return 0; - case CONTENT_DIGEST_CHUNKED_SHA512: - return -1; - default: - throw new IllegalArgumentException( - "Unknown digestAlgorithm2: " + digestAlgorithm2); - } - case CONTENT_DIGEST_CHUNKED_SHA512: - switch (digestAlgorithm2) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return 1; - case CONTENT_DIGEST_CHUNKED_SHA512: - return 0; - default: - throw new IllegalArgumentException( - "Unknown digestAlgorithm2: " + digestAlgorithm2); - } - default: - throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1); - } - } - - private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) { - switch (sigAlgorithm) { - case SIGNATURE_RSA_PSS_WITH_SHA256: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: - case SIGNATURE_ECDSA_WITH_SHA256: - case SIGNATURE_DSA_WITH_SHA256: - return CONTENT_DIGEST_CHUNKED_SHA256; - case SIGNATURE_RSA_PSS_WITH_SHA512: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: - case SIGNATURE_ECDSA_WITH_SHA512: - return CONTENT_DIGEST_CHUNKED_SHA512; - default: - throw new IllegalArgumentException( - "Unknown signature algorithm: 0x" - + Long.toHexString(sigAlgorithm & 0xffffffff)); - } - } - - private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { - switch (digestAlgorithm) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return "SHA-256"; - case CONTENT_DIGEST_CHUNKED_SHA512: - return "SHA-512"; - default: - throw new IllegalArgumentException( - "Unknown content digest algorthm: " + digestAlgorithm); - } - } - - private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) { - switch (digestAlgorithm) { - case CONTENT_DIGEST_CHUNKED_SHA256: - return 256 / 8; - case CONTENT_DIGEST_CHUNKED_SHA512: - return 512 / 8; - default: - throw new IllegalArgumentException( - "Unknown content digest algorthm: " + digestAlgorithm); - } - } - - private static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) { - switch (sigAlgorithm) { - case SIGNATURE_RSA_PSS_WITH_SHA256: - case SIGNATURE_RSA_PSS_WITH_SHA512: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: - return "RSA"; - case SIGNATURE_ECDSA_WITH_SHA256: - case SIGNATURE_ECDSA_WITH_SHA512: - return "EC"; - case SIGNATURE_DSA_WITH_SHA256: - return "DSA"; - default: - throw new IllegalArgumentException( - "Unknown signature algorithm: 0x" - + Long.toHexString(sigAlgorithm & 0xffffffff)); - } - } - - private static Pair - getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) { - switch (sigAlgorithm) { - case SIGNATURE_RSA_PSS_WITH_SHA256: - return Pair.create( - "SHA256withRSA/PSS", - new PSSParameterSpec( - "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); - case SIGNATURE_RSA_PSS_WITH_SHA512: - return Pair.create( - "SHA512withRSA/PSS", - new PSSParameterSpec( - "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: - return Pair.create("SHA256withRSA", null); - case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: - return Pair.create("SHA512withRSA", null); - case SIGNATURE_ECDSA_WITH_SHA256: - return Pair.create("SHA256withECDSA", null); - case SIGNATURE_ECDSA_WITH_SHA512: - return Pair.create("SHA512withECDSA", null); - case SIGNATURE_DSA_WITH_SHA256: - return Pair.create("SHA256withDSA", null); - default: - throw new IllegalArgumentException( - "Unknown signature algorithm: 0x" - + Long.toHexString(sigAlgorithm & 0xffffffff)); - } - } - - /** - * Returns new byte buffer whose content is a shared subsequence of this buffer's content - * between the specified start (inclusive) and end (exclusive) positions. As opposed to - * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source - * buffer's byte order. - */ - private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { - if (start < 0) { - throw new IllegalArgumentException("start: " + start); - } - if (end < start) { - throw new IllegalArgumentException("end < start: " + end + " < " + start); - } - int capacity = source.capacity(); - if (end > source.capacity()) { - throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); - } - int originalLimit = source.limit(); - int originalPosition = source.position(); - try { - source.position(0); - source.limit(end); - source.position(start); - ByteBuffer result = source.slice(); - result.order(source.order()); - return result; - } finally { - source.position(0); - source.limit(originalLimit); - source.position(originalPosition); - } - } - - /** - * Relative get method for reading {@code size} number of bytes from the current - * position of this buffer. - * - *

    This method reads the next {@code size} bytes at this buffer's current position, - * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to - * {@code size}, byte order set to this buffer's byte order; and then increments the position by - * {@code size}. - */ - private static ByteBuffer getByteBuffer(ByteBuffer source, int size) - throws BufferUnderflowException { - if (size < 0) { - throw new IllegalArgumentException("size: " + size); - } - int originalLimit = source.limit(); - int position = source.position(); - int limit = position + size; - if ((limit < position) || (limit > originalLimit)) { - throw new BufferUnderflowException(); - } - source.limit(limit); - try { - ByteBuffer result = source.slice(); - result.order(source.order()); - source.position(limit); - return result; - } finally { - source.limit(originalLimit); - } - } - - private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException { - if (source.remaining() < 4) { - throw new IOException( - "Remaining buffer too short to contain length of length-prefixed field." - + " Remaining: " + source.remaining()); - } - int len = source.getInt(); - if (len < 0) { - throw new IllegalArgumentException("Negative length"); - } else if (len > source.remaining()) { - throw new IOException("Length-prefixed field longer than remaining buffer." - + " Field length: " + len + ", remaining: " + source.remaining()); - } - return getByteBuffer(source, len); - } - - private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException { - int len = buf.getInt(); - if (len < 0) { - throw new IOException("Negative length"); - } else if (len > buf.remaining()) { - throw new IOException("Underflow while reading length-prefixed value. Length: " + len - + ", available: " + buf.remaining()); - } - byte[] result = new byte[len]; - buf.get(result); - return result; - } - - private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { - result[offset] = (byte) (value & 0xff); - result[offset + 1] = (byte) ((value >>> 8) & 0xff); - result[offset + 2] = (byte) ((value >>> 16) & 0xff); - result[offset + 3] = (byte) ((value >>> 24) & 0xff); - } - - private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; - private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; - private static final int APK_SIG_BLOCK_MIN_SIZE = 32; - - private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; - - private static Pair findApkSigningBlock( - RandomAccessFile apk, long centralDirOffset) - throws IOException, SignatureNotFoundException { - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes payload - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - - if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { - throw new SignatureNotFoundException( - "APK too small for APK Signing Block. ZIP Central Directory offset: " - + centralDirOffset); - } - // Read the magic and offset in file from the footer section of the block: - // * uint64: size of block - // * 16 bytes: magic - ByteBuffer footer = ByteBuffer.allocate(24); - footer.order(ByteOrder.LITTLE_ENDIAN); - apk.seek(centralDirOffset - footer.capacity()); - apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity()); - if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) - || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { - throw new SignatureNotFoundException( - "No APK Signing Block before ZIP Central Directory"); - } - // Read and compare size fields - long apkSigBlockSizeInFooter = footer.getLong(0); - if ((apkSigBlockSizeInFooter < footer.capacity()) - || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { - throw new SignatureNotFoundException( - "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); - } - int totalSize = (int) (apkSigBlockSizeInFooter + 8); - long apkSigBlockOffset = centralDirOffset - totalSize; - if (apkSigBlockOffset < 0) { - throw new SignatureNotFoundException( - "APK Signing Block offset out of range: " + apkSigBlockOffset); - } - ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); - apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); - apk.seek(apkSigBlockOffset); - apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity()); - long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); - if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { - throw new SignatureNotFoundException( - "APK Signing Block sizes in header and footer do not match: " - + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); - } - return Pair.create(apkSigBlock, apkSigBlockOffset); - } - - private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock) - throws SignatureNotFoundException { - checkByteOrderLittleEndian(apkSigningBlock); - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes pairs - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); - - int entryCount = 0; - while (pairs.hasRemaining()) { - entryCount++; - if (pairs.remaining() < 8) { - throw new SignatureNotFoundException( - "Insufficient data to read size of APK Signing Block entry #" + entryCount); - } - long lenLong = pairs.getLong(); - if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount - + " size out of range: " + lenLong); - } - int len = (int) lenLong; - int nextEntryPos = pairs.position() + len; - if (len > pairs.remaining()) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount + " size out of range: " + len - + ", available: " + pairs.remaining()); - } - int id = pairs.getInt(); - if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { - return getByteBuffer(pairs, len - 4); - } - pairs.position(nextEntryPos); - } - - throw new SignatureNotFoundException( - "No APK Signature Scheme v2 block in APK Signing Block"); - } - - private static void checkByteOrderLittleEndian(ByteBuffer buffer) { - if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { - throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); - } - } - - public static class SignatureNotFoundException extends Exception { - private static final long serialVersionUID = 1L; - - public SignatureNotFoundException(String message) { - super(message); - } - - public SignatureNotFoundException(String message, Throwable cause) { - super(message, cause); - } - } - - /** - * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is feeded. - */ - private static class MultipleDigestDataDigester implements DataDigester { - private final MessageDigest[] mMds; - - MultipleDigestDataDigester(MessageDigest[] mds) { - mMds = mds; - } - - @Override - public void consume(ByteBuffer buffer) { - buffer = buffer.slice(); - for (MessageDigest md : mMds) { - buffer.position(0); - md.update(buffer); - } - } - - @Override - public void finish() {} - } - - /** - * For legacy reasons we need to return exactly the original encoded certificate bytes, instead - * of letting the underlying implementation have a shot at re-encoding the data. - */ - private static class VerbatimX509Certificate extends WrappedX509Certificate { - private byte[] encodedVerbatim; - - public VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) { - super(wrapped); - this.encodedVerbatim = encodedVerbatim; - } - - @Override - public byte[] getEncoded() throws CertificateEncodingException { - return encodedVerbatim; - } - } - - private static class WrappedX509Certificate extends X509Certificate { - private final X509Certificate wrapped; - - public WrappedX509Certificate(X509Certificate wrapped) { - this.wrapped = wrapped; - } - - @Override - public Set getCriticalExtensionOIDs() { - return wrapped.getCriticalExtensionOIDs(); - } - - @Override - public byte[] getExtensionValue(String oid) { - return wrapped.getExtensionValue(oid); - } - - @Override - public Set getNonCriticalExtensionOIDs() { - return wrapped.getNonCriticalExtensionOIDs(); - } - - @Override - public boolean hasUnsupportedCriticalExtension() { - return wrapped.hasUnsupportedCriticalExtension(); - } - - @Override - public void checkValidity() - throws CertificateExpiredException, CertificateNotYetValidException { - wrapped.checkValidity(); - } - - @Override - public void checkValidity(Date date) - throws CertificateExpiredException, CertificateNotYetValidException { - wrapped.checkValidity(date); - } - - @Override - public int getVersion() { - return wrapped.getVersion(); - } - - @Override - public BigInteger getSerialNumber() { - return wrapped.getSerialNumber(); - } - - @Override - public Principal getIssuerDN() { - return wrapped.getIssuerDN(); - } - - @Override - public Principal getSubjectDN() { - return wrapped.getSubjectDN(); - } - - @Override - public Date getNotBefore() { - return wrapped.getNotBefore(); - } - - @Override - public Date getNotAfter() { - return wrapped.getNotAfter(); - } - - @Override - public byte[] getTBSCertificate() throws CertificateEncodingException { - return wrapped.getTBSCertificate(); - } - - @Override - public byte[] getSignature() { - return wrapped.getSignature(); - } - - @Override - public String getSigAlgName() { - return wrapped.getSigAlgName(); - } - - @Override - public String getSigAlgOID() { - return wrapped.getSigAlgOID(); - } - - @Override - public byte[] getSigAlgParams() { - return wrapped.getSigAlgParams(); - } - - @Override - public boolean[] getIssuerUniqueID() { - return wrapped.getIssuerUniqueID(); - } - - @Override - public boolean[] getSubjectUniqueID() { - return wrapped.getSubjectUniqueID(); - } - - @Override - public boolean[] getKeyUsage() { - return wrapped.getKeyUsage(); - } - - @Override - public int getBasicConstraints() { - return wrapped.getBasicConstraints(); - } - - @Override - public byte[] getEncoded() throws CertificateEncodingException { - return wrapped.getEncoded(); - } - - @Override - public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, - InvalidKeyException, NoSuchProviderException, SignatureException { - wrapped.verify(key); - } - - @Override - public void verify(PublicKey key, String sigProvider) - throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, - NoSuchProviderException, SignatureException { - wrapped.verify(key, sigProvider); - } - - @Override - public String toString() { - return wrapped.toString(); - } - - @Override - public PublicKey getPublicKey() { - return wrapped.getPublicKey(); - } - } } diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java new file mode 100644 index 00000000..e43dee35 --- /dev/null +++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256; +import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512; +import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; + +import android.os.Build; +import android.util.ArrayMap; +import android.util.Pair; + +import java.io.ByteArrayInputStream; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * APK Signature Scheme v3 verifier. + * + * @hide for internal use only. + */ +public class ApkSignatureSchemeV3Verifier { + + /** + * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing. + */ + public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 3; + + private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + + /** + * Returns {@code true} if the provided APK contains an APK Signature Scheme V3 signature. + * + *

    NOTE: This method does not verify the signature. + */ + public static boolean hasSignature(String apkFile) throws IOException { + try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { + findSignature(apk); + return true; + } catch (SignatureNotFoundException e) { + return false; + } + } + + /** + * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates + * associated with each signer. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. + * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not + * verify. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + public static VerifiedSigner verify(String apkFile) + throws SignatureNotFoundException, SecurityException, IOException { + return verify(apkFile, true); + } + + /** + * Returns the certificates associated with each signer for the given APK without verification. + * This method is dangerous and should not be used, unless the caller is absolutely certain the + * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v3 + * Block while gathering signer information. The APK contents are not verified. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + public static VerifiedSigner plsCertsNoVerifyOnlyCerts(String apkFile) + throws SignatureNotFoundException, SecurityException, IOException { + return verify(apkFile, false); + } + + private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity) + throws SignatureNotFoundException, SecurityException, IOException { + try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { + return verify(apk, verifyIntegrity); + } + } + + /** + * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates + * associated with each signer. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. + * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not + * verify. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity) + throws SignatureNotFoundException, SecurityException, IOException { + SignatureInfo signatureInfo = findSignature(apk); + return verify(apk.getFD(), signatureInfo, verifyIntegrity); + } + + /** + * Returns the APK Signature Scheme v3 block contained in the provided APK file and the + * additional information relevant for verifying the block against the file. + * + * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + private static SignatureInfo findSignature(RandomAccessFile apk) + throws IOException, SignatureNotFoundException { + return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + } + + /** + * Verifies the contents of the provided APK file against the provided APK Signature Scheme v3 + * Block. + * + * @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it + * against the APK file. + */ + private static VerifiedSigner verify( + FileDescriptor apkFileDescriptor, + SignatureInfo signatureInfo, + boolean doVerifyIntegrity) throws SecurityException { + int signerCount = 0; + Map contentDigests = new ArrayMap<>(); + VerifiedSigner result = null; + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + ByteBuffer signers; + try { + signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); + } catch (IOException e) { + throw new SecurityException("Failed to read list of signers", e); + } + while (signers.hasRemaining()) { + try { + ByteBuffer signer = getLengthPrefixedSlice(signers); + result = verifySigner(signer, contentDigests, certFactory); + signerCount++; + } catch (PlatformNotSupportedException e) { + // this signer is for a different platform, ignore it. + continue; + } catch (IOException | BufferUnderflowException | SecurityException e) { + throw new SecurityException( + "Failed to parse/verify signer #" + signerCount + " block", + e); + } + } + + if (signerCount < 1 || result == null) { + throw new SecurityException("No signers found"); + } + + if (signerCount != 1) { + throw new SecurityException("APK Signature Scheme V3 only supports one signer: " + + "multiple signers found."); + } + + if (contentDigests.isEmpty()) { + throw new SecurityException("No content digests found"); + } + + if (doVerifyIntegrity) { + ApkSigningBlockUtils.verifyIntegrity( + contentDigests, + apkFileDescriptor, + signatureInfo.apkSigningBlockOffset, + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset, + signatureInfo.eocd); + } + + return result; + } + + private static VerifiedSigner verifySigner( + ByteBuffer signerBlock, + Map contentDigests, + CertificateFactory certFactory) + throws SecurityException, IOException, PlatformNotSupportedException { + ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); + int minSdkVersion = signerBlock.getInt(); + int maxSdkVersion = signerBlock.getInt(); + + if (Build.VERSION.SDK_INT < minSdkVersion || Build.VERSION.SDK_INT > maxSdkVersion) { + // this signature isn't meant to be used with this platform, skip it. + throw new PlatformNotSupportedException( + "Signer not supported by this platform " + + "version. This platform: " + Build.VERSION.SDK_INT + + ", signer minSdkVersion: " + minSdkVersion + + ", maxSdkVersion: " + maxSdkVersion); + } + + ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); + byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); + + int signatureCount = 0; + int bestSigAlgorithm = -1; + byte[] bestSigAlgorithmSignatureBytes = null; + List signaturesSigAlgorithms = new ArrayList<>(); + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = getLengthPrefixedSlice(signatures); + if (signature.remaining() < 8) { + throw new SecurityException("Signature record too short"); + } + int sigAlgorithm = signature.getInt(); + signaturesSigAlgorithms.add(sigAlgorithm); + if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { + continue; + } + if ((bestSigAlgorithm == -1) + || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { + bestSigAlgorithm = sigAlgorithm; + bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); + } + } catch (IOException | BufferUnderflowException e) { + throw new SecurityException( + "Failed to parse signature record #" + signatureCount, + e); + } + } + if (bestSigAlgorithm == -1) { + if (signatureCount == 0) { + throw new SecurityException("No signatures found"); + } else { + throw new SecurityException("No supported signatures found"); + } + } + + String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm); + Pair signatureAlgorithmParams = + getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); + String jcaSignatureAlgorithm = signatureAlgorithmParams.first; + AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; + boolean sigVerified; + try { + PublicKey publicKey = + KeyFactory.getInstance(keyAlgorithm) + .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e) { + throw new SecurityException( + "Failed to verify " + jcaSignatureAlgorithm + " signature", e); + } + if (!sigVerified) { + throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); + } + + // Signature over signedData has verified. + + byte[] contentDigest = null; + signedData.clear(); + ByteBuffer digests = getLengthPrefixedSlice(signedData); + List digestsSigAlgorithms = new ArrayList<>(); + int digestCount = 0; + while (digests.hasRemaining()) { + digestCount++; + try { + ByteBuffer digest = getLengthPrefixedSlice(digests); + if (digest.remaining() < 8) { + throw new IOException("Record too short"); + } + int sigAlgorithm = digest.getInt(); + digestsSigAlgorithms.add(sigAlgorithm); + if (sigAlgorithm == bestSigAlgorithm) { + contentDigest = readLengthPrefixedByteArray(digest); + } + } catch (IOException | BufferUnderflowException e) { + throw new IOException("Failed to parse digest record #" + digestCount, e); + } + } + + if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) { + throw new SecurityException( + "Signature algorithms don't match between digests and signatures records"); + } + int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm); + byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest); + if ((previousSignerDigest != null) + && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) { + throw new SecurityException( + getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) + + " contents digest does not match the digest specified by a preceding signer"); + } + + ByteBuffer certificates = getLengthPrefixedSlice(signedData); + List certs = new ArrayList<>(); + int certificateCount = 0; + while (certificates.hasRemaining()) { + certificateCount++; + byte[] encodedCert = readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = (X509Certificate) + certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); + } catch (CertificateException e) { + throw new SecurityException("Failed to decode certificate #" + certificateCount, e); + } + certificate = new VerbatimX509Certificate( + certificate, encodedCert); + certs.add(certificate); + } + + if (certs.isEmpty()) { + throw new SecurityException("No certificates listed"); + } + X509Certificate mainCertificate = certs.get(0); + byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); + if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { + throw new SecurityException( + "Public key mismatch between certificate and signature record"); + } + + int signedMinSDK = signedData.getInt(); + if (signedMinSDK != minSdkVersion) { + throw new SecurityException( + "minSdkVersion mismatch between signed and unsigned in v3 signer block."); + } + + int signedMaxSDK = signedData.getInt(); + if (signedMaxSDK != maxSdkVersion) { + throw new SecurityException( + "maxSdkVersion mismatch between signed and unsigned in v3 signer block."); + } + + ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData); + return verifyAdditionalAttributes(additionalAttrs, certs, certFactory); + } + + private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + + private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs, + List certs, CertificateFactory certFactory) throws IOException { + X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]); + VerifiedProofOfRotation por = null; + + while (attrs.hasRemaining()) { + ByteBuffer attr = getLengthPrefixedSlice(attrs); + if (attr.remaining() < 4) { + throw new IOException("Remaining buffer too short to contain additional attribute " + + "ID. Remaining: " + attr.remaining()); + } + int id = attr.getInt(); + switch(id) { + case PROOF_OF_ROTATION_ATTR_ID: + if (por != null) { + throw new SecurityException("Encountered multiple Proof-of-rotation records" + + " when verifying APK Signature Scheme v3 signature"); + } + por = verifyProofOfRotationStruct(attr, certFactory); + // make sure that the last certificate in the Proof-of-rotation record matches + // the one used to sign this APK. + try { + if (por.certs.size() > 0 + && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(), + certChain[0].getEncoded())) { + throw new SecurityException("Terminal certificate in Proof-of-rotation" + + " record does not match APK signing certificate"); + } + } catch (CertificateEncodingException e) { + throw new SecurityException("Failed to encode certificate when comparing" + + " Proof-of-rotation record and signing certificate", e); + } + + break; + default: + // not the droid we're looking for, move along, move along. + break; + } + } + return new VerifiedSigner(certChain, por); + } + + private static VerifiedProofOfRotation verifyProofOfRotationStruct( + ByteBuffer porBuf, + CertificateFactory certFactory) + throws SecurityException, IOException { + int levelCount = 0; + int lastSigAlgorithm = -1; + X509Certificate lastCert = null; + List certs = new ArrayList<>(); + List flagsList = new ArrayList<>(); + + // Proof-of-rotation struct: + // is basically a singly linked list of nodes, called levels here, each of which have the + // following structure: + // * length-prefix for the entire level + // - length-prefixed signed data (if previous level exists) + // * length-prefixed X509 Certificate + // * uint32 signature algorithm ID describing how this signed data was signed + // - uint32 flags describing how to treat the cert contained in this level + // - uint32 signature algorithm ID to use to verify the signature of the next level. The + // algorithm here must match the one in the signed data section of the next level. + // - length-prefixed signature over the signed data in this level. The signature here + // is verified using the certificate from the previous level. + // The linking is provided by the certificate of each level signing the one of the next. + while (porBuf.hasRemaining()) { + levelCount++; + try { + ByteBuffer level = getLengthPrefixedSlice(porBuf); + ByteBuffer signedData = getLengthPrefixedSlice(level); + int flags = level.getInt(); + int sigAlgorithm = level.getInt(); + byte[] signature = readLengthPrefixedByteArray(level); + + if (lastCert != null) { + // Use previous level cert to verify current level + Pair sigAlgParams = + getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(sigAlgParams.first); + sig.initVerify(publicKey); + if (sigAlgParams.second != null) { + sig.setParameter(sigAlgParams.second); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + levelCount + " using " + sigAlgParams.first + " when verifying" + + " Proof-of-rotation record"); + } + } + + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + levelCount + " when verifying Proof-of-rotation record"); + } + lastCert = (X509Certificate) + certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); + lastCert = new VerbatimX509Certificate(lastCert, encodedCert); + + lastSigAlgorithm = sigAlgorithm; + certs.add(lastCert); + flagsList.add(flags); + } catch (IOException | BufferUnderflowException e) { + throw new IOException("Failed to parse Proof-of-rotation record", e); + } catch (NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e) { + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + + levelCount + " when verifying Proof-of-rotation record", e); + } catch (CertificateException e) { + throw new SecurityException("Failed to decode certificate #" + levelCount + + " when verifying Proof-of-rotation record", e); + } + } + return new VerifiedProofOfRotation(certs, flagsList); + } + + private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) { + switch (sigAlgorithm) { + case SIGNATURE_RSA_PSS_WITH_SHA256: + case SIGNATURE_RSA_PSS_WITH_SHA512: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: + case SIGNATURE_ECDSA_WITH_SHA256: + case SIGNATURE_ECDSA_WITH_SHA512: + case SIGNATURE_DSA_WITH_SHA256: + return true; + default: + return false; + } + } + + /** + * Verified processed proof of rotation. + * + * @hide for internal use only. + */ + public static class VerifiedProofOfRotation { + public final List certs; + public final List flagsList; + + public VerifiedProofOfRotation(List certs, List flagsList) { + this.certs = certs; + this.flagsList = flagsList; + } + } + + /** + * Verified APK Signature Scheme v3 signer, including the proof of rotation structure. + * + * @hide for internal use only. + */ + public static class VerifiedSigner { + public final X509Certificate[] certs; + public final VerifiedProofOfRotation por; + + public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) { + this.certs = certs; + this.por = por; + } + + } + + private static class PlatformNotSupportedException extends Exception { + + PlatformNotSupportedException(String s) { + super(s); + } + } +} diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java new file mode 100644 index 00000000..81467292 --- /dev/null +++ b/android/util/apk/ApkSignatureVerifier.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION; +import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; + +import android.content.pm.PackageParser; +import android.content.pm.PackageParser.PackageParserException; +import android.content.pm.Signature; +import android.os.Trace; +import android.util.jar.StrictJarFile; + +import com.android.internal.util.ArrayUtils; + +import libcore.io.IoUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.ZipEntry; + +/** + * Facade class that takes care of the details of APK verification on + * behalf of PackageParser. + * + * @hide for internal use only. + */ +public class ApkSignatureVerifier { + + public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + + private static final AtomicReference sBuffer = new AtomicReference<>(); + + /** + * Verifies the provided APK and returns the certificates associated with each signer. + * + * @throws PackageParserException if the APK's signature failed to verify. + */ + public static Result verify(String apkPath, int minSignatureSchemeVersion) + throws PackageParserException { + + if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) { + // V3 and before are older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // first try v3 + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3"); + try { + ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner = + ApkSignatureSchemeV3Verifier.verify(apkPath); + Certificate[][] signerCerts = new Certificate[][] { vSigner.certs }; + Signature[] signerSigs = convertToSignatures(signerCerts); + return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3); + } catch (SignatureNotFoundException e) { + // not signed with v2, try older if allowed + if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v3 signature in package " + apkPath, e); + } + } catch (Exception e) { + // APK Signature Scheme v2 signature found but did not verify + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v2", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + + // redundant, protective version check + if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) { + // V2 and before are older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // try v2 + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2"); + try { + Certificate[][] signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath); + Signature[] signerSigs = convertToSignatures(signerCerts); + + return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2); + } catch (SignatureNotFoundException e) { + // not signed with v2, try older if allowed + if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v2 signature in package " + apkPath, e); + } + } catch (Exception e) { + // APK Signature Scheme v2 signature found but did not verify + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v2", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + + // redundant, protective version check + if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) { + // V1 and is older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // v2 didn't work, try jarsigner + return verifyV1Signature(apkPath, true); + } + + /** + * Verifies the provided APK and returns the certificates associated with each signer. + * + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + * + * @throws PackageParserException if there was a problem collecting certificates + */ + private static Result verifyV1Signature(String apkPath, boolean verifyFull) + throws PackageParserException { + StrictJarFile jarFile = null; + + try { + final Certificate[][] lastCerts; + final Signature[] lastSigs; + + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor"); + + // we still pass verify = true to ctor to collect certs, even though we're not checking + // the whole jar. + jarFile = new StrictJarFile( + apkPath, + true, // collect certs + verifyFull); // whether to reject APK with stripped v2 signatures (b/27887819) + final List toVerify = new ArrayList<>(); + + // Gather certs from AndroidManifest.xml, which every APK must have, as an optimization + // to not need to verify the whole APK when verifyFUll == false. + final ZipEntry manifestEntry = jarFile.findEntry( + PackageParser.ANDROID_MANIFEST_FILENAME); + if (manifestEntry == null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Package " + apkPath + " has no manifest"); + } + lastCerts = loadCertificates(jarFile, manifestEntry); + if (ArrayUtils.isEmpty(lastCerts)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package " + + apkPath + " has no certificates at entry " + + PackageParser.ANDROID_MANIFEST_FILENAME); + } + lastSigs = convertToSignatures(lastCerts); + + // fully verify all contents, except for AndroidManifest.xml and the META-INF/ files. + if (verifyFull) { + final Iterator i = jarFile.iterator(); + while (i.hasNext()) { + final ZipEntry entry = i.next(); + if (entry.isDirectory()) continue; + + final String entryName = entry.getName(); + if (entryName.startsWith("META-INF/")) continue; + if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue; + + toVerify.add(entry); + } + + for (ZipEntry entry : toVerify) { + final Certificate[][] entryCerts = loadCertificates(jarFile, entry); + if (ArrayUtils.isEmpty(entryCerts)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Package " + apkPath + " has no certificates at entry " + + entry.getName()); + } + + // make sure all entries use the same signing certs + final Signature[] entrySigs = convertToSignatures(entryCerts); + if (!Signature.areExactMatch(lastSigs, entrySigs)) { + throw new PackageParserException( + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + "Package " + apkPath + " has mismatched certificates at entry " + + entry.getName()); + } + } + } + return new Result(lastCerts, lastSigs, VERSION_JAR_SIGNATURE_SCHEME); + } catch (GeneralSecurityException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING, + "Failed to collect certificates from " + apkPath, e); + } catch (IOException | RuntimeException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath, e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + closeQuietly(jarFile); + } + } + + private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry) + throws PackageParserException { + InputStream is = null; + try { + // We must read the stream for the JarEntry to retrieve + // its certificates. + is = jarFile.getInputStream(entry); + readFullyIgnoringContents(is); + return jarFile.getCertificateChains(entry); + } catch (IOException | RuntimeException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed reading " + entry.getName() + " in " + jarFile, e); + } finally { + IoUtils.closeQuietly(is); + } + } + + private static void readFullyIgnoringContents(InputStream in) throws IOException { + byte[] buffer = sBuffer.getAndSet(null); + if (buffer == null) { + buffer = new byte[4096]; + } + + int n = 0; + int count = 0; + while ((n = in.read(buffer, 0, buffer.length)) != -1) { + count += n; + } + + sBuffer.set(buffer); + return; + } + + /** + * Converts an array of certificate chains into the {@code Signature} equivalent used by the + * PackageManager. + * + * @throws CertificateEncodingException if it is unable to create a Signature object. + */ + public static Signature[] convertToSignatures(Certificate[][] certs) + throws CertificateEncodingException { + final Signature[] res = new Signature[certs.length]; + for (int i = 0; i < certs.length; i++) { + res[i] = new Signature(certs[i]); + } + return res; + } + + private static void closeQuietly(StrictJarFile jarFile) { + if (jarFile != null) { + try { + jarFile.close(); + } catch (Exception ignored) { + } + } + } + + /** + * Returns the certificates associated with each signer for the given APK without verification. + * This method is dangerous and should not be used, unless the caller is absolutely certain the + * APK is trusted. + * + * @throws PackageParserException if the APK's signature failed to verify. + * or greater is not found, except in the case of no JAR signature. + */ + public static Result plsCertsNoVerifyOnlyCerts(String apkPath, int minSignatureSchemeVersion) + throws PackageParserException { + + if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) { + // V3 and before are older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // first try v3 + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3"); + try { + ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner = + ApkSignatureSchemeV3Verifier.plsCertsNoVerifyOnlyCerts(apkPath); + Certificate[][] signerCerts = new Certificate[][] { vSigner.certs }; + Signature[] signerSigs = convertToSignatures(signerCerts); + return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3); + } catch (SignatureNotFoundException e) { + // not signed with v2, try older if allowed + if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v3 signature in package " + apkPath, e); + } + } catch (Exception e) { + // APK Signature Scheme v2 signature found but did not verify + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v2", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + + // redundant, protective version check + if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) { + // V2 and before are older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // first try v2 + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "certsOnlyV2"); + try { + Certificate[][] signerCerts = + ApkSignatureSchemeV2Verifier.plsCertsNoVerifyOnlyCerts(apkPath); + Signature[] signerSigs = convertToSignatures(signerCerts); + return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2); + } catch (SignatureNotFoundException e) { + // not signed with v2, try older if allowed + if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v2 signature in package " + apkPath, e); + } + } catch (Exception e) { + // APK Signature Scheme v2 signature found but did not verify + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v2", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + + // redundant, protective version check + if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) { + // V1 and is older than the requested minimum signing version + throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // v2 didn't work, try jarsigner + return verifyV1Signature(apkPath, false); + } + + /** + * Result of a successful APK verification operation. + */ + public static class Result { + public final Certificate[][] certs; + public final Signature[] sigs; + public final int signatureSchemeVersion; + + public Result(Certificate[][] certs, Signature[] sigs, int signingVersion) { + this.certs = certs; + this.sigs = sigs; + this.signatureSchemeVersion = signingVersion; + } + } +} diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java new file mode 100644 index 00000000..9279510a --- /dev/null +++ b/android/util/apk/ApkSigningBlockUtils.java @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import android.util.Pair; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.Map; + +/** + * Utility class for an APK Signature Scheme using the APK Signing Block. + * + * @hide for internal use only. + */ +final class ApkSigningBlockUtils { + + private ApkSigningBlockUtils() { + } + + /** + * Returns the APK Signature Scheme block contained in the provided APK file and the + * additional information relevant for verifying the block against the file. + * + * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs + * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 + * block ID. + * + * @throws SignatureNotFoundException if the APK is not signed using this scheme. + * @throws IOException if an I/O error occurs while reading the APK file. + */ + static SignatureInfo findSignature(RandomAccessFile apk, int blockId) + throws IOException, SignatureNotFoundException { + // Find the ZIP End of Central Directory (EoCD) record. + Pair eocdAndOffsetInFile = getEocd(apk); + ByteBuffer eocd = eocdAndOffsetInFile.first; + long eocdOffset = eocdAndOffsetInFile.second; + if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { + throw new SignatureNotFoundException("ZIP64 APK not supported"); + } + + // Find the APK Signing Block. The block immediately precedes the Central Directory. + long centralDirOffset = getCentralDirOffset(eocd, eocdOffset); + Pair apkSigningBlockAndOffsetInFile = + findApkSigningBlock(apk, centralDirOffset); + ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first; + long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second; + + // Find the APK Signature Scheme Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock, + blockId); + + return new SignatureInfo( + apkSignatureSchemeBlock, + apkSigningBlockOffset, + centralDirOffset, + eocdOffset, + eocd); + } + + static void verifyIntegrity( + Map expectedDigests, + FileDescriptor apkFileDescriptor, + long apkSigningBlockOffset, + long centralDirOffset, + long eocdOffset, + ByteBuffer eocdBuf) throws SecurityException { + + if (expectedDigests.isEmpty()) { + throw new SecurityException("No digests provided"); + } + + // We need to verify the integrity of the following three sections of the file: + // 1. Everything up to the start of the APK Signing Block. + // 2. ZIP Central Directory. + // 3. ZIP End of Central Directory (EoCD). + // Each of these sections is represented as a separate DataSource instance below. + + // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to + // avoid wasting physical memory. In most APK verification scenarios, the contents of the + // APK are already there in the OS's page cache and thus mmap does not use additional + // physical memory. + DataSource beforeApkSigningBlock = + new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset); + DataSource centralDir = + new MemoryMappedFileDataSource( + apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset); + + // For the purposes of integrity verification, ZIP End of Central Directory's field Start of + // Central Directory must be considered to point to the offset of the APK Signing Block. + eocdBuf = eocdBuf.duplicate(); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset); + DataSource eocd = new ByteBufferDataSource(eocdBuf); + + int[] digestAlgorithms = new int[expectedDigests.size()]; + int digestAlgorithmCount = 0; + for (int digestAlgorithm : expectedDigests.keySet()) { + digestAlgorithms[digestAlgorithmCount] = digestAlgorithm; + digestAlgorithmCount++; + } + byte[][] actualDigests; + try { + actualDigests = + computeContentDigests( + digestAlgorithms, + new DataSource[] {beforeApkSigningBlock, centralDir, eocd}); + } catch (DigestException e) { + throw new SecurityException("Failed to compute digest(s) of contents", e); + } + for (int i = 0; i < digestAlgorithms.length; i++) { + int digestAlgorithm = digestAlgorithms[i]; + byte[] expectedDigest = expectedDigests.get(digestAlgorithm); + byte[] actualDigest = actualDigests[i]; + if (!MessageDigest.isEqual(expectedDigest, actualDigest)) { + throw new SecurityException( + getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) + + " digest of contents did not verify"); + } + } + } + + private static byte[][] computeContentDigests( + int[] digestAlgorithms, + DataSource[] contents) throws DigestException { + // For each digest algorithm the result is computed as follows: + // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. + // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. + // No chunks are produced for empty (zero length) segments. + // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's + // length in bytes (uint32 little-endian) and the chunk's contents. + // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of + // chunks (uint32 little-endian) and the concatenation of digests of chunks of all + // segments in-order. + + long totalChunkCountLong = 0; + for (DataSource input : contents) { + totalChunkCountLong += getChunkCount(input.size()); + } + if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) { + throw new DigestException("Too many chunks: " + totalChunkCountLong); + } + int totalChunkCount = (int) totalChunkCountLong; + + byte[][] digestsOfChunks = new byte[digestAlgorithms.length][]; + for (int i = 0; i < digestAlgorithms.length; i++) { + int digestAlgorithm = digestAlgorithms[i]; + int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); + byte[] concatenationOfChunkCountAndChunkDigests = + new byte[5 + totalChunkCount * digestOutputSizeBytes]; + concatenationOfChunkCountAndChunkDigests[0] = 0x5a; + setUnsignedInt32LittleEndian( + totalChunkCount, + concatenationOfChunkCountAndChunkDigests, + 1); + digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; + } + + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + int chunkIndex = 0; + MessageDigest[] mds = new MessageDigest[digestAlgorithms.length]; + for (int i = 0; i < digestAlgorithms.length; i++) { + String jcaAlgorithmName = + getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]); + try { + mds[i] = MessageDigest.getInstance(jcaAlgorithmName); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); + } + } + // TODO: Compute digests of chunks in parallel when beneficial. This requires some research + // into how to parallelize (if at all) based on the capabilities of the hardware on which + // this code is running and based on the size of input. + DataDigester digester = new MultipleDigestDataDigester(mds); + int dataSourceIndex = 0; + for (DataSource input : contents) { + long inputOffset = 0; + long inputRemaining = input.size(); + while (inputRemaining > 0) { + int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES); + setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); + for (int i = 0; i < mds.length; i++) { + mds[i].update(chunkContentPrefix); + } + try { + input.feedIntoDataDigester(digester, inputOffset, chunkSize); + } catch (IOException e) { + throw new DigestException( + "Failed to digest chunk #" + chunkIndex + " of section #" + + dataSourceIndex, + e); + } + for (int i = 0; i < digestAlgorithms.length; i++) { + int digestAlgorithm = digestAlgorithms[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + int expectedDigestSizeBytes = + getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); + MessageDigest md = mds[i]; + int actualDigestSizeBytes = + md.digest( + concatenationOfChunkCountAndChunkDigests, + 5 + chunkIndex * expectedDigestSizeBytes, + expectedDigestSizeBytes); + if (actualDigestSizeBytes != expectedDigestSizeBytes) { + throw new RuntimeException( + "Unexpected output size of " + md.getAlgorithm() + " digest: " + + actualDigestSizeBytes); + } + } + inputOffset += chunkSize; + inputRemaining -= chunkSize; + chunkIndex++; + } + dataSourceIndex++; + } + + byte[][] result = new byte[digestAlgorithms.length][]; + for (int i = 0; i < digestAlgorithms.length; i++) { + int digestAlgorithm = digestAlgorithms[i]; + byte[] input = digestsOfChunks[i]; + String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); + MessageDigest md; + try { + md = MessageDigest.getInstance(jcaAlgorithmName); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); + } + byte[] output = md.digest(input); + result[i] = output; + } + return result; + } + + /** + * Returns the ZIP End of Central Directory (EoCD) and its offset in the file. + * + * @throws IOException if an I/O error occurs while reading the file. + * @throws SignatureNotFoundException if the EoCD could not be found. + */ + static Pair getEocd(RandomAccessFile apk) + throws IOException, SignatureNotFoundException { + Pair eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new SignatureNotFoundException( + "Not an APK file: ZIP End of Central Directory record not found"); + } + return eocdAndOffsetInFile; + } + + static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset) + throws SignatureNotFoundException { + // Look up the offset of ZIP Central Directory. + long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); + if (centralDirOffset > eocdOffset) { + throw new SignatureNotFoundException( + "ZIP Central Directory offset out of range: " + centralDirOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd); + if (centralDirOffset + centralDirSize != eocdOffset) { + throw new SignatureNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central" + + " Directory"); + } + return centralDirOffset; + } + + private static long getChunkCount(long inputSizeBytes) { + return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES; + } + + private static final int CHUNK_SIZE_BYTES = 1024 * 1024; + + static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; + static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; + static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; + static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; + static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; + static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; + static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; + + static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; + static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; + + static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) { + int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1); + int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2); + return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2); + } + + private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) { + switch (digestAlgorithm1) { + case CONTENT_DIGEST_CHUNKED_SHA256: + switch (digestAlgorithm2) { + case CONTENT_DIGEST_CHUNKED_SHA256: + return 0; + case CONTENT_DIGEST_CHUNKED_SHA512: + return -1; + default: + throw new IllegalArgumentException( + "Unknown digestAlgorithm2: " + digestAlgorithm2); + } + case CONTENT_DIGEST_CHUNKED_SHA512: + switch (digestAlgorithm2) { + case CONTENT_DIGEST_CHUNKED_SHA256: + return 1; + case CONTENT_DIGEST_CHUNKED_SHA512: + return 0; + default: + throw new IllegalArgumentException( + "Unknown digestAlgorithm2: " + digestAlgorithm2); + } + default: + throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1); + } + } + + static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) { + switch (sigAlgorithm) { + case SIGNATURE_RSA_PSS_WITH_SHA256: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: + case SIGNATURE_ECDSA_WITH_SHA256: + case SIGNATURE_DSA_WITH_SHA256: + return CONTENT_DIGEST_CHUNKED_SHA256; + case SIGNATURE_RSA_PSS_WITH_SHA512: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: + case SIGNATURE_ECDSA_WITH_SHA512: + return CONTENT_DIGEST_CHUNKED_SHA512; + default: + throw new IllegalArgumentException( + "Unknown signature algorithm: 0x" + + Long.toHexString(sigAlgorithm & 0xffffffff)); + } + } + + static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { + switch (digestAlgorithm) { + case CONTENT_DIGEST_CHUNKED_SHA256: + return "SHA-256"; + case CONTENT_DIGEST_CHUNKED_SHA512: + return "SHA-512"; + default: + throw new IllegalArgumentException( + "Unknown content digest algorthm: " + digestAlgorithm); + } + } + + private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) { + switch (digestAlgorithm) { + case CONTENT_DIGEST_CHUNKED_SHA256: + return 256 / 8; + case CONTENT_DIGEST_CHUNKED_SHA512: + return 512 / 8; + default: + throw new IllegalArgumentException( + "Unknown content digest algorthm: " + digestAlgorithm); + } + } + + static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) { + switch (sigAlgorithm) { + case SIGNATURE_RSA_PSS_WITH_SHA256: + case SIGNATURE_RSA_PSS_WITH_SHA512: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: + return "RSA"; + case SIGNATURE_ECDSA_WITH_SHA256: + case SIGNATURE_ECDSA_WITH_SHA512: + return "EC"; + case SIGNATURE_DSA_WITH_SHA256: + return "DSA"; + default: + throw new IllegalArgumentException( + "Unknown signature algorithm: 0x" + + Long.toHexString(sigAlgorithm & 0xffffffff)); + } + } + + static Pair + getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) { + switch (sigAlgorithm) { + case SIGNATURE_RSA_PSS_WITH_SHA256: + return Pair.create( + "SHA256withRSA/PSS", + new PSSParameterSpec( + "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); + case SIGNATURE_RSA_PSS_WITH_SHA512: + return Pair.create( + "SHA512withRSA/PSS", + new PSSParameterSpec( + "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: + return Pair.create("SHA256withRSA", null); + case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: + return Pair.create("SHA512withRSA", null); + case SIGNATURE_ECDSA_WITH_SHA256: + return Pair.create("SHA256withECDSA", null); + case SIGNATURE_ECDSA_WITH_SHA512: + return Pair.create("SHA512withECDSA", null); + case SIGNATURE_DSA_WITH_SHA256: + return Pair.create("SHA256withDSA", null); + default: + throw new IllegalArgumentException( + "Unknown signature algorithm: 0x" + + Long.toHexString(sigAlgorithm & 0xffffffff)); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative get method for reading {@code size} number of bytes from the current + * position of this buffer. + * + *

    This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + static ByteBuffer getByteBuffer(ByteBuffer source, int size) + throws BufferUnderflowException { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException { + if (source.remaining() < 4) { + throw new IOException( + "Remaining buffer too short to contain length of length-prefixed field." + + " Remaining: " + source.remaining()); + } + int len = source.getInt(); + if (len < 0) { + throw new IllegalArgumentException("Negative length"); + } else if (len > source.remaining()) { + throw new IOException("Length-prefixed field longer than remaining buffer." + + " Field length: " + len + ", remaining: " + source.remaining()); + } + return getByteBuffer(source, len); + } + + static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException { + int len = buf.getInt(); + if (len < 0) { + throw new IOException("Negative length"); + } else if (len > buf.remaining()) { + throw new IOException("Underflow while reading length-prefixed value. Length: " + len + + ", available: " + buf.remaining()); + } + byte[] result = new byte[len]; + buf.get(result); + return result; + } + + static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { + result[offset] = (byte) (value & 0xff); + result[offset + 1] = (byte) ((value >>> 8) & 0xff); + result[offset + 2] = (byte) ((value >>> 16) & 0xff); + result[offset + 3] = (byte) ((value >>> 24) & 0xff); + } + + private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + private static final int APK_SIG_BLOCK_MIN_SIZE = 32; + + static Pair findApkSigningBlock( + RandomAccessFile apk, long centralDirOffset) + throws IOException, SignatureNotFoundException { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { + throw new SignatureNotFoundException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + ByteBuffer footer = ByteBuffer.allocate(24); + footer.order(ByteOrder.LITTLE_ENDIAN); + apk.seek(centralDirOffset - footer.capacity()); + apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity()); + if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { + throw new SignatureNotFoundException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new SignatureNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + int totalSize = (int) (apkSigBlockSizeInFooter + 8); + long apkSigBlockOffset = centralDirOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new SignatureNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + apk.seek(apkSigBlockOffset); + apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity()); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new SignatureNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return Pair.create(apkSigBlock, apkSigBlockOffset); + } + + static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId) + throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + int len = (int) lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + if (id == blockId) { + return getByteBuffer(pairs, len - 4); + } + pairs.position(nextEntryPos); + } + + throw new SignatureNotFoundException( + "No block with ID " + blockId + " in APK Signing Block."); + } + + private static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + /** + * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is fed. + */ + private static class MultipleDigestDataDigester implements DataDigester { + private final MessageDigest[] mMds; + + MultipleDigestDataDigester(MessageDigest[] mds) { + mMds = mds; + } + + @Override + public void consume(ByteBuffer buffer) { + buffer = buffer.slice(); + for (MessageDigest md : mMds) { + buffer.position(0); + md.update(buffer); + } + } + + @Override + public void finish() {} + } + +} diff --git a/android/util/apk/SignatureNotFoundException.java b/android/util/apk/SignatureNotFoundException.java new file mode 100644 index 00000000..9c7c7600 --- /dev/null +++ b/android/util/apk/SignatureNotFoundException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +/** + * Indicates that the APK is missing a signature. + * + * @hide + */ +public class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/android/util/apk/VerbatimX509Certificate.java b/android/util/apk/VerbatimX509Certificate.java new file mode 100644 index 00000000..9984c6d2 --- /dev/null +++ b/android/util/apk/VerbatimX509Certificate.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +/** + * For legacy reasons we need to return exactly the original encoded certificate bytes, instead + * of letting the underlying implementation have a shot at re-encoding the data. + */ +class VerbatimX509Certificate extends WrappedX509Certificate { + private final byte[] mEncodedVerbatim; + + VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) { + super(wrapped); + this.mEncodedVerbatim = encodedVerbatim; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return mEncodedVerbatim; + } +} diff --git a/android/util/apk/WrappedX509Certificate.java b/android/util/apk/WrappedX509Certificate.java new file mode 100644 index 00000000..fdaa4202 --- /dev/null +++ b/android/util/apk/WrappedX509Certificate.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +class WrappedX509Certificate extends X509Certificate { + private final X509Certificate mWrapped; + + WrappedX509Certificate(X509Certificate wrapped) { + this.mWrapped = wrapped; + } + + @Override + public Set getCriticalExtensionOIDs() { + return mWrapped.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return mWrapped.getExtensionValue(oid); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return mWrapped.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return mWrapped.hasUnsupportedCriticalExtension(); + } + + @Override + public void checkValidity() + throws CertificateExpiredException, CertificateNotYetValidException { + mWrapped.checkValidity(); + } + + @Override + public void checkValidity(Date date) + throws CertificateExpiredException, CertificateNotYetValidException { + mWrapped.checkValidity(date); + } + + @Override + public int getVersion() { + return mWrapped.getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return mWrapped.getSerialNumber(); + } + + @Override + public Principal getIssuerDN() { + return mWrapped.getIssuerDN(); + } + + @Override + public Principal getSubjectDN() { + return mWrapped.getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return mWrapped.getNotBefore(); + } + + @Override + public Date getNotAfter() { + return mWrapped.getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return mWrapped.getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return mWrapped.getSignature(); + } + + @Override + public String getSigAlgName() { + return mWrapped.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return mWrapped.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return mWrapped.getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return mWrapped.getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return mWrapped.getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return mWrapped.getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return mWrapped.getBasicConstraints(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return mWrapped.getEncoded(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + mWrapped.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException { + mWrapped.verify(key, sigProvider); + } + + @Override + public String toString() { + return mWrapped.toString(); + } + + @Override + public PublicKey getPublicKey() { + return mWrapped.getPublicKey(); + } +} diff --git a/android/util/jar/StrictJarVerifier.java b/android/util/jar/StrictJarVerifier.java index debc170f..45254908 100644 --- a/android/util/jar/StrictJarVerifier.java +++ b/android/util/jar/StrictJarVerifier.java @@ -18,6 +18,8 @@ package android.util.jar; import android.util.apk.ApkSignatureSchemeV2Verifier; +import android.util.apk.ApkSignatureSchemeV3Verifier; + import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -36,6 +38,7 @@ import java.util.Map; import java.util.StringTokenizer; import java.util.jar.Attributes; import java.util.jar.JarFile; + import sun.security.jca.Providers; import sun.security.pkcs.PKCS7; import sun.security.pkcs.SignerInfo; @@ -55,6 +58,15 @@ import sun.security.pkcs.SignerInfo; * */ class StrictJarVerifier { + /** + * {@code .SF} file header section attribute indicating that the APK is signed not just with + * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute + * facilitates v2 signature stripping detection. + * + *

    The attribute contains a comma-separated set of signature scheme IDs. + */ + private static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed"; + /** * List of accepted digest algorithms. This list is in order from most * preferred to least preferred. @@ -373,17 +385,17 @@ class StrictJarVerifier { return; } - // If requested, check whether APK Signature Scheme v2 signature was stripped. + // If requested, check whether a newer APK Signature Scheme signature was stripped. if (signatureSchemeRollbackProtectionsEnforced) { String apkSignatureSchemeIdList = - attributes.getValue( - ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME); + attributes.getValue(SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME); if (apkSignatureSchemeIdList != null) { // This field contains a comma-separated list of APK signature scheme IDs which // were used to sign this APK. If an ID is known to us, it means signatures of that // scheme were stripped from the APK because otherwise we wouldn't have fallen back // to verifying the APK using the JAR signature scheme. boolean v2SignatureGenerated = false; + boolean v3SignatureGenerated = false; StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ","); while (tokenizer.hasMoreTokens()) { String idText = tokenizer.nextToken().trim(); @@ -402,6 +414,12 @@ class StrictJarVerifier { v2SignatureGenerated = true; break; } + if (id == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) { + // This APK was supposed to be signed with APK Signature Scheme v3 but no + // such signature was found. + v3SignatureGenerated = true; + break; + } } if (v2SignatureGenerated) { @@ -409,6 +427,11 @@ class StrictJarVerifier { + " is signed using APK Signature Scheme v2, but no such signature was" + " found. Signature stripped?"); } + if (v3SignatureGenerated) { + throw new SecurityException(signatureFile + " indicates " + jarName + + " is signed using APK Signature Scheme v3, but no such signature was" + + " found. Signature stripped?"); + } } } diff --git a/android/view/Choreographer.java b/android/view/Choreographer.java index 2ffa1c5e..ba6b6cf6 100644 --- a/android/view/Choreographer.java +++ b/android/view/Choreographer.java @@ -164,6 +164,7 @@ public final class Choreographer { private long mLastFrameTimeNanos; private long mFrameIntervalNanos; private boolean mDebugPrintNextFrameTimeDelta; + private int mFPSDivisor = 1; /** * Contains information about the current frame for jank-tracking, @@ -601,6 +602,11 @@ public final class Choreographer { } } + void setFPSDivisor(int divisor) { + if (divisor <= 0) divisor = 1; + mFPSDivisor = divisor; + } + void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { @@ -643,6 +649,14 @@ public final class Choreographer { return; } + if (mFPSDivisor > 1) { + long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos; + if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) { + scheduleVsyncLocked(); + return; + } + } + mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; diff --git a/android/view/Display.java b/android/view/Display.java index 9673be01..5bd7446d 100644 --- a/android/view/Display.java +++ b/android/view/Display.java @@ -1323,10 +1323,10 @@ public final class Display { public static final int HDR_TYPE_HLG = 3; /** @hide */ - @IntDef({ - HDR_TYPE_DOLBY_VISION, - HDR_TYPE_HDR10, - HDR_TYPE_HLG, + @IntDef(prefix = { "HDR_TYPE_" }, value = { + HDR_TYPE_DOLBY_VISION, + HDR_TYPE_HDR10, + HDR_TYPE_HLG, }) @Retention(RetentionPolicy.SOURCE) public @interface HdrType {} diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java index 19cd42e1..e448f14c 100644 --- a/android/view/DisplayCutout.java +++ b/android/view/DisplayCutout.java @@ -21,40 +21,37 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import android.annotation.NonNull; +import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; import android.os.Parcel; import android.os.Parcelable; import com.android.internal.annotations.VisibleForTesting; -import java.util.ArrayList; import java.util.List; /** * Represents a part of the display that is not functional for displaying content. * *

    {@code DisplayCutout} is immutable. - * - * @hide will become API */ public final class DisplayCutout { - private static final Rect ZERO_RECT = new Rect(0, 0, 0, 0); - private static final ArrayList EMPTY_LIST = new ArrayList<>(); + private static final Rect ZERO_RECT = new Rect(); + private static final Region EMPTY_REGION = new Region(); /** - * An instance where {@link #hasCutout()} returns {@code false}. + * An instance where {@link #isEmpty()} returns {@code true}. * * @hide */ - public static final DisplayCutout NO_CUTOUT = - new DisplayCutout(ZERO_RECT, ZERO_RECT, EMPTY_LIST); + public static final DisplayCutout NO_CUTOUT = new DisplayCutout(ZERO_RECT, EMPTY_REGION); private final Rect mSafeInsets; - private final Rect mBoundingRect; - private final List mBoundingPolygon; + private final Region mBounds; /** * Creates a DisplayCutout instance. @@ -64,22 +61,18 @@ public final class DisplayCutout { * @hide */ @VisibleForTesting - public DisplayCutout(Rect safeInsets, Rect boundingRect, List boundingPolygon) { + public DisplayCutout(Rect safeInsets, Region bounds) { mSafeInsets = safeInsets != null ? safeInsets : ZERO_RECT; - mBoundingRect = boundingRect != null ? boundingRect : ZERO_RECT; - mBoundingPolygon = boundingPolygon != null ? boundingPolygon : EMPTY_LIST; + mBounds = bounds != null ? bounds : Region.obtain(); } /** - * Returns whether there is a cutout. - * - * If false, the safe insets will all return zero, and the bounding box or polygon will be - * empty or outside the content view. + * Returns true if there is no cutout or it is outside of the content view. * - * @return {@code true} if there is a cutout, {@code false} otherwise + * @hide */ - public boolean hasCutout() { - return !mSafeInsets.equals(ZERO_RECT); + public boolean isEmpty() { + return mSafeInsets.equals(ZERO_RECT); } /** Returns the inset from the top which avoids the display cutout. */ @@ -103,44 +96,41 @@ public final class DisplayCutout { } /** - * Obtains the safe insets in a rect. + * Returns the safe insets in a rect. * - * @param out a rect which is set to the safe insets. + * @return a rect which is set to the safe insets. * @hide */ - public void getSafeInsets(@NonNull Rect out) { - out.set(mSafeInsets); + public Rect getSafeInsets() { + return new Rect(mSafeInsets); } /** - * Obtains the bounding rect of the cutout. + * Returns the bounding region of the cutout. * - * @param outRect is filled with the bounding rect of the cutout. Coordinates are relative + * @return the bounding region of the cutout. Coordinates are relative * to the top-left corner of the content view. */ - public void getBoundingRect(@NonNull Rect outRect) { - outRect.set(mBoundingRect); + public Region getBounds() { + return Region.obtain(mBounds); } /** - * Obtains the bounding polygon of the cutout. + * Returns the bounding rect of the cutout. * - * @param outPolygon is filled with a list of points representing the corners of a convex - * polygon which covers the cutout. Coordinates are relative to the - * top-left corner of the content view. + * @return the bounding rect of the cutout. Coordinates are relative + * to the top-left corner of the content view. + * @hide */ - public void getBoundingPolygon(List outPolygon) { - outPolygon.clear(); - for (int i = 0; i < mBoundingPolygon.size(); i++) { - outPolygon.add(new Point(mBoundingPolygon.get(i))); - } + public Rect getBoundingRect() { + // TODO(roosa): Inline. + return mBounds.getBounds(); } @Override public int hashCode() { int result = mSafeInsets.hashCode(); - result = result * 31 + mBoundingRect.hashCode(); - result = result * 31 + mBoundingPolygon.hashCode(); + result = result * 31 + mBounds.getBounds().hashCode(); return result; } @@ -152,8 +142,7 @@ public final class DisplayCutout { if (o instanceof DisplayCutout) { DisplayCutout c = (DisplayCutout) o; return mSafeInsets.equals(c.mSafeInsets) - && mBoundingRect.equals(c.mBoundingRect) - && mBoundingPolygon.equals(c.mBoundingPolygon); + && mBounds.equals(c.mBounds); } return false; } @@ -161,7 +150,7 @@ public final class DisplayCutout { @Override public String toString() { return "DisplayCutout{insets=" + mSafeInsets - + " bounding=" + mBoundingRect + + " bounds=" + mBounds + "}"; } @@ -172,15 +161,13 @@ public final class DisplayCutout { * @hide */ public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { - if (mBoundingRect.isEmpty() + if (mBounds.isEmpty() || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) { return this; } Rect safeInsets = new Rect(mSafeInsets); - Rect boundingRect = new Rect(mBoundingRect); - ArrayList boundingPolygon = new ArrayList<>(); - getBoundingPolygon(boundingPolygon); + Region bounds = Region.obtain(mBounds); // Note: it's not really well defined what happens when the inset is negative, because we // don't know if the safe inset needs to expand in general. @@ -197,10 +184,9 @@ public final class DisplayCutout { safeInsets.right = atLeastZero(safeInsets.right - insetRight); } - boundingRect.offset(-insetLeft, -insetTop); - offset(boundingPolygon, -insetLeft, -insetTop); + bounds.translate(-insetLeft, -insetTop); - return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + return new DisplayCutout(safeInsets, bounds); } /** @@ -210,20 +196,17 @@ public final class DisplayCutout { * @hide */ public DisplayCutout calculateRelativeTo(Rect frame) { - if (mBoundingRect.isEmpty() || !Rect.intersects(frame, mBoundingRect)) { + if (mBounds.isEmpty() || !Rect.intersects(frame, mBounds.getBounds())) { return NO_CUTOUT; } - Rect boundingRect = new Rect(mBoundingRect); - ArrayList boundingPolygon = new ArrayList<>(); - getBoundingPolygon(boundingPolygon); - - return DisplayCutout.calculateRelativeTo(frame, boundingRect, boundingPolygon); + return DisplayCutout.calculateRelativeTo(frame, Region.obtain(mBounds)); } - private static DisplayCutout calculateRelativeTo(Rect frame, Rect boundingRect, - ArrayList boundingPolygon) { + private static DisplayCutout calculateRelativeTo(Rect frame, Region bounds) { + Rect boundingRect = bounds.getBounds(); Rect safeRect = new Rect(); + int bestArea = 0; int bestVariant = 0; for (int variant = ROTATION_0; variant <= ROTATION_270; variant++) { @@ -247,10 +230,9 @@ public final class DisplayCutout { Math.max(0, frame.bottom - safeRect.bottom)); } - boundingRect.offset(-frame.left, -frame.top); - offset(boundingPolygon, -frame.left, -frame.top); + bounds.translate(-frame.left, -frame.top); - return new DisplayCutout(safeRect, boundingRect, boundingPolygon); + return new DisplayCutout(safeRect, bounds); } private static int calculateInsetVariantArea(Rect frame, Rect boundingRect, int variant, @@ -277,11 +259,6 @@ public final class DisplayCutout { return value < 0 ? 0 : value; } - private static void offset(ArrayList points, int dx, int dy) { - for (int i = 0; i < points.size(); i++) { - points.get(i).offset(dx, dy); - } - } /** * Creates an instance from a bounding polygon. @@ -289,20 +266,28 @@ public final class DisplayCutout { * @hide */ public static DisplayCutout fromBoundingPolygon(List points) { - Rect boundingRect = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE, - Integer.MIN_VALUE, Integer.MIN_VALUE); - ArrayList boundingPolygon = new ArrayList<>(); + Region bounds = Region.obtain(); + Path path = new Path(); + path.reset(); for (int i = 0; i < points.size(); i++) { Point point = points.get(i); - boundingRect.left = Math.min(boundingRect.left, point.x); - boundingRect.right = Math.max(boundingRect.right, point.x); - boundingRect.top = Math.min(boundingRect.top, point.y); - boundingRect.bottom = Math.max(boundingRect.bottom, point.y); - boundingPolygon.add(new Point(point)); + if (i == 0) { + path.moveTo(point.x, point.y); + } else { + path.lineTo(point.x, point.y); + } } + path.close(); + + RectF clipRect = new RectF(); + path.computeBounds(clipRect, false /* unused */); + Region clipRegion = Region.obtain(); + clipRegion.set((int) clipRect.left, (int) clipRect.top, + (int) clipRect.right, (int) clipRect.bottom); - return new DisplayCutout(ZERO_RECT, boundingRect, boundingPolygon); + bounds.setPath(path, clipRegion); + return new DisplayCutout(ZERO_RECT, bounds); } /** @@ -336,8 +321,7 @@ public final class DisplayCutout { } else { out.writeInt(1); out.writeTypedObject(mInner.mSafeInsets, flags); - out.writeTypedObject(mInner.mBoundingRect, flags); - out.writeTypedList(mInner.mBoundingPolygon, flags); + out.writeTypedObject(mInner.mBounds, flags); } } @@ -368,13 +352,10 @@ public final class DisplayCutout { return NO_CUTOUT; } - ArrayList boundingPolygon = new ArrayList<>(); - Rect safeInsets = in.readTypedObject(Rect.CREATOR); - Rect boundingRect = in.readTypedObject(Rect.CREATOR); - in.readTypedList(boundingPolygon, Point.CREATOR); + Region bounds = in.readTypedObject(Region.CREATOR); - return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + return new DisplayCutout(safeInsets, bounds); } public DisplayCutout get() { diff --git a/android/view/FrameInfo.java b/android/view/FrameInfo.java index c79547c8..6c5e048f 100644 --- a/android/view/FrameInfo.java +++ b/android/view/FrameInfo.java @@ -16,7 +16,7 @@ package android.view; -import android.annotation.IntDef; +import android.annotation.LongDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -48,7 +48,7 @@ final class FrameInfo { // Is this the first-draw following a window layout? public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1; - @IntDef(flag = true, value = { + @LongDef(flag = true, value = { FLAG_WINDOW_LAYOUT_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface FrameInfoFlags {} diff --git a/android/view/GestureDetector.java b/android/view/GestureDetector.java index 52e53b07..bc2953e0 100644 --- a/android/view/GestureDetector.java +++ b/android/view/GestureDetector.java @@ -520,162 +520,163 @@ public class GestureDetector { boolean handled = false; switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_POINTER_DOWN: - mDownFocusX = mLastFocusX = focusX; - mDownFocusY = mLastFocusY = focusY; - // Cancel long press and taps - cancelTaps(); - break; - - case MotionEvent.ACTION_POINTER_UP: - mDownFocusX = mLastFocusX = focusX; - mDownFocusY = mLastFocusY = focusY; - - // Check the dot product of current velocities. - // If the pointer that left was opposing another velocity vector, clear. - mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); - final int upIndex = ev.getActionIndex(); - final int id1 = ev.getPointerId(upIndex); - final float x1 = mVelocityTracker.getXVelocity(id1); - final float y1 = mVelocityTracker.getYVelocity(id1); - for (int i = 0; i < count; i++) { - if (i == upIndex) continue; - - final int id2 = ev.getPointerId(i); - final float x = x1 * mVelocityTracker.getXVelocity(id2); - final float y = y1 * mVelocityTracker.getYVelocity(id2); - - final float dot = x + y; - if (dot < 0) { - mVelocityTracker.clear(); - break; + case MotionEvent.ACTION_POINTER_DOWN: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + // Cancel long press and taps + cancelTaps(); + break; + + case MotionEvent.ACTION_POINTER_UP: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + + // Check the dot product of current velocities. + // If the pointer that left was opposing another velocity vector, clear. + mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final int upIndex = ev.getActionIndex(); + final int id1 = ev.getPointerId(upIndex); + final float x1 = mVelocityTracker.getXVelocity(id1); + final float y1 = mVelocityTracker.getYVelocity(id1); + for (int i = 0; i < count; i++) { + if (i == upIndex) continue; + + final int id2 = ev.getPointerId(i); + final float x = x1 * mVelocityTracker.getXVelocity(id2); + final float y = y1 * mVelocityTracker.getYVelocity(id2); + + final float dot = x + y; + if (dot < 0) { + mVelocityTracker.clear(); + break; + } } - } - break; - - case MotionEvent.ACTION_DOWN: - if (mDoubleTapListener != null) { - boolean hadTapMessage = mHandler.hasMessages(TAP); - if (hadTapMessage) mHandler.removeMessages(TAP); - if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && - isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { - // This is a second tap - mIsDoubleTapping = true; - // Give a callback with the first tap of the double-tap - handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); - // Give a callback with down event of the double-tap - handled |= mDoubleTapListener.onDoubleTapEvent(ev); - } else { - // This is a first tap - mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + break; + + case MotionEvent.ACTION_DOWN: + if (mDoubleTapListener != null) { + boolean hadTapMessage = mHandler.hasMessages(TAP); + if (hadTapMessage) mHandler.removeMessages(TAP); + if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) + && hadTapMessage + && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { + // This is a second tap + mIsDoubleTapping = true; + // Give a callback with the first tap of the double-tap + handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); + // Give a callback with down event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else { + // This is a first tap + mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + } } - } - mDownFocusX = mLastFocusX = focusX; - mDownFocusY = mLastFocusY = focusY; - if (mCurrentDownEvent != null) { - mCurrentDownEvent.recycle(); - } - mCurrentDownEvent = MotionEvent.obtain(ev); - mAlwaysInTapRegion = true; - mAlwaysInBiggerTapRegion = true; - mStillDown = true; - mInLongPress = false; - mDeferConfirmSingleTap = false; - - if (mIsLongpressEnabled) { - mHandler.removeMessages(LONG_PRESS); - mHandler.sendEmptyMessageAtTime(LONG_PRESS, - mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT); - } - mHandler.sendEmptyMessageAtTime(SHOW_PRESS, - mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); - handled |= mListener.onDown(ev); - break; + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + if (mCurrentDownEvent != null) { + mCurrentDownEvent.recycle(); + } + mCurrentDownEvent = MotionEvent.obtain(ev); + mAlwaysInTapRegion = true; + mAlwaysInBiggerTapRegion = true; + mStillDown = true; + mInLongPress = false; + mDeferConfirmSingleTap = false; - case MotionEvent.ACTION_MOVE: - if (mInLongPress || mInContextClick) { + if (mIsLongpressEnabled) { + mHandler.removeMessages(LONG_PRESS); + mHandler.sendEmptyMessageAtTime(LONG_PRESS, + mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT); + } + mHandler.sendEmptyMessageAtTime(SHOW_PRESS, + mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); + handled |= mListener.onDown(ev); break; - } - final float scrollX = mLastFocusX - focusX; - final float scrollY = mLastFocusY - focusY; - if (mIsDoubleTapping) { - // Give the move events of the double-tap - handled |= mDoubleTapListener.onDoubleTapEvent(ev); - } else if (mAlwaysInTapRegion) { - final int deltaX = (int) (focusX - mDownFocusX); - final int deltaY = (int) (focusY - mDownFocusY); - int distance = (deltaX * deltaX) + (deltaY * deltaY); - int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare; - if (distance > slopSquare) { + + case MotionEvent.ACTION_MOVE: + if (mInLongPress || mInContextClick) { + break; + } + final float scrollX = mLastFocusX - focusX; + final float scrollY = mLastFocusY - focusY; + if (mIsDoubleTapping) { + // Give the move events of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mAlwaysInTapRegion) { + final int deltaX = (int) (focusX - mDownFocusX); + final int deltaY = (int) (focusY - mDownFocusY); + int distance = (deltaX * deltaX) + (deltaY * deltaY); + int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare; + if (distance > slopSquare) { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastFocusX = focusX; + mLastFocusY = focusY; + mAlwaysInTapRegion = false; + mHandler.removeMessages(TAP); + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + } + int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare; + if (distance > doubleTapSlopSquare) { + mAlwaysInBiggerTapRegion = false; + } + } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); mLastFocusX = focusX; mLastFocusY = focusY; - mAlwaysInTapRegion = false; - mHandler.removeMessages(TAP); - mHandler.removeMessages(SHOW_PRESS); - mHandler.removeMessages(LONG_PRESS); } - int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare; - if (distance > doubleTapSlopSquare) { - mAlwaysInBiggerTapRegion = false; + break; + + case MotionEvent.ACTION_UP: + mStillDown = false; + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mIsDoubleTapping) { + // Finally, give the up event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mInLongPress) { + mHandler.removeMessages(TAP); + mInLongPress = false; + } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) { + handled = mListener.onSingleTapUp(ev); + if (mDeferConfirmSingleTap && mDoubleTapListener != null) { + mDoubleTapListener.onSingleTapConfirmed(ev); + } + } else if (!mIgnoreNextUpEvent) { + + // A fling must travel the minimum tap distance + final VelocityTracker velocityTracker = mVelocityTracker; + final int pointerId = ev.getPointerId(0); + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final float velocityY = velocityTracker.getYVelocity(pointerId); + final float velocityX = velocityTracker.getXVelocity(pointerId); + + if ((Math.abs(velocityY) > mMinimumFlingVelocity) + || (Math.abs(velocityX) > mMinimumFlingVelocity)) { + handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); + } } - } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { - handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); - mLastFocusX = focusX; - mLastFocusY = focusY; - } - break; - - case MotionEvent.ACTION_UP: - mStillDown = false; - MotionEvent currentUpEvent = MotionEvent.obtain(ev); - if (mIsDoubleTapping) { - // Finally, give the up event of the double-tap - handled |= mDoubleTapListener.onDoubleTapEvent(ev); - } else if (mInLongPress) { - mHandler.removeMessages(TAP); - mInLongPress = false; - } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) { - handled = mListener.onSingleTapUp(ev); - if (mDeferConfirmSingleTap && mDoubleTapListener != null) { - mDoubleTapListener.onSingleTapConfirmed(ev); + if (mPreviousUpEvent != null) { + mPreviousUpEvent.recycle(); } - } else if (!mIgnoreNextUpEvent) { - - // A fling must travel the minimum tap distance - final VelocityTracker velocityTracker = mVelocityTracker; - final int pointerId = ev.getPointerId(0); - velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); - final float velocityY = velocityTracker.getYVelocity(pointerId); - final float velocityX = velocityTracker.getXVelocity(pointerId); - - if ((Math.abs(velocityY) > mMinimumFlingVelocity) - || (Math.abs(velocityX) > mMinimumFlingVelocity)){ - handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); + // Hold the event we obtained above - listeners may have changed the original. + mPreviousUpEvent = currentUpEvent; + if (mVelocityTracker != null) { + // This may have been cleared when we called out to the + // application above. + mVelocityTracker.recycle(); + mVelocityTracker = null; } - } - if (mPreviousUpEvent != null) { - mPreviousUpEvent.recycle(); - } - // Hold the event we obtained above - listeners may have changed the original. - mPreviousUpEvent = currentUpEvent; - if (mVelocityTracker != null) { - // This may have been cleared when we called out to the - // application above. - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - mIsDoubleTapping = false; - mDeferConfirmSingleTap = false; - mIgnoreNextUpEvent = false; - mHandler.removeMessages(SHOW_PRESS); - mHandler.removeMessages(LONG_PRESS); - break; - - case MotionEvent.ACTION_CANCEL: - cancel(); - break; + mIsDoubleTapping = false; + mDeferConfirmSingleTap = false; + mIgnoreNextUpEvent = false; + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + break; + + case MotionEvent.ACTION_CANCEL: + cancel(); + break; } if (!handled && mInputEventConsistencyVerifier != null) { diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java index 6c006cae..4d804c55 100644 --- a/android/view/IWindowManagerImpl.java +++ b/android/view/IWindowManagerImpl.java @@ -155,12 +155,6 @@ public class IWindowManagerImpl implements IWindowManager { return 0; } - @Override - public boolean inKeyguardRestrictedInputMode() throws RemoteException { - // TODO Auto-generated method stub - return false; - } - @Override public boolean inputMethodClientHasFocus(IInputMethodClient arg0) throws RemoteException { // TODO Auto-generated method stub diff --git a/android/view/Surface.java b/android/view/Surface.java index ddced6cd..a417a4a0 100644 --- a/android/view/Surface.java +++ b/android/view/Surface.java @@ -121,8 +121,12 @@ public class Surface implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({SCALING_MODE_FREEZE, SCALING_MODE_SCALE_TO_WINDOW, - SCALING_MODE_SCALE_CROP, SCALING_MODE_NO_SCALE_CROP}) + @IntDef(prefix = { "SCALING_MODE_" }, value = { + SCALING_MODE_FREEZE, + SCALING_MODE_SCALE_TO_WINDOW, + SCALING_MODE_SCALE_CROP, + SCALING_MODE_NO_SCALE_CROP + }) public @interface ScalingMode {} // From system/window.h /** @hide */ @@ -135,7 +139,12 @@ public class Surface implements Parcelable { public static final int SCALING_MODE_NO_SCALE_CROP = 3; /** @hide */ - @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}) + @IntDef(prefix = { "ROTATION_" }, value = { + ROTATION_0, + ROTATION_90, + ROTATION_180, + ROTATION_270 + }) @Retention(RetentionPolicy.SOURCE) public @interface Rotation {} diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index 3d01ec23..268e460d 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -16,17 +16,34 @@ package android.view; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; +import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MSCALE_Y; +import static android.graphics.Matrix.MSKEW_X; +import static android.graphics.Matrix.MSKEW_Y; +import static android.graphics.Matrix.MTRANS_X; +import static android.graphics.Matrix.MTRANS_Y; + import android.annotation.Size; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; import android.graphics.PixelFormat; +import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; import android.os.Process; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.Log; import android.view.Surface.OutOfResourcesException; + +import com.android.internal.annotations.GuardedBy; + import dalvik.system.CloseGuard; import libcore.util.NativeAllocationRegistry; @@ -36,12 +53,14 @@ import java.io.Closeable; * SurfaceControl * @hide */ -public class SurfaceControl { +public class SurfaceControl implements Parcelable { private static final String TAG = "SurfaceControl"; private static native long nativeCreate(SurfaceSession session, String name, int w, int h, int format, int flags, long parentObject, int windowType, int ownerUid) throws OutOfResourcesException; + private static native long nativeReadFromParcel(Parcel in); + private static native void nativeWriteToParcel(long nativeObject, Parcel out); private static native void nativeRelease(long nativeObject); private static native void nativeDestroy(long nativeObject); private static native void nativeDisconnect(long nativeObject); @@ -55,8 +74,6 @@ public class SurfaceControl { private static native void nativeScreenshot(IBinder displayToken, Surface consumer, Rect sourceCrop, int width, int height, int minLayer, int maxLayer, boolean allLayers, boolean useIdentityTransform); - private static native void nativeCaptureLayers(IBinder layerHandleToken, Surface consumer, - Rect sourceCrop, float frameScale); private static native GraphicBuffer nativeCaptureLayers(IBinder layerHandleToken, Rect sourceCrop, float frameScale); @@ -141,6 +158,13 @@ public class SurfaceControl { private final String mName; long mNativeObject; // package visibility only for Surface.java access + // TODO: Move this to native. + private final Object mSizeLock = new Object(); + @GuardedBy("mSizeLock") + private int mWidth; + @GuardedBy("mSizeLock") + private int mHeight; + static Transaction sGlobalTransaction; static long sTransactionNestCount = 0; @@ -555,6 +579,8 @@ public class SurfaceControl { } mName = name; + mWidth = w; + mHeight = h; mNativeObject = nativeCreate(session, name, w, h, format, flags, parent != null ? parent.mNativeObject : 0, windowType, ownerUid); if (mNativeObject == 0) { @@ -570,12 +596,49 @@ public class SurfaceControl { // event logging. public SurfaceControl(SurfaceControl other) { mName = other.mName; + mWidth = other.mWidth; + mHeight = other.mHeight; mNativeObject = other.mNativeObject; other.mCloseGuard.close(); other.mNativeObject = 0; mCloseGuard.open("release"); } + private SurfaceControl(Parcel in) { + mName = in.readString(); + mWidth = in.readInt(); + mHeight = in.readInt(); + mNativeObject = nativeReadFromParcel(in); + if (mNativeObject == 0) { + throw new IllegalArgumentException("Couldn't read SurfaceControl from parcel=" + in); + } + mCloseGuard.open("release"); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeInt(mWidth); + dest.writeInt(mHeight); + nativeWriteToParcel(mNativeObject, dest); + } + + public static final Creator CREATOR + = new Creator() { + public SurfaceControl createFromParcel(Parcel in) { + return new SurfaceControl(in); + } + + public SurfaceControl[] newArray(int size) { + return new SurfaceControl[size]; + } + }; + @Override protected void finalize() throws Throwable { try { @@ -666,7 +729,7 @@ public class SurfaceControl { */ @Deprecated public static void mergeToGlobalTransaction(Transaction t) { - synchronized(sGlobalTransaction) { + synchronized(SurfaceControl.class) { sGlobalTransaction.merge(t); } } @@ -826,6 +889,22 @@ public class SurfaceControl { } } + /** + * Sets the transform and position of a {@link SurfaceControl} from a 3x3 transformation matrix. + * + * @param matrix The matrix to apply. + * @param float9 An array of 9 floats to be used to extract the values from the matrix. + */ + public void setMatrix(Matrix matrix, float[] float9) { + checkNotReleased(); + matrix.getValues(float9); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setMatrix(this, float9[MSCALE_X], float9[MSKEW_Y], + float9[MSKEW_X], float9[MSCALE_Y]); + sGlobalTransaction.setPosition(this, float9[MTRANS_X], float9[MTRANS_Y]); + } + } + public void setWindowCrop(Rect crop) { checkNotReleased(); synchronized (SurfaceControl.class) { @@ -863,6 +942,18 @@ public class SurfaceControl { } } + public int getWidth() { + synchronized (mSizeLock) { + return mWidth; + } + } + + public int getHeight() { + synchronized (mSizeLock) { + return mHeight; + } + } + @Override public String toString() { return "Surface(name=" + mName + ")/@0x" + @@ -1090,7 +1181,9 @@ public class SurfaceControl { } /** - * Copy the current screen contents into a bitmap and return it. + * Copy the current screen contents into a hardware bitmap and return it. + * Note: If you want to modify the Bitmap in software, you will need to copy the Bitmap into + * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)} * * CAVEAT: Versions of screenshot that return a {@link Bitmap} can * be extremely slow; avoid use unless absolutely necessary; prefer @@ -1115,7 +1208,7 @@ public class SurfaceControl { * screenshots in its native portrait orientation by default, so this is * useful for returning screenshots that are independent of device * orientation. - * @return Returns a Bitmap containing the screen contents, or null + * @return Returns a hardware Bitmap containing the screen contents, or null * if an error occurs. Make sure to call Bitmap.recycle() as soon as * possible, once its content is not needed anymore. */ @@ -1143,23 +1236,36 @@ public class SurfaceControl { } /** - * Like {@link SurfaceControl#screenshot(int, int, int, int, boolean)} but - * includes all Surfaces in the screenshot. + * Like {@link SurfaceControl#screenshot(Rect, int, int, int, int, boolean, int)} but + * includes all Surfaces in the screenshot. This will also update the orientation so it + * sends the correct coordinates to SF based on the rotation value. * + * @param sourceCrop The portion of the screen to capture into the Bitmap; + * caller may pass in 'new Rect()' if no cropping is desired. * @param width The desired width of the returned bitmap; the raw * screen will be scaled down to this size. * @param height The desired height of the returned bitmap; the raw * screen will be scaled down to this size. + * @param rotation Apply a custom clockwise rotation to the screenshot, i.e. + * Surface.ROTATION_0,90,180,270. Surfaceflinger will always take + * screenshots in its native portrait orientation by default, so this is + * useful for returning screenshots that are independent of device + * orientation. * @return Returns a Bitmap containing the screen contents, or null * if an error occurs. Make sure to call Bitmap.recycle() as soon as * possible, once its content is not needed anymore. */ - public static Bitmap screenshot(int width, int height) { + public static Bitmap screenshot(Rect sourceCrop, int width, int height, int rotation) { // TODO: should take the display as a parameter IBinder displayToken = SurfaceControl.getBuiltInDisplay( SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true, - false, Surface.ROTATION_0); + if (rotation == ROTATION_90 || rotation == ROTATION_270) { + rotation = (rotation == ROTATION_90) ? ROTATION_270 : ROTATION_90; + } + + SurfaceControl.rotateCropForSF(sourceCrop, rotation); + return nativeScreenshot(displayToken, sourceCrop, width, height, 0, 0, true, + false, rotation); } private static void screenshot(IBinder display, Surface consumer, Rect sourceCrop, @@ -1175,26 +1281,29 @@ public class SurfaceControl { minLayer, maxLayer, allLayers, useIdentityTransform); } + private static void rotateCropForSF(Rect crop, int rot) { + if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + int tmp = crop.top; + crop.top = crop.left; + crop.left = tmp; + tmp = crop.right; + crop.right = crop.bottom; + crop.bottom = tmp; + } + } + /** - * Captures a layer and its children into the provided {@link Surface}. + * Captures a layer and its children and returns a {@link GraphicBuffer} with the content. * * @param layerHandleToken The root layer to capture. - * @param consumer The {@link Surface} to capture the layer into. * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new * Rect()' or null if no cropping is desired. * @param frameScale The desired scale of the returned buffer; the raw * screen will be scaled up/down. + * + * @return Returns a GraphicBuffer that contains the layer capture. */ - public static void captureLayers(IBinder layerHandleToken, Surface consumer, Rect sourceCrop, - float frameScale) { - nativeCaptureLayers(layerHandleToken, consumer, sourceCrop, frameScale); - } - - /** - * Same as {@link #captureLayers(IBinder, Surface, Rect, float)} except this - * captures to a {@link GraphicBuffer} instead of a {@link Surface}. - */ - public static GraphicBuffer captureLayersToBuffer(IBinder layerHandleToken, Rect sourceCrop, + public static GraphicBuffer captureLayers(IBinder layerHandleToken, Rect sourceCrop, float frameScale) { return nativeCaptureLayers(layerHandleToken, sourceCrop, frameScale); } @@ -1205,6 +1314,7 @@ public class SurfaceControl { nativeGetNativeTransactionFinalizer(), 512); private long mNativeObject; + private final ArrayMap mResizedSurfaces = new ArrayMap<>(); Runnable mFreeNativeResources; public Transaction() { @@ -1235,9 +1345,22 @@ public class SurfaceControl { * Jankier version of apply. Avoid use (b/28068298). */ public void apply(boolean sync) { + applyResizedSurfaces(); nativeApplyTransaction(mNativeObject, sync); } + private void applyResizedSurfaces() { + for (int i = mResizedSurfaces.size() - 1; i >= 0; i--) { + final Point size = mResizedSurfaces.valueAt(i); + final SurfaceControl surfaceControl = mResizedSurfaces.keyAt(i); + synchronized (surfaceControl.mSizeLock) { + surfaceControl.mWidth = size.x; + surfaceControl.mHeight = size.y; + } + } + mResizedSurfaces.clear(); + } + public Transaction show(SurfaceControl sc) { sc.checkNotReleased(); nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_HIDDEN); @@ -1258,6 +1381,7 @@ public class SurfaceControl { public Transaction setSize(SurfaceControl sc, int w, int h) { sc.checkNotReleased(); + mResizedSurfaces.put(sc, new Point(w, h)); nativeSetSize(mNativeObject, sc.mNativeObject, w, h); return this; } @@ -1296,6 +1420,14 @@ public class SurfaceControl { return this; } + public Transaction setMatrix(SurfaceControl sc, Matrix matrix, float[] float9) { + matrix.getValues(float9); + setMatrix(sc, float9[MSCALE_X], float9[MSKEW_Y], + float9[MSKEW_X], float9[MSCALE_Y]); + setPosition(sc, float9[MTRANS_X], float9[MTRANS_Y]); + return this; + } + public Transaction setWindowCrop(SurfaceControl sc, Rect crop) { sc.checkNotReleased(); if (crop != null) { @@ -1482,6 +1614,8 @@ public class SurfaceControl { * other transaction as if it had been applied. */ public Transaction merge(Transaction other) { + mResizedSurfaces.putAll(other.mResizedSurfaces); + other.mResizedSurfaces.clear(); nativeMergeTransaction(mNativeObject, other.mNativeObject); return this; } diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index 578679b1..ebb2af45 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,1215 +16,115 @@ package android.view; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; -import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER; -import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER; -import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER; +import com.android.layoutlib.bridge.MockView; import android.content.Context; -import android.content.res.CompatibilityInfo.Translator; -import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; -import android.os.Build; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.SystemClock; import android.util.AttributeSet; -import android.util.Log; - -import com.android.internal.view.SurfaceCallbackHelper; - -import java.util.ArrayList; -import java.util.concurrent.locks.ReentrantLock; /** - * Provides a dedicated drawing surface embedded inside of a view hierarchy. - * You can control the format of this surface and, if you like, its size; the - * SurfaceView takes care of placing the surface at the correct location on the - * screen - * - *

    The surface is Z ordered so that it is behind the window holding its - * SurfaceView; the SurfaceView punches a hole in its window to allow its - * surface to be displayed. The view hierarchy will take care of correctly - * compositing with the Surface any siblings of the SurfaceView that would - * normally appear on top of it. This can be used to place overlays such as - * buttons on top of the Surface, though note however that it can have an - * impact on performance since a full alpha-blended composite will be performed - * each time the Surface changes. - * - *

    The transparent region that makes the surface visible is based on the - * layout positions in the view hierarchy. If the post-layout transform - * properties are used to draw a sibling view on top of the SurfaceView, the - * view may not be properly composited with the surface. + * Mock version of the SurfaceView. + * Only non override public methods from the real SurfaceView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. * - *

    Access to the underlying surface is provided via the SurfaceHolder interface, - * which can be retrieved by calling {@link #getHolder}. + * TODO: generate automatically. * - *

    The Surface will be created for you while the SurfaceView's window is - * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} - * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the - * Surface is created and destroyed as the window is shown and hidden. - * - *

    One of the purposes of this class is to provide a surface in which a - * secondary thread can render into the screen. If you are going to use it - * this way, you need to be aware of some threading semantics: - * - *

      - *
    • All SurfaceView and - * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called - * from the thread running the SurfaceView's window (typically the main thread - * of the application). They thus need to correctly synchronize with any - * state that is also touched by the drawing thread. - *
    • You must ensure that the drawing thread only touches the underlying - * Surface while it is valid -- between - * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()} - * and - * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}. - *
    - * - *

    Note: Starting in platform version - * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is - * updated synchronously with other View rendering. This means that translating - * and scaling a SurfaceView on screen will not cause rendering artifacts. Such - * artifacts may occur on previous versions of the platform when its window is - * positioned asynchronously.

    */ -public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback { - private static final String TAG = "SurfaceView"; - private static final boolean DEBUG = false; - - final ArrayList mCallbacks - = new ArrayList(); - - final int[] mLocation = new int[2]; - - final ReentrantLock mSurfaceLock = new ReentrantLock(); - final Surface mSurface = new Surface(); // Current surface in use - boolean mDrawingStopped = true; - // We use this to track if the application has produced a frame - // in to the Surface. Up until that point, we should be careful not to punch - // holes. - boolean mDrawFinished = false; - - final Rect mScreenRect = new Rect(); - SurfaceSession mSurfaceSession; - - SurfaceControl mSurfaceControl; - // In the case of format changes we switch out the surface in-place - // we need to preserve the old one until the new one has drawn. - SurfaceControl mDeferredDestroySurfaceControl; - final Rect mTmpRect = new Rect(); - final Configuration mConfiguration = new Configuration(); - - int mSubLayer = APPLICATION_MEDIA_SUBLAYER; - - boolean mIsCreating = false; - private volatile boolean mRtHandlingPositionUpdates = false; - - private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener - = new ViewTreeObserver.OnScrollChangedListener() { - @Override - public void onScrollChanged() { - updateSurface(); - } - }; - - private final ViewTreeObserver.OnPreDrawListener mDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // reposition ourselves where the surface is - mHaveFrame = getWidth() > 0 && getHeight() > 0; - updateSurface(); - return true; - } - }; - - boolean mRequestedVisible = false; - boolean mWindowVisibility = false; - boolean mLastWindowVisibility = false; - boolean mViewVisibility = false; - boolean mWindowStopped = false; - - int mRequestedWidth = -1; - int mRequestedHeight = -1; - /* Set SurfaceView's format to 565 by default to maintain backward - * compatibility with applications assuming this format. - */ - int mRequestedFormat = PixelFormat.RGB_565; - - boolean mHaveFrame = false; - boolean mSurfaceCreated = false; - long mLastLockTime = 0; - - boolean mVisible = false; - int mWindowSpaceLeft = -1; - int mWindowSpaceTop = -1; - int mSurfaceWidth = -1; - int mSurfaceHeight = -1; - int mFormat = -1; - final Rect mSurfaceFrame = new Rect(); - int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; - private Translator mTranslator; - - private boolean mGlobalListenersAdded; - private boolean mAttachedToWindow; - - private int mSurfaceFlags = SurfaceControl.HIDDEN; - - private int mPendingReportDraws; +public class SurfaceView extends MockView { public SurfaceView(Context context) { this(context, null); } public SurfaceView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs , 0); } - public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + public SurfaceView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mRenderNode.requestPositionUpdates(this); - - setWillNotDraw(true); - } - - /** - * Return the SurfaceHolder providing access and control over this - * SurfaceView's underlying surface. - * - * @return SurfaceHolder The holder of the surface. - */ - public SurfaceHolder getHolder() { - return mSurfaceHolder; - } - - private void updateRequestedVisibility() { - mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped; - } - - /** @hide */ - @Override - public void windowStopped(boolean stopped) { - mWindowStopped = stopped; - updateRequestedVisibility(); - updateSurface(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - getViewRootImpl().addWindowStoppedCallback(this); - mWindowStopped = false; - - mViewVisibility = getVisibility() == VISIBLE; - updateRequestedVisibility(); - - mAttachedToWindow = true; - mParent.requestTransparentRegion(SurfaceView.this); - if (!mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnScrollChangedListener(mScrollChangedListener); - observer.addOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = true; - } - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mWindowVisibility = visibility == VISIBLE; - updateRequestedVisibility(); - updateSurface(); - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - mViewVisibility = visibility == VISIBLE; - boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped; - if (newRequestedVisible != mRequestedVisible) { - // our base class (View) invalidates the layout only when - // we go from/to the GONE state. However, SurfaceView needs - // to request a re-layout when the visibility changes at all. - // This is needed because the transparent region is computed - // as part of the layout phase, and it changes (obviously) when - // the visibility changes. - requestLayout(); - } - mRequestedVisible = newRequestedVisible; - updateSurface(); - } - - private void performDrawFinished() { - if (mPendingReportDraws > 0) { - mDrawFinished = true; - if (mAttachedToWindow) { - notifyDrawFinished(); - invalidate(); - } - } else { - Log.e(TAG, System.identityHashCode(this) + "finished drawing" - + " but no pending report draw (extra call" - + " to draw completion runnable?)"); - } - } - - void notifyDrawFinished() { - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot != null) { - viewRoot.pendingDrawFinished(); - } - mPendingReportDraws--; - } - - @Override - protected void onDetachedFromWindow() { - ViewRootImpl viewRoot = getViewRootImpl(); - // It's possible to create a SurfaceView using the default constructor and never - // attach it to a view hierarchy, this is a common use case when dealing with - // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage - // the lifecycle. Instead of attaching it to a view, he/she can just pass - // the SurfaceHolder forward, most live wallpapers do it. - if (viewRoot != null) { - viewRoot.removeWindowStoppedCallback(this); - } - - mAttachedToWindow = false; - if (mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnScrollChangedListener(mScrollChangedListener); - observer.removeOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = false; - } - - while (mPendingReportDraws > 0) { - notifyDrawFinished(); - } - - mRequestedVisible = false; - - updateSurface(); - if (mSurfaceControl != null) { - mSurfaceControl.destroy(); - } - mSurfaceControl = null; - - mHaveFrame = false; - - super.onDetachedFromWindow(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = mRequestedWidth >= 0 - ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0) - : getDefaultSize(0, widthMeasureSpec); - int height = mRequestedHeight >= 0 - ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0) - : getDefaultSize(0, heightMeasureSpec); - setMeasuredDimension(width, height); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - boolean result = super.setFrame(left, top, right, bottom); - updateSurface(); - return result; } - @Override public boolean gatherTransparentRegion(Region region) { - if (isAboveParent() || !mDrawFinished) { - return super.gatherTransparentRegion(region); - } - - boolean opaque = true; - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // this view draws, remove it from the transparent region - opaque = super.gatherTransparentRegion(region); - } else if (region != null) { - int w = getWidth(); - int h = getHeight(); - if (w>0 && h>0) { - getLocationInWindow(mLocation); - // otherwise, punch a hole in the whole hierarchy - int l = mLocation[0]; - int t = mLocation[1]; - region.op(l, t, l+w, t+h, Region.Op.UNION); - } - } - if (PixelFormat.formatHasAlpha(mRequestedFormat)) { - opaque = false; - } - return opaque; + return false; } - @Override - public void draw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.draw(canvas); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.dispatchDraw(canvas); - } - - /** - * Control whether the surface view's surface is placed on top of another - * regular surface view in the window (but still behind the window itself). - * This is typically used to place overlays on top of an underlying media - * surface view. - * - *

    Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - *

    Calling this overrides any previous call to {@link #setZOrderOnTop}. - */ public void setZOrderMediaOverlay(boolean isMediaOverlay) { - mSubLayer = isMediaOverlay - ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; } - /** - * Control whether the surface view's surface is placed on top of its - * window. Normally it is placed behind the window, to allow it to - * (for the most part) appear to composite with the views in the - * hierarchy. By setting this, you cause it to be placed above the - * window. This means that none of the contents of the window this - * SurfaceView is in will be visible on top of its surface. - * - *

    Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - *

    Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. - */ public void setZOrderOnTop(boolean onTop) { - if (onTop) { - mSubLayer = APPLICATION_PANEL_SUBLAYER; - } else { - mSubLayer = APPLICATION_MEDIA_SUBLAYER; - } } - /** - * Control whether the surface view's content should be treated as secure, - * preventing it from appearing in screenshots or from being viewed on - * non-secure displays. - * - *

    Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - *

    See {@link android.view.Display#FLAG_SECURE} for details. - * - * @param isSecure True if the surface view is secure. - */ public void setSecure(boolean isSecure) { - if (isSecure) { - mSurfaceFlags |= SurfaceControl.SECURE; - } else { - mSurfaceFlags &= ~SurfaceControl.SECURE; - } - } - - private void updateOpaqueFlag() { - if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { - mSurfaceFlags |= SurfaceControl.OPAQUE; - } else { - mSurfaceFlags &= ~SurfaceControl.OPAQUE; - } } - private Rect getParentSurfaceInsets() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } else { - return root.mWindowAttributes.surfaceInsets; - } - } - - /** @hide */ - protected void updateSurface() { - if (!mHaveFrame) { - return; - } - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) { - return; - } - - mTranslator = viewRoot.mTranslator; - if (mTranslator != null) { - mSurface.setCompatibilityTranslator(mTranslator); - } - - int myWidth = mRequestedWidth; - if (myWidth <= 0) myWidth = getWidth(); - int myHeight = mRequestedHeight; - if (myHeight <= 0) myHeight = getHeight(); - - final boolean formatChanged = mFormat != mRequestedFormat; - final boolean visibleChanged = mVisible != mRequestedVisible; - final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged) - && mRequestedVisible; - final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight; - final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility; - boolean redrawNeeded = false; - - if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) { - getLocationInWindow(mLocation); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Changes: creating=" + creating - + " format=" + formatChanged + " size=" + sizeChanged - + " visible=" + visibleChanged - + " left=" + (mWindowSpaceLeft != mLocation[0]) - + " top=" + (mWindowSpaceTop != mLocation[1])); - - try { - final boolean visible = mVisible = mRequestedVisible; - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - mSurfaceWidth = myWidth; - mSurfaceHeight = myHeight; - mFormat = mRequestedFormat; - mLastWindowVisibility = mWindowVisibility; - - mScreenRect.left = mWindowSpaceLeft; - mScreenRect.top = mWindowSpaceTop; - mScreenRect.right = mWindowSpaceLeft + getWidth(); - mScreenRect.bottom = mWindowSpaceTop + getHeight(); - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - final Rect surfaceInsets = getParentSurfaceInsets(); - mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); - - if (creating) { - mSurfaceSession = new SurfaceSession(viewRoot.mSurface); - mDeferredDestroySurfaceControl = mSurfaceControl; - - updateOpaqueFlag(); - final String name = "SurfaceView - " + viewRoot.getTitle().toString(); - - mSurfaceControl = new SurfaceControlWithBackground( - name, - (mSurfaceFlags & SurfaceControl.OPAQUE) != 0, - new SurfaceControl.Builder(mSurfaceSession) - .setSize(mSurfaceWidth, mSurfaceHeight) - .setFormat(mFormat) - .setFlags(mSurfaceFlags)); - } else if (mSurfaceControl == null) { - return; - } - - boolean realSizeChanged = false; - - mSurfaceLock.lock(); - try { - mDrawingStopped = !visible; - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Cur surface: " + mSurface); - - SurfaceControl.openTransaction(); - try { - mSurfaceControl.setLayer(mSubLayer); - if (mViewVisibility) { - mSurfaceControl.show(); - } else { - mSurfaceControl.hide(); - } - - // While creating the surface, we will set it's initial - // geometry. Outside of that though, we should generally - // leave it to the RenderThread. - // - // There is one more case when the buffer size changes we aren't yet - // prepared to sync (as even following the transaction applying - // we still need to latch a buffer). - // b/28866173 - if (sizeChanged || creating || !mRtHandlingPositionUpdates) { - mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top); - mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - mScreenRect.height() / (float) mSurfaceHeight); - } - if (sizeChanged) { - mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight); - } - } finally { - SurfaceControl.closeTransaction(); - } - - if (sizeChanged || creating) { - redrawNeeded = true; - } - - mSurfaceFrame.left = 0; - mSurfaceFrame.top = 0; - if (mTranslator == null) { - mSurfaceFrame.right = mSurfaceWidth; - mSurfaceFrame.bottom = mSurfaceHeight; - } else { - float appInvertedScale = mTranslator.applicationInvertedScale; - mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); - mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); - } - - final int surfaceWidth = mSurfaceFrame.right; - final int surfaceHeight = mSurfaceFrame.bottom; - realSizeChanged = mLastSurfaceWidth != surfaceWidth - || mLastSurfaceHeight != surfaceHeight; - mLastSurfaceWidth = surfaceWidth; - mLastSurfaceHeight = surfaceHeight; - } finally { - mSurfaceLock.unlock(); - } - - try { - redrawNeeded |= visible && !mDrawFinished; - - SurfaceHolder.Callback callbacks[] = null; - - final boolean surfaceChanged = creating; - if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) { - mSurfaceCreated = false; - if (mSurface.isValid()) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceDestroyed"); - callbacks = getSurfaceCallbacks(); - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceDestroyed(mSurfaceHolder); - } - // Since Android N the same surface may be reused and given to us - // again by the system server at a later point. However - // as we didn't do this in previous releases, clients weren't - // necessarily required to clean up properly in - // surfaceDestroyed. This leads to problems for example when - // clients don't destroy their EGL context, and try - // and create a new one on the same surface following reuse. - // Since there is no valid use of the surface in-between - // surfaceDestroyed and surfaceCreated, we force a disconnect, - // so the next connect will always work if we end up reusing - // the surface. - if (mSurface.isValid()) { - mSurface.forceScopedDisconnect(); - } - } - } - - if (creating) { - mSurface.copyFrom(mSurfaceControl); - } - - if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion - < Build.VERSION_CODES.O) { - // Some legacy applications use the underlying native {@link Surface} object - // as a key to whether anything has changed. In these cases, updates to the - // existing {@link Surface} will be ignored when the size changes. - // Therefore, we must explicitly recreate the {@link Surface} in these - // cases. - mSurface.createFrom(mSurfaceControl); - } - - if (visible && mSurface.isValid()) { - if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) { - mSurfaceCreated = true; - mIsCreating = true; - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceCreated"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceCreated(mSurfaceHolder); - } - } - if (creating || formatChanged || sizeChanged - || visibleChanged || realSizeChanged) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceChanged -- format=" + mFormat - + " w=" + myWidth + " h=" + myHeight); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); - } - } - if (redrawNeeded) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceRedrawNeeded"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - - mPendingReportDraws++; - viewRoot.drawPending(); - SurfaceCallbackHelper sch = - new SurfaceCallbackHelper(this::onDrawFinished); - sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks); - } - } - } finally { - mIsCreating = false; - if (mSurfaceControl != null && !mSurfaceCreated) { - mSurface.release(); - // If we are not in the stopped state, then the destruction of the Surface - // represents a visual change we need to display, and we should go ahead - // and destroy the SurfaceControl. However if we are in the stopped state, - // we can just leave the Surface around so it can be a part of animations, - // and we let the life-time be tied to the parent surface. - if (!mWindowStopped) { - mSurfaceControl.destroy(); - mSurfaceControl = null; - } - } - } - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - if (DEBUG) Log.v( - TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top - + " w=" + mScreenRect.width() + " h=" + mScreenRect.height() - + ", frame=" + mSurfaceFrame); - } else { - // Calculate the window position in case RT loses the window - // and we need to fallback to a UI-thread driven position update - getLocationInSurface(mLocation); - final boolean positionChanged = mWindowSpaceLeft != mLocation[0] - || mWindowSpaceTop != mLocation[1]; - final boolean layoutSizeChanged = getWidth() != mScreenRect.width() - || getHeight() != mScreenRect.height(); - if (positionChanged || layoutSizeChanged) { // Only the position has changed - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - // For our size changed check, we keep mScreenRect.width() and mScreenRect.height() - // in view local space. - mLocation[0] = getWidth(); - mLocation[1] = getHeight(); - - mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop, - mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]); - - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - if (mSurfaceControl == null) { - return; - } - - if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, -1); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - } - - private void onDrawFinished() { - if (DEBUG) { - Log.i(TAG, System.identityHashCode(this) + " " - + "finishedDrawing"); - } - - if (mDeferredDestroySurfaceControl != null) { - mDeferredDestroySurfaceControl.destroy(); - mDeferredDestroySurfaceControl = null; - } - - runOnUiThread(() -> { - performDrawFinished(); - }); - } - - private void setParentSpaceRectangle(Rect position, long frameNumber) { - ViewRootImpl viewRoot = getViewRootImpl(); - - SurfaceControl.openTransaction(); - try { - if (frameNumber > 0) { - mSurfaceControl.deferTransactionUntil(viewRoot.mSurface, frameNumber); - } - mSurfaceControl.setPosition(position.left, position.top); - mSurfaceControl.setMatrix(position.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - position.height() / (float) mSurfaceHeight); - } finally { - SurfaceControl.closeTransaction(); - } - } - - private Rect mRTLastReportedPosition = new Rect(); - - /** - * Called by native by a Rendering Worker thread to update the window position - * @hide - */ - public final void updateSurfacePosition_renderWorker(long frameNumber, - int left, int top, int right, int bottom) { - if (mSurfaceControl == null) { - return; - } - - // TODO: This is teensy bit racey in that a brand new SurfaceView moving on - // its 2nd frame if RenderThread is running slowly could potentially see - // this as false, enter the branch, get pre-empted, then this comes along - // and reports a new position, then the UI thread resumes and reports - // its position. This could therefore be de-sync'd in that interval, but - // the synchronization would violate the rule that RT must never block - // on the UI thread which would open up potential deadlocks. The risk of - // a single-frame desync is therefore preferable for now. - mRtHandlingPositionUpdates = true; - if (mRTLastReportedPosition.left == left - && mRTLastReportedPosition.top == top - && mRTLastReportedPosition.right == right - && mRTLastReportedPosition.bottom == bottom) { - return; - } - try { - if (DEBUG) { - Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - frameNumber, left, top, right, bottom)); - } - mRTLastReportedPosition.set(left, top, right, bottom); - setParentSpaceRectangle(mRTLastReportedPosition, frameNumber); - // Now overwrite mRTLastReportedPosition with our values - } catch (Exception ex) { - Log.e(TAG, "Exception from repositionChild", ex); - } - } - - /** - * Called by native on RenderThread to notify that the view is no longer in the - * draw tree. UI thread is blocked at this point. - * @hide - */ - public final void surfacePositionLost_uiRtSync(long frameNumber) { - if (DEBUG) { - Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d", - System.identityHashCode(this), frameNumber)); - } - mRTLastReportedPosition.setEmpty(); - - if (mSurfaceControl == null) { - return; - } - if (mRtHandlingPositionUpdates) { - mRtHandlingPositionUpdates = false; - // This callback will happen while the UI thread is blocked, so we can - // safely access other member variables at this time. - // So do what the UI thread would have done if RT wasn't handling position - // updates. - if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, frameNumber); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - - private SurfaceHolder.Callback[] getSurfaceCallbacks() { - SurfaceHolder.Callback callbacks[]; - synchronized (mCallbacks) { - callbacks = new SurfaceHolder.Callback[mCallbacks.size()]; - mCallbacks.toArray(callbacks); - } - return callbacks; - } - - /** - * This method still exists only for compatibility reasons because some applications have relied - * on this method via reflection. See Issue 36345857 for details. - * - * @deprecated No platform code is using this method anymore. - * @hide - */ - @Deprecated - public void setWindowType(int type) { - if (getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) { - throw new UnsupportedOperationException( - "SurfaceView#setWindowType() has never been a public API."); - } - - if (type == TYPE_APPLICATION_PANEL) { - Log.e(TAG, "If you are calling SurfaceView#setWindowType(TYPE_APPLICATION_PANEL) " - + "just to make the SurfaceView to be placed on top of its window, you must " - + "call setZOrderOnTop(true) instead.", new Throwable()); - setZOrderOnTop(true); - return; - } - Log.e(TAG, "SurfaceView#setWindowType(int) is deprecated and now does nothing. " - + "type=" + type, new Throwable()); - } - - private void runOnUiThread(Runnable runnable) { - Handler handler = getHandler(); - if (handler != null && handler.getLooper() != Looper.myLooper()) { - handler.post(runnable); - } else { - runnable.run(); - } - } - - /** - * Check to see if the surface has fixed size dimensions or if the surface's - * dimensions are dimensions are dependent on its current layout. - * - * @return true if the surface has dimensions that are fixed in size - * @hide - */ - public boolean isFixedSize() { - return (mRequestedWidth != -1 || mRequestedHeight != -1); - } - - private boolean isAboveParent() { - return mSubLayer >= 0; + public SurfaceHolder getHolder() { + return mSurfaceHolder; } - private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { - private static final String LOG_TAG = "SurfaceHolder"; + private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { @Override public boolean isCreating() { - return mIsCreating; + return false; } @Override public void addCallback(Callback callback) { - synchronized (mCallbacks) { - // This is a linear search, but in practice we'll - // have only a couple callbacks, so it doesn't matter. - if (mCallbacks.contains(callback) == false) { - mCallbacks.add(callback); - } - } } @Override public void removeCallback(Callback callback) { - synchronized (mCallbacks) { - mCallbacks.remove(callback); - } } @Override public void setFixedSize(int width, int height) { - if (mRequestedWidth != width || mRequestedHeight != height) { - mRequestedWidth = width; - mRequestedHeight = height; - requestLayout(); - } } @Override public void setSizeFromLayout() { - if (mRequestedWidth != -1 || mRequestedHeight != -1) { - mRequestedWidth = mRequestedHeight = -1; - requestLayout(); - } } @Override public void setFormat(int format) { - // for backward compatibility reason, OPAQUE always - // means 565 for SurfaceView - if (format == PixelFormat.OPAQUE) - format = PixelFormat.RGB_565; - - mRequestedFormat = format; - if (mSurfaceControl != null) { - updateSurface(); - } } - /** - * @deprecated setType is now ignored. - */ @Override - @Deprecated - public void setType(int type) { } + public void setType(int type) { + } @Override public void setKeepScreenOn(boolean screenOn) { - runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn)); } - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * The caller must redraw the entire surface. - * @return A canvas for drawing into the surface. - */ @Override public Canvas lockCanvas() { - return internalLockCanvas(null, false); - } - - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * @param inOutDirty A rectangle that represents the dirty region that the caller wants - * to redraw. This function may choose to expand the dirty rectangle if for example - * the surface has been resized or if the previous contents of the surface were - * not available. The caller must redraw the entire dirty region as represented - * by the contents of the inOutDirty rectangle upon return from this function. - * The caller may also pass null instead, in the case where the - * entire surface should be redrawn. - * @return A canvas for drawing into the surface. - */ - @Override - public Canvas lockCanvas(Rect inOutDirty) { - return internalLockCanvas(inOutDirty, false); + return null; } @Override - public Canvas lockHardwareCanvas() { - return internalLockCanvas(null, true); - } - - private Canvas internalLockCanvas(Rect dirty, boolean hardware) { - mSurfaceLock.lock(); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped=" - + mDrawingStopped + ", surfaceControl=" + mSurfaceControl); - - Canvas c = null; - if (!mDrawingStopped && mSurfaceControl != null) { - try { - if (hardware) { - c = mSurface.lockHardwareCanvas(); - } else { - c = mSurface.lockCanvas(dirty); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Exception locking surface", e); - } - } - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c); - if (c != null) { - mLastLockTime = SystemClock.uptimeMillis(); - return c; - } - - // If the Surface is not ready to be drawn, then return null, - // but throttle calls to this function so it isn't called more - // than every 100ms. - long now = SystemClock.uptimeMillis(); - long nextTime = mLastLockTime + 100; - if (nextTime > now) { - try { - Thread.sleep(nextTime-now); - } catch (InterruptedException e) { - } - now = SystemClock.uptimeMillis(); - } - mLastLockTime = now; - mSurfaceLock.unlock(); - + public Canvas lockCanvas(Rect dirty) { return null; } - /** - * Posts the new contents of the {@link Canvas} to the surface and - * releases the {@link Canvas}. - * - * @param canvas The canvas previously obtained from {@link #lockCanvas}. - */ @Override public void unlockCanvasAndPost(Canvas canvas) { - mSurface.unlockCanvasAndPost(canvas); - mSurfaceLock.unlock(); } @Override public Surface getSurface() { - return mSurface; + return null; } @Override public Rect getSurfaceFrame() { - return mSurfaceFrame; + return null; } }; - - class SurfaceControlWithBackground extends SurfaceControl { - private SurfaceControl mBackgroundControl; - private boolean mOpaque = true; - public boolean mVisible = false; - - public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b) - throws Exception { - super(b.setName(name).build()); - - mBackgroundControl = b.setName("Background for -" + name) - .setFormat(OPAQUE) - .setColorLayer(true) - .build(); - mOpaque = opaque; - } - - @Override - public void setAlpha(float alpha) { - super.setAlpha(alpha); - mBackgroundControl.setAlpha(alpha); - } - - @Override - public void setLayer(int zorder) { - super.setLayer(zorder); - // -3 is below all other child layers as SurfaceView never goes below -2 - mBackgroundControl.setLayer(-3); - } - - @Override - public void setPosition(float x, float y) { - super.setPosition(x, y); - mBackgroundControl.setPosition(x, y); - } - - @Override - public void setSize(int w, int h) { - super.setSize(w, h); - mBackgroundControl.setSize(w, h); - } - - @Override - public void setWindowCrop(Rect crop) { - super.setWindowCrop(crop); - mBackgroundControl.setWindowCrop(crop); - } - - @Override - public void setFinalCrop(Rect crop) { - super.setFinalCrop(crop); - mBackgroundControl.setFinalCrop(crop); - } - - @Override - public void setLayerStack(int layerStack) { - super.setLayerStack(layerStack); - mBackgroundControl.setLayerStack(layerStack); - } - - @Override - public void setOpaque(boolean isOpaque) { - super.setOpaque(isOpaque); - mOpaque = isOpaque; - updateBackgroundVisibility(); - } - - @Override - public void setSecure(boolean isSecure) { - super.setSecure(isSecure); - } - - @Override - public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { - super.setMatrix(dsdx, dtdx, dsdy, dtdy); - mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy); - } - - @Override - public void hide() { - super.hide(); - mVisible = false; - updateBackgroundVisibility(); - } - - @Override - public void show() { - super.show(); - mVisible = true; - updateBackgroundVisibility(); - } - - @Override - public void destroy() { - super.destroy(); - mBackgroundControl.destroy(); - } - - @Override - public void release() { - super.release(); - mBackgroundControl.release(); - } - - @Override - public void setTransparentRegionHint(Region region) { - super.setTransparentRegionHint(region); - mBackgroundControl.setTransparentRegionHint(region); - } - - @Override - public void deferTransactionUntil(IBinder handle, long frame) { - super.deferTransactionUntil(handle, frame); - mBackgroundControl.deferTransactionUntil(handle, frame); - } - - @Override - public void deferTransactionUntil(Surface barrier, long frame) { - super.deferTransactionUntil(barrier, frame); - mBackgroundControl.deferTransactionUntil(barrier, frame); - } - - void updateBackgroundVisibility() { - if (mOpaque && mVisible) { - mBackgroundControl.show(); - } else { - mBackgroundControl.hide(); - } - } - } } + diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java index 7c76bab2..6a8f8b12 100644 --- a/android/view/ThreadedRenderer.java +++ b/android/view/ThreadedRenderer.java @@ -190,6 +190,17 @@ public final class ThreadedRenderer { public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY = "debug.hwui.show_non_rect_clip"; + /** + * Sets the FPS devisor to lower the FPS. + * + * Sets a positive integer as a divisor. 1 (the default value) menas the full FPS, and 2 + * means half the full FPS. + * + * + * @hide + */ + public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor"; + static { // Try to check OpenGL support early if possible. isAvailable(); @@ -333,8 +344,10 @@ public final class ThreadedRenderer { private static final int FLAG_DUMP_FRAMESTATS = 1 << 0; private static final int FLAG_DUMP_RESET = 1 << 1; - @IntDef(flag = true, value = { - FLAG_DUMP_FRAMESTATS, FLAG_DUMP_RESET }) + @IntDef(flag = true, prefix = { "FLAG_DUMP_" }, value = { + FLAG_DUMP_FRAMESTATS, + FLAG_DUMP_RESET + }) @Retention(RetentionPolicy.SOURCE) public @interface DumpFlags {} @@ -955,6 +968,9 @@ public final class ThreadedRenderer { if (mInitialized) return; mInitialized = true; mAppContext = context.getApplicationContext(); + + // b/68769804: For low FPS experiments. + setFPSDivisor(SystemProperties.getInt(DEBUG_FPS_DIVISOR, 1)); initSched(renderProxy); initGraphicsStats(); } @@ -1007,6 +1023,13 @@ public final class ThreadedRenderer { observer.mNative = null; } + /** b/68769804: For low FPS experiments. */ + public static void setFPSDivisor(int divisor) { + if (divisor <= 0) divisor = 1; + Choreographer.getInstance().setFPSDivisor(divisor); + nHackySetRTAnimationsEnabled(divisor == 1); + } + /** Not actually public - internal use only. This doc to make lint happy */ public static native void disableVsync(); @@ -1075,4 +1098,6 @@ public final class ThreadedRenderer { private static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height); private static native void nSetHighContrastText(boolean enabled); + // For temporary experimentation b/66945974 + private static native void nHackySetRTAnimationsEnabled(boolean enabled); } diff --git a/android/view/View.java b/android/view/View.java index 0525ab16..cc63a62c 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -893,6 +893,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static boolean sAutoFocusableOffUIThreadWontNotifyParents; + /** + * Prior to P things like setScaleX() allowed passing float values that were bogus such as + * Float.NaN. If the app is targetting P or later then passing these values will result in an + * exception being thrown. If the app is targetting an earlier SDK version, then we will + * silently clamp these values to avoid crashes elsewhere when the rendering code hits + * these bogus values. + */ + private static boolean sThrowOnInvalidFloatProperties; + /** @hide */ @IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO}) @Retention(RetentionPolicy.SOURCE) @@ -1169,7 +1178,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private AutofillId mAutofillId; /** @hide */ - @IntDef({ + @IntDef(prefix = { "AUTOFILL_TYPE_" }, value = { AUTOFILL_TYPE_NONE, AUTOFILL_TYPE_TEXT, AUTOFILL_TYPE_TOGGLE, @@ -1240,7 +1249,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int AUTOFILL_TYPE_DATE = 4; /** @hide */ - @IntDef({ + @IntDef(prefix = { "IMPORTANT_FOR_AUTOFILL_" }, value = { IMPORTANT_FOR_AUTOFILL_AUTO, IMPORTANT_FOR_AUTOFILL_YES, IMPORTANT_FOR_AUTOFILL_NO, @@ -1291,9 +1300,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS = 0x8; /** @hide */ - @IntDef( - flag = true, - value = {AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS}) + @IntDef(flag = true, prefix = { "AUTOFILL_FLAG_" }, value = { + AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + }) @Retention(RetentionPolicy.SOURCE) public @interface AutofillFlags {} @@ -1443,7 +1452,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH, DRAWING_CACHE_QUALITY_AUTO}) + @IntDef(prefix = { "DRAWING_CACHE_QUALITY_" }, value = { + DRAWING_CACHE_QUALITY_LOW, + DRAWING_CACHE_QUALITY_HIGH, + DRAWING_CACHE_QUALITY_AUTO + }) public @interface DrawingCacheQuality {} /** @@ -1542,13 +1555,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int CONTEXT_CLICKABLE = 0x00800000; - /** @hide */ - @IntDef({ - SCROLLBARS_INSIDE_OVERLAY, - SCROLLBARS_INSIDE_INSET, - SCROLLBARS_OUTSIDE_OVERLAY, - SCROLLBARS_OUTSIDE_INSET + @IntDef(prefix = { "SCROLLBARS_" }, value = { + SCROLLBARS_INSIDE_OVERLAY, + SCROLLBARS_INSIDE_INSET, + SCROLLBARS_OUTSIDE_OVERLAY, + SCROLLBARS_OUTSIDE_INSET }) @Retention(RetentionPolicy.SOURCE) public @interface ScrollBarStyle {} @@ -1642,11 +1654,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int TOOLTIP = 0x40000000; /** @hide */ - @IntDef(flag = true, - value = { - FOCUSABLES_ALL, - FOCUSABLES_TOUCH_MODE - }) + @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = { + FOCUSABLES_ALL, + FOCUSABLES_TOUCH_MODE + }) @Retention(RetentionPolicy.SOURCE) public @interface FocusableMode {} @@ -1663,7 +1674,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; /** @hide */ - @IntDef({ + @IntDef(prefix = { "FOCUS_" }, value = { FOCUS_BACKWARD, FOCUS_FORWARD, FOCUS_LEFT, @@ -1675,7 +1686,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public @interface FocusDirection {} /** @hide */ - @IntDef({ + @IntDef(prefix = { "FOCUS_" }, value = { FOCUS_LEFT, FOCUS_UP, FOCUS_RIGHT, @@ -2417,20 +2428,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int PFLAG2_DRAG_HOVERED = 0x00000002; /** @hide */ - @IntDef({ - LAYOUT_DIRECTION_LTR, - LAYOUT_DIRECTION_RTL, - LAYOUT_DIRECTION_INHERIT, - LAYOUT_DIRECTION_LOCALE + @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = { + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, + LAYOUT_DIRECTION_INHERIT, + LAYOUT_DIRECTION_LOCALE }) @Retention(RetentionPolicy.SOURCE) // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection public @interface LayoutDir {} /** @hide */ - @IntDef({ - LAYOUT_DIRECTION_LTR, - LAYOUT_DIRECTION_RTL + @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = { + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL }) @Retention(RetentionPolicy.SOURCE) public @interface ResolvedLayoutDir {} @@ -2636,14 +2647,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT; /** @hide */ - @IntDef({ - TEXT_ALIGNMENT_INHERIT, - TEXT_ALIGNMENT_GRAVITY, - TEXT_ALIGNMENT_CENTER, - TEXT_ALIGNMENT_TEXT_START, - TEXT_ALIGNMENT_TEXT_END, - TEXT_ALIGNMENT_VIEW_START, - TEXT_ALIGNMENT_VIEW_END + @IntDef(prefix = { "TEXT_ALIGNMENT_" }, value = { + TEXT_ALIGNMENT_INHERIT, + TEXT_ALIGNMENT_GRAVITY, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_TEXT_START, + TEXT_ALIGNMENT_TEXT_END, + TEXT_ALIGNMENT_VIEW_START, + TEXT_ALIGNMENT_VIEW_END }) @Retention(RetentionPolicy.SOURCE) public @interface TextAlignment {} @@ -3040,15 +3051,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = { - SCROLL_INDICATOR_TOP, - SCROLL_INDICATOR_BOTTOM, - SCROLL_INDICATOR_LEFT, - SCROLL_INDICATOR_RIGHT, - SCROLL_INDICATOR_START, - SCROLL_INDICATOR_END, - }) + @IntDef(flag = true, prefix = { "SCROLL_INDICATOR_" }, value = { + SCROLL_INDICATOR_TOP, + SCROLL_INDICATOR_BOTTOM, + SCROLL_INDICATOR_LEFT, + SCROLL_INDICATOR_RIGHT, + SCROLL_INDICATOR_START, + SCROLL_INDICATOR_END, + }) public @interface ScrollIndicators {} /** @@ -3674,8 +3684,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; /** @hide */ - @IntDef(flag = true, - value = { FIND_VIEWS_WITH_TEXT, FIND_VIEWS_WITH_CONTENT_DESCRIPTION }) + @IntDef(flag = true, prefix = { "FIND_VIEWS_" }, value = { + FIND_VIEWS_WITH_TEXT, + FIND_VIEWS_WITH_CONTENT_DESCRIPTION + }) @Retention(RetentionPolicy.SOURCE) public @interface FindViewFlags {} @@ -4287,6 +4299,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ Runnable mShowTooltipRunnable; Runnable mHideTooltipRunnable; + + /** + * Hover move is ignored if it is within this distance in pixels from the previous one. + */ + int mHoverSlop; + + /** + * Update the anchor position if it significantly (that is by at least mHoverSlop) + * different from the previously stored position. Ignoring insignificant changes + * filters out the jitter which is typical for such input sources as stylus. + * + * @return True if the position has been updated. + */ + private boolean updateAnchorPos(MotionEvent event) { + final int newAnchorX = (int) event.getX(); + final int newAnchorY = (int) event.getY(); + if (Math.abs(newAnchorX - mAnchorX) <= mHoverSlop + && Math.abs(newAnchorY - mAnchorY) <= mHoverSlop) { + return false; + } + mAnchorX = newAnchorX; + mAnchorY = newAnchorY; + return true; + } + + /** + * Clear the anchor position to ensure that the next change is considered significant. + */ + private void clearAnchorPos() { + mAnchorX = Integer.MAX_VALUE; + mAnchorY = Integer.MAX_VALUE; + } } TooltipInfo mTooltipInfo; @@ -4752,6 +4796,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sUseDefaultFocusHighlight = context.getResources().getBoolean( com.android.internal.R.bool.config_useDefaultFocusHighlight); + sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P; + sCompatibilityDone = true; } } @@ -7208,8 +7254,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param text The announcement text. */ public void announceForAccessibility(CharSequence text) { - if (AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_ANNOUNCEMENT) && mParent != null) { + if (AccessibilityManager.getInstance(mContext).isEnabled() && mParent != null) { AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_ANNOUNCEMENT); onInitializeAccessibilityEvent(event); @@ -10968,8 +11013,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0) { mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_FOCUSED; invalidate(); - if (AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); event.setAction(action); @@ -11794,8 +11838,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private void sendViewTextTraversedAtGranularityEvent(int action, int granularity, int fromIndex, int toIndex) { - if (mParent == null || !AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY)) { + if (mParent == null) { return; } AccessibilityEvent event = AccessibilityEvent.obtain( @@ -14275,7 +14318,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleX(float scaleX) { if (scaleX != getScaleX()) { - requireIsFinite(scaleX, "scaleX"); + scaleX = sanitizeFloatPropertyValue(scaleX, "scaleX"); invalidateViewProperty(true, false); mRenderNode.setScaleX(scaleX); invalidateViewProperty(false, true); @@ -14312,7 +14355,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleY(float scaleY) { if (scaleY != getScaleY()) { - requireIsFinite(scaleY, "scaleY"); + scaleY = sanitizeFloatPropertyValue(scaleY, "scaleY"); invalidateViewProperty(true, false); mRenderNode.setScaleY(scaleY); invalidateViewProperty(false, true); @@ -14862,13 +14905,41 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - private static void requireIsFinite(float transform, String propertyName) { - if (Float.isNaN(transform)) { - throw new IllegalArgumentException("Cannot set '" + propertyName + "' to Float.NaN"); + private static float sanitizeFloatPropertyValue(float value, String propertyName) { + return sanitizeFloatPropertyValue(value, propertyName, -Float.MAX_VALUE, Float.MAX_VALUE); + } + + private static float sanitizeFloatPropertyValue(float value, String propertyName, + float min, float max) { + // The expected "nothing bad happened" path + if (value >= min && value <= max) return value; + + if (value < min || value == Float.NEGATIVE_INFINITY) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to " + + value + ", the value must be >= " + min); + } + return min; + } + + if (value > max || value == Float.POSITIVE_INFINITY) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to " + + value + ", the value must be <= " + max); + } + return max; } - if (Float.isInfinite(transform)) { - throw new IllegalArgumentException("Cannot set '" + propertyName + "' to infinity"); + + if (Float.isNaN(value)) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException( + "Cannot set '" + propertyName + "' to Float.NaN"); + } + return 0; // Unclear which direction this NaN went so... 0? } + + // Shouldn't be possible to reach this. + throw new IllegalStateException("How do you get here?? " + value); } /** @@ -14957,7 +15028,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setElevation(float elevation) { if (elevation != getElevation()) { - requireIsFinite(elevation, "elevation"); + elevation = sanitizeFloatPropertyValue(elevation, "elevation"); invalidateViewProperty(true, false); mRenderNode.setElevation(elevation); invalidateViewProperty(false, true); @@ -15050,7 +15121,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setTranslationZ(float translationZ) { if (translationZ != getTranslationZ()) { - requireIsFinite(translationZ, "translationZ"); + translationZ = sanitizeFloatPropertyValue(translationZ, "translationZ"); invalidateViewProperty(true, false); mRenderNode.setTranslationZ(translationZ); invalidateViewProperty(false, true); @@ -25721,6 +25792,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ final Rect mStableInsets = new Rect(); + final DisplayCutout.ParcelableWrapper mDisplayCutout = + new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT); + /** * For windows that include areas that are not covered by real surface these are the outsets * for real surface. @@ -26185,8 +26259,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Override public void run() { - if (AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_VIEW_SCROLLED)) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_VIEW_SCROLLED); event.setScrollDeltaX(mDeltaX); @@ -26775,6 +26848,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTooltipInfo = new TooltipInfo(); mTooltipInfo.mShowTooltipRunnable = this::showHoverTooltip; mTooltipInfo.mHideTooltipRunnable = this::hideTooltip; + mTooltipInfo.mHoverSlop = ViewConfiguration.get(mContext).getScaledHoverSlop(); + mTooltipInfo.clearAnchorPos(); } mTooltipInfo.mTooltipText = tooltipText; } @@ -26815,7 +26890,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mAttachInfo == null || mTooltipInfo == null) { return false; } - if ((mViewFlags & ENABLED_MASK) != ENABLED) { + if (fromLongClick && (mViewFlags & ENABLED_MASK) != ENABLED) { return false; } if (TextUtils.isEmpty(mTooltipInfo.mTooltipText)) { @@ -26841,6 +26916,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTooltipInfo.mTooltipPopup.hide(); mTooltipInfo.mTooltipPopup = null; mTooltipInfo.mTooltipFromLongClick = false; + mTooltipInfo.clearAnchorPos(); if (mAttachInfo != null) { mAttachInfo.mTooltipHost = null; } @@ -26862,14 +26938,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } switch(event.getAction()) { case MotionEvent.ACTION_HOVER_MOVE: - if ((mViewFlags & TOOLTIP) != TOOLTIP || (mViewFlags & ENABLED_MASK) != ENABLED) { + if ((mViewFlags & TOOLTIP) != TOOLTIP) { break; } - if (!mTooltipInfo.mTooltipFromLongClick) { + if (!mTooltipInfo.mTooltipFromLongClick && mTooltipInfo.updateAnchorPos(event)) { if (mTooltipInfo.mTooltipPopup == null) { // Schedule showing the tooltip after a timeout. - mTooltipInfo.mAnchorX = (int) event.getX(); - mTooltipInfo.mAnchorY = (int) event.getY(); removeCallbacks(mTooltipInfo.mShowTooltipRunnable); postDelayed(mTooltipInfo.mShowTooltipRunnable, ViewConfiguration.getHoverTooltipShowTimeout()); @@ -26891,6 +26965,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return true; case MotionEvent.ACTION_HOVER_EXIT: + mTooltipInfo.clearAnchorPos(); if (!mTooltipInfo.mTooltipFromLongClick) { hideTooltip(); } diff --git a/android/view/ViewConfiguration.java b/android/view/ViewConfiguration.java index c44c8dda..c5a94daa 100644 --- a/android/view/ViewConfiguration.java +++ b/android/view/ViewConfiguration.java @@ -290,6 +290,7 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; + private final int mHoverSlop; private final int mMinScrollbarTouchTarget; private final int mDoubleTapTouchSlop; private final int mPagingTouchSlop; @@ -320,6 +321,7 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; + mHoverSlop = TOUCH_SLOP / 2; mMinScrollbarTouchTarget = MIN_SCROLLBAR_TOUCH_TARGET; mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; @@ -407,6 +409,8 @@ public class ViewConfiguration { com.android.internal.R.bool.config_ui_enableFadingMarquee); mTouchSlop = res.getDimensionPixelSize( com.android.internal.R.dimen.config_viewConfigurationTouchSlop); + mHoverSlop = res.getDimensionPixelSize( + com.android.internal.R.dimen.config_viewConfigurationHoverSlop); mMinScrollbarTouchTarget = res.getDimensionPixelSize( com.android.internal.R.dimen.config_minScrollbarTouchTarget); mPagingTouchSlop = mTouchSlop * 2; @@ -639,6 +643,14 @@ public class ViewConfiguration { return mTouchSlop; } + /** + * @return Distance in pixels a hover can wander while it is still considered "stationary". + * + */ + public int getScaledHoverSlop() { + return mHoverSlop; + } + /** * @return Distance in pixels the first touch can wander before we do not consider this a * potential double tap event diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 1c9d8639..6c5091c2 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -20,6 +20,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; +import static android.view.WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; @@ -142,10 +143,11 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV; /** - * Set to false if we do not want to use the multi threaded renderer. Note that by disabling + * Set to false if we do not want to use the multi threaded renderer even though + * threaded renderer (aka hardware renderering) is used. Note that by disabling * this, WindowCallbacks will not fire. */ - private static final boolean USE_MT_RENDERER = true; + private static final boolean MT_RENDERER_AVAILABLE = true; /** * Set this system property to true to force the view hierarchy to render @@ -302,6 +304,7 @@ public final class ViewRootImpl implements ViewParent, Rect mDirty; public boolean mIsAnimating; + private boolean mUseMTRenderer; private boolean mDragResizing; private boolean mInvalidateRootRequested; private int mResizeMode; @@ -321,6 +324,15 @@ public final class ViewRootImpl implements ViewParent, final Rect mTempRect; // used in the transaction to not thrash the heap. final Rect mVisRect; // used to retrieve visible rect of focused view. + // This is used to reduce the race between window focus changes being dispatched from + // the window manager and input events coming through the input system. + @GuardedBy("this") + boolean mWindowFocusChanged; + @GuardedBy("this") + boolean mUpcomingWindowFocus; + @GuardedBy("this") + boolean mUpcomingInTouchMode; + public boolean mTraversalScheduled; int mTraversalBarrier; boolean mWillDrawSoon; @@ -384,12 +396,15 @@ public final class ViewRootImpl implements ViewParent, final Rect mPendingContentInsets = new Rect(); final Rect mPendingOutsets = new Rect(); final Rect mPendingBackDropFrame = new Rect(); + final DisplayCutout.ParcelableWrapper mPendingDisplayCutout = + new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT); boolean mPendingAlwaysConsumeNavBar; final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets = new ViewTreeObserver.InternalInsetsInfo(); final Rect mDispatchContentInsets = new Rect(); final Rect mDispatchStableInsets = new Rect(); + DisplayCutout mDispatchDisplayCutout = DisplayCutout.NO_CUTOUT; private WindowInsets mLastWindowInsets; @@ -545,18 +560,14 @@ public final class ViewRootImpl implements ViewParent, } public void addWindowCallbacks(WindowCallbacks callback) { - if (USE_MT_RENDERER) { - synchronized (mWindowCallbacks) { - mWindowCallbacks.add(callback); - } + synchronized (mWindowCallbacks) { + mWindowCallbacks.add(callback); } } public void removeWindowCallbacks(WindowCallbacks callback) { - if (USE_MT_RENDERER) { - synchronized (mWindowCallbacks) { - mWindowCallbacks.remove(callback); - } + synchronized (mWindowCallbacks) { + mWindowCallbacks.remove(callback); } } @@ -682,7 +693,17 @@ public final class ViewRootImpl implements ViewParent, // If the application owns the surface, don't enable hardware acceleration if (mSurfaceHolder == null) { + // While this is supposed to enable only, it can effectively disable + // the acceleration too. enableHardwareAcceleration(attrs); + final boolean useMTRenderer = MT_RENDERER_AVAILABLE + && mAttachInfo.mThreadedRenderer != null; + if (mUseMTRenderer != useMTRenderer) { + // Shouldn't be resizing, as it's done only in window setup, + // but end just in case. + endDragResizing(); + mUseMTRenderer = useMTRenderer; + } } boolean restore = false; @@ -730,7 +751,7 @@ public final class ViewRootImpl implements ViewParent, res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, - mAttachInfo.mOutsets, mInputChannel); + mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; @@ -752,6 +773,7 @@ public final class ViewRootImpl implements ViewParent, mPendingOverscanInsets.set(0, 0, 0, 0); mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingStableInsets.set(mAttachInfo.mStableInsets); + mPendingDisplayCutout.set(mAttachInfo.mDisplayCutout); mPendingVisibleInsets.set(0, 0, 0, 0); mAttachInfo.mAlwaysConsumeNavBar = (res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR) != 0; @@ -1544,15 +1566,20 @@ public final class ViewRootImpl implements ViewParent, if (mLastWindowInsets == null || forceConstruct) { mDispatchContentInsets.set(mAttachInfo.mContentInsets); mDispatchStableInsets.set(mAttachInfo.mStableInsets); + mDispatchDisplayCutout = mAttachInfo.mDisplayCutout.get(); + Rect contentInsets = mDispatchContentInsets; Rect stableInsets = mDispatchStableInsets; + DisplayCutout displayCutout = mDispatchDisplayCutout; // For dispatch we preserve old logic, but for direct requests from Views we allow to // immediately use pending insets. if (!forceConstruct && (!mPendingContentInsets.equals(contentInsets) || - !mPendingStableInsets.equals(stableInsets))) { + !mPendingStableInsets.equals(stableInsets) || + !mPendingDisplayCutout.get().equals(displayCutout))) { contentInsets = mPendingContentInsets; stableInsets = mPendingStableInsets; + displayCutout = mPendingDisplayCutout.get(); } Rect outsets = mAttachInfo.mOutsets; if (outsets.left > 0 || outsets.top > 0 || outsets.right > 0 || outsets.bottom > 0) { @@ -1563,13 +1590,21 @@ public final class ViewRootImpl implements ViewParent, mLastWindowInsets = new WindowInsets(contentInsets, null /* windowDecorInsets */, stableInsets, mContext.getResources().getConfiguration().isScreenRound(), - mAttachInfo.mAlwaysConsumeNavBar, null /* displayCutout */); + mAttachInfo.mAlwaysConsumeNavBar, displayCutout); } return mLastWindowInsets; } void dispatchApplyInsets(View host) { - host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */)); + WindowInsets insets = getWindowInsets(true /* forceConstruct */); + final boolean layoutInCutout = + (mWindowAttributes.flags2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0; + if (!layoutInCutout) { + // Window is either not laid out in cutout or the status bar inset takes care of + // clearing the cutout, so we don't need to dispatch the cutout to the hierarchy. + insets = insets.consumeDisplayCutout(); + } + host.dispatchApplyWindowInsets(insets); } private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) { @@ -1730,6 +1765,9 @@ public final class ViewRootImpl implements ViewParent, if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) { insetsChanged = true; } + if (!mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout)) { + insetsChanged = true; + } if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: " @@ -1906,7 +1944,8 @@ public final class ViewRootImpl implements ViewParent, + " overscan=" + mPendingOverscanInsets.toShortString() + " content=" + mPendingContentInsets.toShortString() + " visible=" + mPendingVisibleInsets.toShortString() - + " visible=" + mPendingStableInsets.toShortString() + + " stable=" + mPendingStableInsets.toShortString() + + " cutout=" + mPendingDisplayCutout.get().toString() + " outsets=" + mPendingOutsets.toShortString() + " surface=" + mSurface); @@ -1931,6 +1970,8 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mVisibleInsets); final boolean stableInsetsChanged = !mPendingStableInsets.equals( mAttachInfo.mStableInsets); + final boolean cutoutChanged = !mPendingDisplayCutout.equals( + mAttachInfo.mDisplayCutout); final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets); final boolean surfaceSizeChanged = (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0; @@ -1955,6 +1996,14 @@ public final class ViewRootImpl implements ViewParent, // Need to relayout with content insets. contentInsetsChanged = true; } + if (cutoutChanged) { + mAttachInfo.mDisplayCutout.set(mPendingDisplayCutout); + if (DEBUG_LAYOUT) { + Log.v(mTag, "DisplayCutout changing to: " + mAttachInfo.mDisplayCutout); + } + // Need to relayout with content insets. + contentInsetsChanged = true; + } if (alwaysConsumeNavBarChanged) { mAttachInfo.mAlwaysConsumeNavBar = mPendingAlwaysConsumeNavBar; contentInsetsChanged = true; @@ -2056,6 +2105,7 @@ public final class ViewRootImpl implements ViewParent, mResizeMode = freeformResizing ? RESIZE_MODE_FREEFORM : RESIZE_MODE_DOCKED_DIVIDER; + // TODO: Need cutout? startDragResizing(mPendingBackDropFrame, mWinFrame.equals(mPendingBackDropFrame), mPendingVisibleInsets, mPendingStableInsets, mResizeMode); @@ -2064,7 +2114,7 @@ public final class ViewRootImpl implements ViewParent, endDragResizing(); } } - if (!USE_MT_RENDERER) { + if (!mUseMTRenderer) { if (dragResizing) { mCanvasOffsetX = mWinFrame.left; mCanvasOffsetY = mWinFrame.top; @@ -2420,6 +2470,93 @@ public final class ViewRootImpl implements ViewParent, } } + private void handleWindowFocusChanged() { + final boolean hasWindowFocus; + final boolean inTouchMode; + synchronized (this) { + if (!mWindowFocusChanged) { + return; + } + mWindowFocusChanged = false; + hasWindowFocus = mUpcomingWindowFocus; + inTouchMode = mUpcomingInTouchMode; + } + + if (mAdded) { + profileRendering(hasWindowFocus); + + if (hasWindowFocus) { + ensureTouchModeLocally(inTouchMode); + if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) { + mFullRedrawNeeded = true; + try { + final WindowManager.LayoutParams lp = mWindowAttributes; + final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null; + mAttachInfo.mThreadedRenderer.initializeIfNeeded( + mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets); + } catch (OutOfResourcesException e) { + Log.e(mTag, "OutOfResourcesException locking surface", e); + try { + if (!mWindowSession.outOfMemory(mWindow)) { + Slog.w(mTag, "No processes killed for memory;" + + " killing self"); + Process.killProcess(Process.myPid()); + } + } catch (RemoteException ex) { + } + // Retry in a bit. + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_WINDOW_FOCUS_CHANGED), 500); + return; + } + } + } + + mAttachInfo.mHasWindowFocus = hasWindowFocus; + + mLastWasImTarget = WindowManager.LayoutParams + .mayUseInputMethod(mWindowAttributes.flags); + + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { + imm.onPreWindowFocus(mView, hasWindowFocus); + } + if (mView != null) { + mAttachInfo.mKeyDispatchState.reset(); + mView.dispatchWindowFocusChanged(hasWindowFocus); + mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus); + + if (mAttachInfo.mTooltipHost != null) { + mAttachInfo.mTooltipHost.hideTooltip(); + } + } + + // Note: must be done after the focus change callbacks, + // so all of the view state is set up correctly. + if (hasWindowFocus) { + if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { + imm.onPostWindowFocus(mView, mView.findFocus(), + mWindowAttributes.softInputMode, + !mHasHadWindowFocus, mWindowAttributes.flags); + } + // Clear the forward bit. We can just do this directly, since + // the window manager doesn't care about it. + mWindowAttributes.softInputMode &= + ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; + ((WindowManager.LayoutParams) mView.getLayoutParams()) + .softInputMode &= + ~WindowManager.LayoutParams + .SOFT_INPUT_IS_FORWARD_NAVIGATION; + mHasHadWindowFocus = true; + } else { + if (mPointerCapture) { + handlePointerCaptureChanged(false); + } + } + } + mFirstInputStage.onWindowFocusChanged(hasWindowFocus); + } + private void handleOutOfResourcesException(Surface.OutOfResourcesException e) { Log.e(mTag, "OutOfResourcesException initializing HW surface", e); try { @@ -2702,8 +2839,10 @@ public final class ViewRootImpl implements ViewParent, @Override public void onPostDraw(DisplayListCanvas canvas) { drawAccessibilityFocusedDrawableIfNeeded(canvas); - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onPostDraw(canvas); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onPostDraw(canvas); + } } } @@ -3034,7 +3173,8 @@ public final class ViewRootImpl implements ViewParent, return; } - if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { + if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, + scalingRequired, dirty, surfaceInsets)) { return; } } @@ -3050,11 +3190,22 @@ public final class ViewRootImpl implements ViewParent, * @return true if drawing was successful, false if an error occurred */ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, - boolean scalingRequired, Rect dirty) { + boolean scalingRequired, Rect dirty, Rect surfaceInsets) { // Draw with software renderer. final Canvas canvas; + + // We already have the offset of surfaceInsets in xoff, yoff and dirty region, + // therefore we need to add it back when moving the dirty region. + int dirtyXOffset = xoff; + int dirtyYOffset = yoff; + if (surfaceInsets != null) { + dirtyXOffset += surfaceInsets.left; + dirtyYOffset += surfaceInsets.top; + } + try { + dirty.offset(-dirtyXOffset, -dirtyYOffset); final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; @@ -3081,6 +3232,8 @@ public final class ViewRootImpl implements ViewParent, // kill stuff (or ourself) for no reason. mLayoutRequested = true; // ask wm for a new surface next time. return false; + } finally { + dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value. } try { @@ -3776,6 +3929,7 @@ public final class ViewRootImpl implements ViewParent, && mPendingOverscanInsets.equals(args.arg5) && mPendingContentInsets.equals(args.arg2) && mPendingStableInsets.equals(args.arg6) + && mPendingDisplayCutout.get().equals(args.arg9) && mPendingVisibleInsets.equals(args.arg3) && mPendingOutsets.equals(args.arg7) && mPendingBackDropFrame.equals(args.arg8) @@ -3808,6 +3962,7 @@ public final class ViewRootImpl implements ViewParent, || !mPendingOverscanInsets.equals(args.arg5) || !mPendingContentInsets.equals(args.arg2) || !mPendingStableInsets.equals(args.arg6) + || !mPendingDisplayCutout.get().equals(args.arg9) || !mPendingVisibleInsets.equals(args.arg3) || !mPendingOutsets.equals(args.arg7); @@ -3815,6 +3970,7 @@ public final class ViewRootImpl implements ViewParent, mPendingOverscanInsets.set((Rect) args.arg5); mPendingContentInsets.set((Rect) args.arg2); mPendingStableInsets.set((Rect) args.arg6); + mPendingDisplayCutout.set((DisplayCutout) args.arg9); mPendingVisibleInsets.set((Rect) args.arg3); mPendingOutsets.set((Rect) args.arg7); mPendingBackDropFrame.set((Rect) args.arg8); @@ -3849,81 +4005,7 @@ public final class ViewRootImpl implements ViewParent, } break; case MSG_WINDOW_FOCUS_CHANGED: { - final boolean hasWindowFocus = msg.arg1 != 0; - if (mAdded) { - mAttachInfo.mHasWindowFocus = hasWindowFocus; - - profileRendering(hasWindowFocus); - - if (hasWindowFocus) { - boolean inTouchMode = msg.arg2 != 0; - ensureTouchModeLocally(inTouchMode); - if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) { - mFullRedrawNeeded = true; - try { - final WindowManager.LayoutParams lp = mWindowAttributes; - final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null; - mAttachInfo.mThreadedRenderer.initializeIfNeeded( - mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets); - } catch (OutOfResourcesException e) { - Log.e(mTag, "OutOfResourcesException locking surface", e); - try { - if (!mWindowSession.outOfMemory(mWindow)) { - Slog.w(mTag, "No processes killed for memory;" - + " killing self"); - Process.killProcess(Process.myPid()); - } - } catch (RemoteException ex) { - } - // Retry in a bit. - sendMessageDelayed(obtainMessage(msg.what, msg.arg1, msg.arg2), - 500); - return; - } - } - } - - mLastWasImTarget = WindowManager.LayoutParams - .mayUseInputMethod(mWindowAttributes.flags); - - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { - imm.onPreWindowFocus(mView, hasWindowFocus); - } - if (mView != null) { - mAttachInfo.mKeyDispatchState.reset(); - mView.dispatchWindowFocusChanged(hasWindowFocus); - mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus); - - if (mAttachInfo.mTooltipHost != null) { - mAttachInfo.mTooltipHost.hideTooltip(); - } - } - - // Note: must be done after the focus change callbacks, - // so all of the view state is set up correctly. - if (hasWindowFocus) { - if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { - imm.onPostWindowFocus(mView, mView.findFocus(), - mWindowAttributes.softInputMode, - !mHasHadWindowFocus, mWindowAttributes.flags); - } - // Clear the forward bit. We can just do this directly, since - // the window manager doesn't care about it. - mWindowAttributes.softInputMode &= - ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; - ((WindowManager.LayoutParams) mView.getLayoutParams()) - .softInputMode &= - ~WindowManager.LayoutParams - .SOFT_INPUT_IS_FORWARD_NAVIGATION; - mHasHadWindowFocus = true; - } else { - if (mPointerCapture) { - handlePointerCaptureChanged(false); - } - } - } - mFirstInputStage.onWindowFocusChanged(hasWindowFocus); + handleWindowFocusChanged(); } break; case MSG_DIE: doDie(); @@ -6258,7 +6340,7 @@ public final class ViewRootImpl implements ViewParent, (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, - mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, + mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout, mPendingMergedConfiguration, mSurface); mPendingAlwaysConsumeNavBar = @@ -6541,7 +6623,8 @@ public final class ViewRootImpl implements ViewParent, private void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { if (DEBUG_LAYOUT) Log.v(mTag, "Resizing " + this + ": frame=" + frame.toShortString() + " contentInsets=" + contentInsets.toShortString() + " visibleInsets=" + visibleInsets.toShortString() @@ -6550,7 +6633,7 @@ public final class ViewRootImpl implements ViewParent, // Tell all listeners that we are resizing the window so that the chrome can get // updated as fast as possible on a separate thread, - if (mDragResizing) { + if (mDragResizing && mUseMTRenderer) { boolean fullscreen = frame.equals(backDropFrame); synchronized (mWindowCallbacks) { for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { @@ -6578,6 +6661,7 @@ public final class ViewRootImpl implements ViewParent, args.arg6 = sameProcessCall ? new Rect(stableInsets) : stableInsets; args.arg7 = sameProcessCall ? new Rect(outsets) : outsets; args.arg8 = sameProcessCall ? new Rect(backDropFrame) : backDropFrame; + args.arg9 = displayCutout.get(); // DisplayCutout is immutable. args.argi1 = forceLayout ? 1 : 0; args.argi2 = alwaysConsumeNavBar ? 1 : 0; args.argi3 = displayId; @@ -6792,6 +6876,7 @@ public final class ViewRootImpl implements ViewParent, } if (stage != null) { + handleWindowFocusChanged(); stage.deliver(q); } else { finishInputEvent(q); @@ -7097,10 +7182,13 @@ public final class ViewRootImpl implements ViewParent, } public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { + synchronized (this) { + mWindowFocusChanged = true; + mUpcomingWindowFocus = hasFocus; + mUpcomingInTouchMode = inTouchMode; + } Message msg = Message.obtain(); msg.what = MSG_WINDOW_FOCUS_CHANGED; - msg.arg1 = hasFocus ? 1 : 0; - msg.arg2 = inTouchMode ? 1 : 0; mHandler.sendMessage(msg); } @@ -7610,12 +7698,13 @@ public final class ViewRootImpl implements ViewParent, public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { viewAncestor.dispatchResized(frame, overscanInsets, contentInsets, visibleInsets, stableInsets, outsets, reportDraw, mergedConfiguration, - backDropFrame, forceLayout, alwaysConsumeNavBar, displayId); + backDropFrame, forceLayout, alwaysConsumeNavBar, displayId, displayCutout); } } @@ -7798,9 +7887,11 @@ public final class ViewRootImpl implements ViewParent, Rect stableInsets, int resizeMode) { if (!mDragResizing) { mDragResizing = true; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds, fullscreen, - systemInsets, stableInsets, resizeMode); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onWindowDragResizeStart( + initialBounds, fullscreen, systemInsets, stableInsets, resizeMode); + } } mFullRedrawNeeded = true; } @@ -7812,8 +7903,10 @@ public final class ViewRootImpl implements ViewParent, private void endDragResizing() { if (mDragResizing) { mDragResizing = false; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onWindowDragResizeEnd(); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onWindowDragResizeEnd(); + } } mFullRedrawNeeded = true; } @@ -7821,19 +7914,21 @@ public final class ViewRootImpl implements ViewParent, private boolean updateContentDrawBounds() { boolean updated = false; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - updated |= mWindowCallbacks.get(i).onContentDrawn( - mWindowAttributes.surfaceInsets.left, - mWindowAttributes.surfaceInsets.top, - mWidth, mHeight); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + updated |= + mWindowCallbacks.get(i).onContentDrawn(mWindowAttributes.surfaceInsets.left, + mWindowAttributes.surfaceInsets.top, mWidth, mHeight); + } } return updated | (mDragResizing && mReportNextDraw); } private void requestDrawWindow() { - if (mReportNextDraw) { - mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size()); + if (!mUseMTRenderer) { + return; } + mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size()); for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { mWindowCallbacks.get(i).onRequestDraw(mReportNextDraw); } @@ -7877,6 +7972,7 @@ public final class ViewRootImpl implements ViewParent, if (!registered) { mAttachInfo.mAccessibilityWindowId = mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, + mContext.getPackageName(), new AccessibilityInteractionConnection(ViewRootImpl.this)); } } diff --git a/android/view/View_Delegate.java b/android/view/View_Delegate.java index 408ec549..5d39e4c9 100644 --- a/android/view/View_Delegate.java +++ b/android/view/View_Delegate.java @@ -16,10 +16,13 @@ package android.view; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.content.Context; +import android.graphics.Canvas; import android.os.IBinder; /** @@ -44,4 +47,50 @@ public class View_Delegate { } return null; } + + @LayoutlibDelegate + /*package*/ static void draw(View thisView, Canvas canvas) { + try { + // This code is run within a catch to prevent misbehaving components from breaking + // all the layout. + thisView.draw_Original(canvas); + } catch (Throwable t) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View draw failed", t, null); + } + } + + @LayoutlibDelegate + /*package*/ static boolean draw( + View thisView, Canvas canvas, ViewGroup parent, long drawingTime) { + try { + // This code is run within a catch to prevent misbehaving components from breaking + // all the layout. + return thisView.draw_Original(canvas, parent, drawingTime); + } catch (Throwable t) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View draw failed", t, null); + } + return false; + } + + @LayoutlibDelegate + /*package*/ static void measure(View thisView, int widthMeasureSpec, int heightMeasureSpec) { + try { + // This code is run within a catch to prevent misbehaving components from breaking + // all the layout. + thisView.measure_Original(widthMeasureSpec, heightMeasureSpec); + } catch (Throwable t) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View measure failed", t, null); + } + } + + @LayoutlibDelegate + /*package*/ static void layout(View thisView, int l, int t, int r, int b) { + try { + // This code is run within a catch to prevent misbehaving components from breaking + // all the layout. + thisView.layout_Original(l, t, r, b); + } catch (Throwable th) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View layout failed", th, null); + } + } } diff --git a/android/view/WindowInsets.java b/android/view/WindowInsets.java index df124ac5..e5cbe96b 100644 --- a/android/view/WindowInsets.java +++ b/android/view/WindowInsets.java @@ -17,7 +17,7 @@ package android.view; -import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Rect; /** @@ -49,7 +49,7 @@ public final class WindowInsets { private boolean mSystemWindowInsetsConsumed = false; private boolean mWindowDecorInsetsConsumed = false; private boolean mStableInsetsConsumed = false; - private boolean mCutoutConsumed = false; + private boolean mDisplayCutoutConsumed = false; private static final Rect EMPTY_RECT = new Rect(0, 0, 0, 0); @@ -80,8 +80,9 @@ public final class WindowInsets { mIsRound = isRound; mAlwaysConsumeNavBar = alwaysConsumeNavBar; - mCutoutConsumed = displayCutout == null; - mDisplayCutout = mCutoutConsumed ? DisplayCutout.NO_CUTOUT : displayCutout; + mDisplayCutoutConsumed = displayCutout == null; + mDisplayCutout = (mDisplayCutoutConsumed || displayCutout.isEmpty()) + ? null : displayCutout; } /** @@ -99,7 +100,7 @@ public final class WindowInsets { mIsRound = src.mIsRound; mAlwaysConsumeNavBar = src.mAlwaysConsumeNavBar; mDisplayCutout = src.mDisplayCutout; - mCutoutConsumed = src.mCutoutConsumed; + mDisplayCutoutConsumed = src.mDisplayCutoutConsumed; } /** @hide */ @@ -269,15 +270,16 @@ public final class WindowInsets { */ public boolean hasInsets() { return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets() - || mDisplayCutout.hasCutout(); + || mDisplayCutout != null; } /** - * @return the display cutout + * Returns the display cutout if there is one. + * + * @return the display cutout or null if there is none * @see DisplayCutout - * @hide pending API */ - @NonNull + @Nullable public DisplayCutout getDisplayCutout() { return mDisplayCutout; } @@ -286,12 +288,11 @@ public final class WindowInsets { * Returns a copy of this WindowInsets with the cutout fully consumed. * * @return A modified copy of this WindowInsets - * @hide pending API */ - public WindowInsets consumeCutout() { + public WindowInsets consumeDisplayCutout() { final WindowInsets result = new WindowInsets(this); - result.mDisplayCutout = DisplayCutout.NO_CUTOUT; - result.mCutoutConsumed = true; + result.mDisplayCutout = null; + result.mDisplayCutoutConsumed = true; return result; } @@ -311,7 +312,7 @@ public final class WindowInsets { */ public boolean isConsumed() { return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed - && mCutoutConsumed; + && mDisplayCutoutConsumed; } /** @@ -530,7 +531,7 @@ public final class WindowInsets { return "WindowInsets{systemWindowInsets=" + mSystemWindowInsets + " windowDecorInsets=" + mWindowDecorInsets + " stableInsets=" + mStableInsets - + (mDisplayCutout.hasCutout() ? " cutout=" + mDisplayCutout : "") + + (mDisplayCutout != null ? " cutout=" + mDisplayCutout : "") + (isRound() ? " round" : "") + "}"; } diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index 905c0715..cbe012af 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -20,6 +20,7 @@ import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT; import android.Manifest.permission; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; @@ -1268,6 +1269,33 @@ public interface WindowManager extends ViewManager { }, formatToHexString = true) public int flags; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @LongDef( + flag = true, + value = { + LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA, + }) + @interface Flags2 {} + + /** + * Window flag: allow placing the window within the area that overlaps with the + * display cutout. + * + *

    + * The window must correctly position its contents to take the display cutout into account. + * + * @see DisplayCutout + */ + public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 0x00000001; + + /** + * Various behavioral options/flags. Default is none. + * + * @see #FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA + */ + @Flags2 public long flags2; + /** * If the window has requested hardware acceleration, but this is not * allowed in the process it is in, then still render it as if it is @@ -1642,12 +1670,20 @@ public interface WindowManager extends ViewManager { * Visibility state for {@link #softInputMode}: please show the soft * input area when normally appropriate (when the user is navigating * forward to your window). + * + *

    Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag + * is ignored unless there is a focused view that returns {@code true} from + * {@link View#isInEditMode()} when the window is focused.

    */ public static final int SOFT_INPUT_STATE_VISIBLE = 4; /** * Visibility state for {@link #softInputMode}: please always make the * soft input area visible when this window receives input focus. + * + *

    Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag + * is ignored unless there is a focused view that returns {@code true} from + * {@link View#isInEditMode()} when the window is focused.

    */ public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5; @@ -1708,7 +1744,7 @@ public interface WindowManager extends ViewManager { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { + @IntDef(flag = true, prefix = { "SOFT_INPUT_" }, value = { SOFT_INPUT_STATE_UNSPECIFIED, SOFT_INPUT_STATE_UNCHANGED, SOFT_INPUT_STATE_HIDDEN, @@ -2211,6 +2247,7 @@ public interface WindowManager extends ViewManager { out.writeInt(y); out.writeInt(type); out.writeInt(flags); + out.writeLong(flags2); out.writeInt(privateFlags); out.writeInt(softInputMode); out.writeInt(gravity); @@ -2266,6 +2303,7 @@ public interface WindowManager extends ViewManager { y = in.readInt(); type = in.readInt(); flags = in.readInt(); + flags2 = in.readLong(); privateFlags = in.readInt(); softInputMode = in.readInt(); gravity = in.readInt(); @@ -2398,6 +2436,10 @@ public interface WindowManager extends ViewManager { flags = o.flags; changes |= FLAGS_CHANGED; } + if (flags2 != o.flags2) { + flags2 = o.flags2; + changes |= FLAGS_CHANGED; + } if (privateFlags != o.privateFlags) { privateFlags = o.privateFlags; changes |= PRIVATE_FLAGS_CHANGED; @@ -2651,6 +2693,11 @@ public interface WindowManager extends ViewManager { sb.append(System.lineSeparator()); sb.append(prefix).append(" fl=").append( ViewDebug.flagsToString(LayoutParams.class, "flags", flags)); + if (flags2 != 0) { + sb.append(System.lineSeparator()); + // TODO(roosa): add a long overload for ViewDebug.flagsToString. + sb.append(prefix).append(" fl2=0x").append(Long.toHexString(flags2)); + } if (privateFlags != 0) { sb.append(System.lineSeparator()); sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString( diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java index d890f329..72af203e 100644 --- a/android/view/accessibility/AccessibilityInteractionClient.java +++ b/android/view/accessibility/AccessibilityInteractionClient.java @@ -29,6 +29,7 @@ import android.util.LongSparseArray; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import java.util.ArrayList; import java.util.Collections; @@ -213,7 +214,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @return The {@link AccessibilityWindowInfo}. */ @@ -299,7 +300,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -335,18 +336,19 @@ public final class AccessibilityInteractionClient } final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success; + final String[] packageNames; try { - success = connection.findAccessibilityNodeInfoByAccessibilityId( + packageNames = connection.findAccessibilityNodeInfoByAccessibilityId( accessibilityWindowId, accessibilityNodeId, interactionId, this, prefetchFlags, Thread.currentThread().getId(), arguments); } finally { Binder.restoreCallingIdentity(identityToken); } - if (success) { + if (packageNames != null) { List infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, bypassCache); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + bypassCache, packageNames); if (infos != null && !infos.isEmpty()) { for (int i = 1; i < infos.size(); i++) { infos.get(i).recycle(); @@ -373,7 +375,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -389,20 +391,21 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success; + final String[] packageNames; try { - success = connection.findAccessibilityNodeInfosByViewId( + packageNames = connection.findAccessibilityNodeInfosByViewId( accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, Thread.currentThread().getId()); } finally { Binder.restoreCallingIdentity(identityToken); } - if (success) { + if (packageNames != null) { List infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); if (infos != null) { - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, false); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + false, packageNames); return infos; } } @@ -426,7 +429,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -442,20 +445,21 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success; + final String[] packageNames; try { - success = connection.findAccessibilityNodeInfosByText( + packageNames = connection.findAccessibilityNodeInfosByText( accessibilityWindowId, accessibilityNodeId, text, interactionId, this, Thread.currentThread().getId()); } finally { Binder.restoreCallingIdentity(identityToken); } - if (success) { + if (packageNames != null) { List infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); if (infos != null) { - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, false); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + false, packageNames); return infos; } } @@ -478,7 +482,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -494,19 +498,19 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success; + final String[] packageNames; try { - success = connection.findFocus(accessibilityWindowId, + packageNames = connection.findFocus(accessibilityWindowId, accessibilityNodeId, focusType, interactionId, this, Thread.currentThread().getId()); } finally { Binder.restoreCallingIdentity(identityToken); } - if (success) { + if (packageNames != null) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); return info; } } else { @@ -527,7 +531,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -543,19 +547,19 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success; + final String[] packageNames; try { - success = connection.focusSearch(accessibilityWindowId, + packageNames = connection.focusSearch(accessibilityWindowId, accessibilityNodeId, direction, interactionId, this, Thread.currentThread().getId()); } finally { Binder.restoreCallingIdentity(identityToken); } - if (success) { + if (packageNames != null) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); return info; } } else { @@ -574,7 +578,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -661,7 +665,7 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { final boolean success = waitForResultTimedLocked(interactionId); - List result = null; + final List result; if (success) { result = mFindAccessibilityNodeInfosResult; } else { @@ -779,11 +783,22 @@ public final class AccessibilityInteractionClient * @param connectionId The id of the connection to the system. * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if * this value is {@code false} + * @param packageNames The valid package names a node can come from. */ private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, - int connectionId, boolean bypassCache) { + int connectionId, boolean bypassCache, String[] packageNames) { if (info != null) { info.setConnectionId(connectionId); + // Empty array means any package name is Okay + if (!ArrayUtils.isEmpty(packageNames)) { + CharSequence packageName = info.getPackageName(); + if (packageName == null + || !ArrayUtils.contains(packageNames, packageName.toString())) { + // If the node package not one of the valid ones, pick the top one - this + // is one of the packages running in the introspected UID. + info.setPackageName(packageNames[0]); + } + } info.setSealed(true); if (!bypassCache) { sAccessibilityCache.add(info); @@ -798,14 +813,16 @@ public final class AccessibilityInteractionClient * @param connectionId The id of the connection to the system. * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if * this value is {@code false} + * @param packageNames The valid package names a node can come from. */ private void finalizeAndCacheAccessibilityNodeInfos(List infos, - int connectionId, boolean bypassCache) { + int connectionId, boolean bypassCache, String[] packageNames) { if (infos != null) { final int infosCount = infos.size(); for (int i = 0; i < infosCount; i++) { AccessibilityNodeInfo info = infos.get(i); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId, bypassCache); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, + bypassCache, packageNames); } } } diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 0375635f..dd8ba556 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,153 +16,46 @@ package android.view.accessibility; -import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; - -import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SdkConstant; -import android.annotation.SystemService; -import android.annotation.TestApi; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.os.Binder; import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.ArrayMap; -import android.util.Log; -import android.util.SparseArray; import android.view.IWindow; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.IntPair; - -import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, - * and provides facilities for querying the accessibility state of the system. - * Accessibility events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. + * Such events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@link android.accessibilityservice.AccessibilityService}. + * {@code android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see AccessibilityNodeInfo - * @see android.accessibilityservice.AccessibilityService - * @see Context#getSystemService - * @see Context#ACCESSIBILITY_SERVICE + * @see android.content.Context#getSystemService */ -@SystemService(Context.ACCESSIBILITY_SERVICE) +@SuppressWarnings("UnusedDeclaration") public final class AccessibilityManager { - private static final boolean DEBUG = false; - - private static final String LOG_TAG = "AccessibilityManager"; - - /** @hide */ - public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; - - /** @hide */ - public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; - - /** @hide */ - public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; - - /** @hide */ - public static final int DALTONIZER_DISABLED = -1; - /** @hide */ - public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); - /** @hide */ - public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; - - /** @hide */ - public static final int AUTOCLICK_DELAY_DEFAULT = 600; /** - * Activity action: Launch UI to manage which accessibility service or feature is assigned - * to the navigation bar Accessibility button. - *

    - * Input: Nothing. - *

    - *

    - * Output: Nothing. - *

    - * - * @hide - */ - @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = - "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; - - static final Object sInstanceSync = new Object(); - - private static AccessibilityManager sInstance; - - private final Object mLock = new Object(); - - private IAccessibilityManager mService; - - final int mUserId; - - final Handler mHandler; - - final Handler.Callback mCallback; - - boolean mIsEnabled; - - int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; - - boolean mIsTouchExplorationEnabled; - - boolean mIsHighTextContrastEnabled; - - private final ArrayMap - mAccessibilityStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap - mTouchExplorationStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap - mHighTextContrastStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap - mServicesStateChangeListeners = new ArrayMap<>(); - - /** - * Map from a view's accessibility id to the list of request preparers set for that view - */ - private SparseArray> mRequestPreparerLists; - - /** - * Listener for the system accessibility state. To listen for changes to the - * accessibility state on the device, implement this interface and register - * it with the system by calling {@link #addAccessibilityStateChangeListener}. + * Listener for the accessibility state. */ public interface AccessibilityStateChangeListener { /** - * Called when the accessibility enabled state changes. + * Called back on change in the accessibility state. * * @param enabled Whether accessibility is enabled. */ - void onAccessibilityStateChanged(boolean enabled); + public void onAccessibilityStateChanged(boolean enabled); } /** @@ -178,25 +71,7 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - void onTouchExplorationStateChanged(boolean enabled); - } - - /** - * Listener for changes to the state of accessibility services. Changes include services being - * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. - * {@see #addAccessibilityServicesStateChangeListener}. - * - * @hide - */ - @TestApi - public interface AccessibilityServicesStateChangeListener { - - /** - * Called when the state of accessibility services changes. - * - * @param manager The manager that is calling back - */ - void onAccessibilityServicesStateChanged(AccessibilityManager manager); + public void onTouchExplorationStateChanged(boolean enabled); } /** @@ -204,8 +79,6 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. - * - * @hide */ public interface HighTextContrastChangeListener { @@ -214,72 +87,26 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - void onHighTextContrastStateChanged(boolean enabled); + public void onHighTextContrastStateChanged(boolean enabled); } private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - @Override - public void setState(int state) { - // We do not want to change this immediately as the application may - // have already checked that accessibility is on and fired an event, - // that is now propagating up the view tree, Hence, if accessibility - // is now off an exception will be thrown. We want to have the exception - // enforcement to guard against apps that fire unnecessary accessibility - // events when accessibility is off. - mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); - } - - @Override - public void notifyServicesStateChanged() { - final ArrayMap listeners; - synchronized (mLock) { - if (mServicesStateChangeListeners.isEmpty()) { - return; + public void setState(int state) { } - listeners = new ArrayMap<>(mServicesStateChangeListeners); - } - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityServicesStateChangeListener listener = - mServicesStateChangeListeners.keyAt(i); - mServicesStateChangeListeners.valueAt(i).post(() -> listener - .onAccessibilityServicesStateChanged(AccessibilityManager.this)); - } - } + public void notifyServicesStateChanged() { + } - @Override - public void setRelevantEventTypes(int eventTypes) { - mRelevantEventTypes = eventTypes; - } - }; + public void setRelevantEventTypes(int eventTypes) { + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * - * @param context Context in which this manager operates. - * - * @hide */ public static AccessibilityManager getInstance(Context context) { - synchronized (sInstanceSync) { - if (sInstance == null) { - final int userId; - if (Binder.getCallingUid() == Process.SYSTEM_UID - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS) - == PackageManager.PERMISSION_GRANTED - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS_FULL) - == PackageManager.PERMISSION_GRANTED) { - userId = UserHandle.USER_CURRENT; - } else { - userId = UserHandle.myUserId(); - } - sInstance = new AccessibilityManager(context, null, userId); - } - } return sInstance; } @@ -287,68 +114,21 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { - // Constructor can't be chained because we can't create an instance of an inner class - // before calling another constructor. - mCallback = new MyCallback(); - mHandler = new Handler(context.getMainLooper(), mCallback); - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } } - /** - * Create an instance. - * - * @param handler The handler to use - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide - */ - public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { - mCallback = new MyCallback(); - mHandler = handler; - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } - } - - /** - * @hide - */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * @hide - */ - @VisibleForTesting - public Handler.Callback getCallback() { - return mCallback; - } - - /** - * Returns if the accessibility in the system is enabled. + * Returns if the {@link AccessibilityManager} is enabled. * - * @return True if accessibility is enabled, false otherwise. + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. */ public boolean isEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsEnabled; - } + return false; } /** @@ -357,13 +137,7 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsTouchExplorationEnabled; - } + return true; } /** @@ -373,84 +147,15 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. *

    * - * @return True if high text contrast is enabled, false otherwise. - * - * @hide */ public boolean isHighTextContrastEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsHighTextContrastEnabled; - } + return false; } /** * Sends an {@link AccessibilityEvent}. - * - * @param event The event to send. - * - * @throws IllegalStateException if accessibility is not enabled. - * - * Note: The preferred mechanism for sending custom accessibility - * events is through calling - * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} - * instead of this method to allow predecessors to augment/filter events sent by - * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); - return; - } - } - if ((event.getEventType() & mRelevantEventTypes) == 0) { - if (DEBUG) { - Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event - + " that is not among " - + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); - } - return; - } - userId = mUserId; - } - try { - event.setEventTime(SystemClock.uptimeMillis()); - // it is possible that this manager is in the same process as the service but - // client using it is called through Binder from another process. Example: MMS - // app adds a SMS notification and the NotificationManagerService calls this method - long identityToken = Binder.clearCallingIdentity(); - try { - service.sendAccessibilityEvent(event, userId); - } finally { - Binder.restoreCallingIdentity(identityToken); - } - if (DEBUG) { - Log.i(LOG_TAG, event + " sent"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error during sending " + event + " ", re); - } finally { - event.recycle(); - } } /** @@ -462,95 +167,27 @@ public final class AccessibilityManager { * @return Whether the event is being observed. */ public boolean isObservedEventType(@AccessibilityEvent.EventType int type) { - return mIsEnabled && (mRelevantEventTypes & type) != 0; + return false; } /** - * Requests feedback interruption from all accessibility services. + * Requests interruption of the accessibility feedback from all accessibility services. */ public void interrupt() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); - return; - } - } - userId = mUserId; - } - try { - service.interrupt(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Requested interrupt from all services"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); - } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. - * - * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List getAccessibilityServiceList() { - List infos = getInstalledAccessibilityServiceList(); - List services = new ArrayList<>(); - final int infoCount = infos.size(); - for (int i = 0; i < infoCount; i++) { - AccessibilityServiceInfo info = infos.get(i); - services.add(info.getResolveInfo().serviceInfo); - } - return Collections.unmodifiableList(services); + return Collections.emptyList(); } - /** - * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. - * - * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. - */ public List getInstalledAccessibilityServiceList() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List services = null; - try { - services = service.getInstalledAccessibilityServiceList(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** @@ -565,48 +202,21 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL - * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List services = null; - try { - services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. Equivalent to calling - * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} - * with a null handler. + * the global accessibility state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - addAccessibilityStateChangeListener(listener, null); + AccessibilityStateChangeListener listener) { return true; } @@ -620,40 +230,22 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mAccessibilityStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} - /** - * Unregisters an {@link AccessibilityStateChangeListener}. - * - * @param listener The listener. - * @return True if the listener was previously registered. - */ public boolean removeAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - synchronized (mLock) { - int index = mAccessibilityStateChangeListeners.indexOfKey(listener); - mAccessibilityStateChangeListeners.remove(listener); - return (index >= 0); - } + AccessibilityStateChangeListener listener) { + return true; } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. Equivalent to calling - * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} - * with a null handler. + * the global touch exploration state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - addTouchExplorationStateChangeListener(listener, null); return true; } @@ -667,105 +259,17 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mTouchExplorationStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if listener was previously registered. + * @return True if successfully unregistered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - synchronized (mLock) { - int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); - mTouchExplorationStateChangeListeners.remove(listener); - return (index >= 0); - } - } - - /** - * Registers a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * @param handler The handler on which the listener should be called back, or {@code null} - * for a callback on the process's main handler. - * @hide - */ - @TestApi - public void addAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mServicesStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } - - /** - * Unregisters a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * - * @hide - */ - @TestApi - public void removeAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener) { - // Final CopyOnWriteArrayList - no lock needed. - mServicesStateChangeListeners.remove(listener); - } - - /** - * Registers a {@link AccessibilityRequestPreparer}. - */ - public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - mRequestPreparerLists = new SparseArray<>(1); - } - int id = preparer.getView().getAccessibilityViewId(); - List requestPreparerList = mRequestPreparerLists.get(id); - if (requestPreparerList == null) { - requestPreparerList = new ArrayList<>(1); - mRequestPreparerLists.put(id, requestPreparerList); - } - requestPreparerList.add(preparer); - } - - /** - * Unregisters a {@link AccessibilityRequestPreparer}. - */ - public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - return; - } - int viewId = preparer.getView().getAccessibilityViewId(); - List requestPreparerList = mRequestPreparerLists.get(viewId); - if (requestPreparerList != null) { - requestPreparerList.remove(preparer); - if (requestPreparerList.isEmpty()) { - mRequestPreparerLists.remove(viewId); - } - } - } - - /** - * Get the preparers that are registered for an accessibility ID - * - * @param id The ID of interest - * @return The list of preparers, or {@code null} if there are none. - * - * @hide - */ - public List getRequestPreparersForAccessibilityId(int id) { - if (mRequestPreparerLists == null) { - return null; - } - return mRequestPreparerLists.get(id); + return true; } /** @@ -777,12 +281,7 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -792,51 +291,7 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners.remove(listener); - } - } - - /** - * Check if the accessibility volume stream is active. - * - * @return True if accessibility volume is active (i.e. some service has requested it). False - * otherwise. - * @hide - */ - public boolean isAccessibilityVolumeStreamActive() { - List serviceInfos = - getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); - for (int i = 0; i < serviceInfos.size(); i++) { - if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { - return true; - } - } - return false; - } - - /** - * Report a fingerprint gesture to accessibility. Only available for the system process. - * - * @param keyCode The key code of the gesture - * @return {@code true} if accessibility consumes the event. {@code false} if not. - * @hide - */ - public boolean sendFingerprintGesture(int keyCode) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return false; - } - } - try { - return service.sendFingerprintGesture(keyCode); - } catch (RemoteException e) { - return false; - } - } + @NonNull HighTextContrastChangeListener listener) {} /** * Sets the current state and notifies listeners, if necessary. @@ -844,314 +299,14 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { - final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; - final boolean touchExplorationEnabled = - (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; - final boolean highTextContrastEnabled = - (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; - - final boolean wasEnabled = mIsEnabled; - final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; - final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; - - // Ensure listeners get current state from isZzzEnabled() calls. - mIsEnabled = enabled; - mIsTouchExplorationEnabled = touchExplorationEnabled; - mIsHighTextContrastEnabled = highTextContrastEnabled; - - if (wasEnabled != enabled) { - notifyAccessibilityStateChanged(); - } - - if (wasTouchExplorationEnabled != touchExplorationEnabled) { - notifyTouchExplorationStateChanged(); - } - - if (wasHighTextContrastEnabled != highTextContrastEnabled) { - notifyHighTextContrastStateChanged(); - } } - /** - * Find an installed service with the specified {@link ComponentName}. - * - * @param componentName The name to match to the service. - * - * @return The info corresponding to the installed service, or {@code null} if no such service - * is installed. - * @hide - */ - public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( - ComponentName componentName) { - final List installedServiceInfos = - getInstalledAccessibilityServiceList(); - if ((installedServiceInfos == null) || (componentName == null)) { - return null; - } - for (int i = 0; i < installedServiceInfos.size(); i++) { - if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { - return installedServiceInfos.get(i); - } - } - return null; - } - - /** - * Adds an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is added. - * @param connection The connection. - * - * @hide - */ public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return View.NO_ID; - } - userId = mUserId; - } - try { - return service.addAccessibilityInteractionConnection(windowToken, connection, userId); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); - } return View.NO_ID; } - /** - * Removed an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is removed. - * - * @hide - */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.removeAccessibilityInteractionConnection(windowToken); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); - } - } - - /** - * Perform the accessibility shortcut if the caller has permission. - * - * @hide - */ - public void performAccessibilityShortcut() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.performAccessibilityShortcut(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); - } - } - - /** - * Notifies that the accessibility button in the system's navigation area has been clicked - * - * @hide - */ - public void notifyAccessibilityButtonClicked() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonClicked(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); - } - } - - /** - * Notifies that the visibility of the accessibility button in the system's navigation area - * has changed. - * - * @param shown {@code true} if the accessibility button is visible within the system - * navigation area, {@code false} otherwise - * @hide - */ - public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonVisibilityChanged(shown); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); - } - } - - /** - * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture - * window. Intended for use by the System UI only. - * - * @param connection The connection to handle the actions. Set to {@code null} to avoid - * affecting the actions. - * - * @hide - */ - public void setPictureInPictureActionReplacingConnection( - @Nullable IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.setPictureInPictureActionReplacingConnection(connection); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); - } } - private IAccessibilityManager getServiceLocked() { - if (mService == null) { - tryConnectToServiceLocked(null); - } - return mService; - } - - private void tryConnectToServiceLocked(IAccessibilityManager service) { - if (service == null) { - IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); - if (iBinder == null) { - return; - } - service = IAccessibilityManager.Stub.asInterface(iBinder); - } - - try { - final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); - setStateLocked(IntPair.first(userStateAndRelevantEvents)); - mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); - mService = service; - } catch (RemoteException re) { - Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); - } - } - - /** - * Notifies the registered {@link AccessibilityStateChangeListener}s. - */ - private void notifyAccessibilityStateChanged() { - final boolean isEnabled; - final ArrayMap listeners; - synchronized (mLock) { - if (mAccessibilityStateChangeListeners.isEmpty()) { - return; - } - isEnabled = mIsEnabled; - listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityStateChangeListener listener = - mAccessibilityStateChangeListeners.keyAt(i); - mAccessibilityStateChangeListeners.valueAt(i) - .post(() -> listener.onAccessibilityStateChanged(isEnabled)); - } - } - - /** - * Notifies the registered {@link TouchExplorationStateChangeListener}s. - */ - private void notifyTouchExplorationStateChanged() { - final boolean isTouchExplorationEnabled; - final ArrayMap listeners; - synchronized (mLock) { - if (mTouchExplorationStateChangeListeners.isEmpty()) { - return; - } - isTouchExplorationEnabled = mIsTouchExplorationEnabled; - listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final TouchExplorationStateChangeListener listener = - mTouchExplorationStateChangeListeners.keyAt(i); - mTouchExplorationStateChangeListeners.valueAt(i) - .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); - } - } - - /** - * Notifies the registered {@link HighTextContrastChangeListener}s. - */ - private void notifyHighTextContrastStateChanged() { - final boolean isHighTextContrastEnabled; - final ArrayMap listeners; - synchronized (mLock) { - if (mHighTextContrastStateChangeListeners.isEmpty()) { - return; - } - isHighTextContrastEnabled = mIsHighTextContrastEnabled; - listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final HighTextContrastChangeListener listener = - mHighTextContrastStateChangeListeners.keyAt(i); - mHighTextContrastStateChangeListeners.valueAt(i) - .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); - } - } - - /** - * Determines if the accessibility button within the system navigation area is supported. - * - * @return {@code true} if the accessibility button is supported on this device, - * {@code false} otherwise - */ - public static boolean isAccessibilityButtonSupported() { - final Resources res = Resources.getSystem(); - return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); - } - - private final class MyCallback implements Handler.Callback { - public static final int MSG_SET_STATE = 1; - - @Override - public boolean handleMessage(Message message) { - switch (message.what) { - case MSG_SET_STATE: { - // See comment at mClient - final int state = message.arg1; - synchronized (mLock) { - setStateLocked(state); - } - } break; - } - return true; - } - } } diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java index faea9200..28ef6978 100644 --- a/android/view/accessibility/AccessibilityNodeInfo.java +++ b/android/view/accessibility/AccessibilityNodeInfo.java @@ -2325,7 +2325,7 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Returns whether the node is explicitly marked as a focusable unit by a screen reader. Note * that {@code false} indicates that it is not explicitly marked, not that the node is not - * a focusable unit. Screen readers should generally used other signals, such as + * a focusable unit. Screen readers should generally use other signals, such as * {@link #isFocusable()}, or the presence of text in a node, to determine what should receive * focus. * @@ -3695,8 +3695,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (DEBUG) { builder.append("; sourceNodeId: " + mSourceNodeId); - builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); - builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); + builder.append("; windowId: " + mWindowId); + builder.append("; accessibilityViewId: ").append(getAccessibilityViewId(mSourceNodeId)); + builder.append("; virtualDescendantId: ").append(getVirtualDescendantId(mSourceNodeId)); builder.append("; mParentNodeId: " + mParentNodeId); builder.append("; traversalBefore: ").append(mTraversalBefore); builder.append("; traversalAfter: ").append(mTraversalAfter); @@ -3726,8 +3727,8 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("]"); } - builder.append("; boundsInParent: " + mBoundsInParent); - builder.append("; boundsInScreen: " + mBoundsInScreen); + builder.append("; boundsInParent: ").append(mBoundsInParent); + builder.append("; boundsInScreen: ").append(mBoundsInScreen); builder.append("; packageName: ").append(mPackageName); builder.append("; className: ").append(mClassName); diff --git a/android/view/accessibility/AccessibilityRequestPreparer.java b/android/view/accessibility/AccessibilityRequestPreparer.java index 889feb98..25f830a5 100644 --- a/android/view/accessibility/AccessibilityRequestPreparer.java +++ b/android/view/accessibility/AccessibilityRequestPreparer.java @@ -44,10 +44,9 @@ public abstract class AccessibilityRequestPreparer { public static final int REQUEST_TYPE_EXTRA_DATA = 0x00000001; /** @hide */ - @IntDef(flag = true, - value = { - REQUEST_TYPE_EXTRA_DATA - }) + @IntDef(flag = true, prefix = { "REQUEST_TYPE_" }, value = { + REQUEST_TYPE_EXTRA_DATA + }) @Retention(RetentionPolicy.SOURCE) public @interface RequestTypes {} diff --git a/android/view/accessibility/AccessibilityWindowInfo.java b/android/view/accessibility/AccessibilityWindowInfo.java index f11767de..ef1a3f3b 100644 --- a/android/view/accessibility/AccessibilityWindowInfo.java +++ b/android/view/accessibility/AccessibilityWindowInfo.java @@ -87,6 +87,7 @@ public final class AccessibilityWindowInfo implements Parcelable { private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0; private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1; private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 1 << 2; + private static final int BOOLEAN_PROPERTY_PICTURE_IN_PICTURE = 1 << 3; // Housekeeping. private static final int MAX_POOL_SIZE = 10; @@ -103,8 +104,7 @@ public final class AccessibilityWindowInfo implements Parcelable { private final Rect mBoundsInScreen = new Rect(); private LongArray mChildIds; private CharSequence mTitle; - private int mAnchorId = UNDEFINED_WINDOW_ID; - private boolean mInPictureInPicture; + private long mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; private int mConnectionId = UNDEFINED_WINDOW_ID; @@ -202,7 +202,7 @@ public final class AccessibilityWindowInfo implements Parcelable { * * @hide */ - public void setAnchorId(int anchorId) { + public void setAnchorId(long anchorId) { mAnchorId = anchorId; } @@ -212,7 +212,8 @@ public final class AccessibilityWindowInfo implements Parcelable { * @return The anchor node, or {@code null} if none exists. */ public AccessibilityNodeInfo getAnchor() { - if ((mConnectionId == UNDEFINED_WINDOW_ID) || (mAnchorId == UNDEFINED_WINDOW_ID) + if ((mConnectionId == UNDEFINED_WINDOW_ID) + || (mAnchorId == AccessibilityNodeInfo.UNDEFINED_NODE_ID) || (mParentId == UNDEFINED_WINDOW_ID)) { return null; } @@ -224,17 +225,7 @@ public final class AccessibilityWindowInfo implements Parcelable { /** @hide */ public void setPictureInPicture(boolean pictureInPicture) { - mInPictureInPicture = pictureInPicture; - } - - /** - * Check if the window is in picture-in-picture mode. - * - * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise. - * @removed - */ - public boolean inPictureInPicture() { - return isInPictureInPictureMode(); + setBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE, pictureInPicture); } /** @@ -243,7 +234,7 @@ public final class AccessibilityWindowInfo implements Parcelable { * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise. */ public boolean isInPictureInPictureMode() { - return mInPictureInPicture; + return getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE); } /** @@ -463,7 +454,6 @@ public final class AccessibilityWindowInfo implements Parcelable { infoClone.mBoundsInScreen.set(info.mBoundsInScreen); infoClone.mTitle = info.mTitle; infoClone.mAnchorId = info.mAnchorId; - infoClone.mInPictureInPicture = info.mInPictureInPicture; if (info.mChildIds != null && info.mChildIds.size() > 0) { if (infoClone.mChildIds == null) { @@ -520,8 +510,7 @@ public final class AccessibilityWindowInfo implements Parcelable { parcel.writeInt(mParentId); mBoundsInScreen.writeToParcel(parcel, flags); parcel.writeCharSequence(mTitle); - parcel.writeInt(mAnchorId); - parcel.writeInt(mInPictureInPicture ? 1 : 0); + parcel.writeLong(mAnchorId); final LongArray childIds = mChildIds; if (childIds == null) { @@ -545,8 +534,7 @@ public final class AccessibilityWindowInfo implements Parcelable { mParentId = parcel.readInt(); mBoundsInScreen.readFromParcel(parcel); mTitle = parcel.readCharSequence(); - mAnchorId = parcel.readInt(); - mInPictureInPicture = parcel.readInt() == 1; + mAnchorId = parcel.readLong(); final int childCount = parcel.readInt(); if (childCount > 0) { @@ -593,7 +581,7 @@ public final class AccessibilityWindowInfo implements Parcelable { builder.append(", bounds=").append(mBoundsInScreen); builder.append(", focused=").append(isFocused()); builder.append(", active=").append(isActive()); - builder.append(", pictureInPicture=").append(inPictureInPicture()); + builder.append(", pictureInPicture=").append(isInPictureInPictureMode()); if (DEBUG) { builder.append(", parent=").append(mParentId); builder.append(", children=["); @@ -611,7 +599,8 @@ public final class AccessibilityWindowInfo implements Parcelable { builder.append(']'); } else { builder.append(", hasParent=").append(mParentId != UNDEFINED_WINDOW_ID); - builder.append(", isAnchored=").append(mAnchorId != UNDEFINED_WINDOW_ID); + builder.append(", isAnchored=") + .append(mAnchorId != AccessibilityNodeInfo.UNDEFINED_NODE_ID); builder.append(", hasChildren=").append(mChildIds != null && mChildIds.size() > 0); } @@ -633,8 +622,7 @@ public final class AccessibilityWindowInfo implements Parcelable { mChildIds.clear(); } mConnectionId = UNDEFINED_WINDOW_ID; - mAnchorId = UNDEFINED_WINDOW_ID; - mInPictureInPicture = false; + mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID; mTitle = null; } diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java index 547e0db9..26974545 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -36,6 +36,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -45,6 +46,7 @@ import android.view.View; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -426,7 +428,7 @@ public final class AutofillManager { * @hide */ public AutofillManager(Context context, IAutoFillManager service) { - mContext = context; + mContext = Preconditions.checkNotNull(context, "context cannot be null"); mService = service; } @@ -455,7 +457,7 @@ public final class AutofillManager { if (mSessionId != NO_SESSION) { ensureServiceClientAddedIfNeededLocked(); - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client != null) { try { final boolean sessionWasRestored = mService.restoreSession(mSessionId, @@ -528,10 +530,13 @@ public final class AutofillManager { * @return whether autofill is enabled for the current user. */ public boolean isEnabled() { - if (!hasAutofillFeature() || isDisabledByService()) { + if (!hasAutofillFeature()) { return false; } synchronized (mLock) { + if (isDisabledByServiceLocked()) { + return false; + } ensureServiceClientAddedIfNeededLocked(); return mEnabled; } @@ -603,19 +608,16 @@ public final class AutofillManager { } private boolean shouldIgnoreViewEnteredLocked(@NonNull View view, int flags) { - if (isDisabledByService()) { + if (isDisabledByServiceLocked()) { if (sVerbose) { Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view + ") on state " + getStateAsStringLocked()); } return true; } - if (mState == STATE_FINISHED && (flags & FLAG_MANUAL_REQUEST) == 0) { - if (sVerbose) { - Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view - + ") on state " + getStateAsStringLocked()); - } - return true; + if (sVerbose && isFinishedLocked()) { + Log.v(TAG, "not ignoring notifyViewEntered(flags=" + flags + ", view=" + view + + ") on state " + getStateAsStringLocked()); } return false; } @@ -1006,6 +1008,76 @@ public final class AutofillManager { } } + /** + * Returns the component name of the {@link AutofillService} that is enabled for the current + * user. + */ + @Nullable + public ComponentName getAutofillServiceComponentName() { + if (mService == null) return null; + + try { + return mService.getAutofillServiceComponentName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets the user data used for + * field classification. + * + *

    Note: This method should only be called by an app providing an autofill service. + * + * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was + * reset or if the caller currently does not have an enabled autofill service for the user. + */ + @Nullable public UserData getUserData() { + try { + return mService.getUserData(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return null; + } + } + + /** + * Sets the user data used for + * field classification + * + *

    Note: This method should only be called by an app providing an autofill service, + * and it's ignored if the caller currently doesn't have an enabled autofill service for + * the user. + */ + public void setUserData(@Nullable UserData userData) { + try { + mService.setUserData(userData); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Checks if field classification is + * enabled. + * + *

    As field classification is an expensive operation, it could be disabled, either + * temporarily (for example, because the service exceeded a rate-limit threshold) or + * permanently (for example, because the device is a low-level device). + * + *

    Note: This method should only be called by an app providing an autofill service, + * and it's ignored if the caller currently doesn't have an enabled autofill service for + * the user. + */ + public boolean isFieldClassificationEnabled() { + try { + return mService.isFieldClassificationEnabled(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return false; + } + } + /** * Returns {@code true} if autofill is supported by the current device and * is supported for this user. @@ -1026,7 +1098,8 @@ public final class AutofillManager { } } - private AutofillClient getClientLocked() { + // Note: don't need to use locked suffix because mContext is final. + private AutofillClient getClient() { final AutofillClient client = mContext.getAutofillClient(); if (client == null && sDebug) { Log.d(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context " @@ -1081,24 +1154,24 @@ public final class AutofillManager { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value + ", flags=" + flags + ", state=" + getStateAsStringLocked()); } - if (mState != STATE_UNKNOWN && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { if (sVerbose) { Log.v(TAG, "not automatically starting session for " + id - + " on state " + getStateAsStringLocked()); + + " on state " + getStateAsStringLocked() + " and flags " + flags); } return; } try { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); + if (client == null) return; // NOTE: getClient() already logd it.. + mSessionId = mService.startSession(mContext.getActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, client.getComponentName()); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; } - if (client != null) { - client.autofillCallbackResetableStateAvailable(); - } + client.autofillCallbackResetableStateAvailable(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1150,7 +1223,9 @@ public final class AutofillManager { try { if (restartIfNecessary) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); + if (client == null) return; // NOTE: getClient() already logd it.. + final int newId = mService.updateOrRestartSession(mContext.getActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), mCallback != null, flags, client.getComponentName(), mSessionId, action); @@ -1158,9 +1233,7 @@ public final class AutofillManager { if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId); mSessionId = newId; mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE; - if (client != null) { - client.autofillCallbackResetableStateAvailable(); - } + client.autofillCallbackResetableStateAvailable(); } } else { mService.updateSession(mSessionId, id, bounds, value, action, flags, @@ -1173,7 +1246,7 @@ public final class AutofillManager { } private void ensureServiceClientAddedIfNeededLocked() { - if (getClientLocked() == null) { + if (getClient() == null) { return; } @@ -1256,7 +1329,7 @@ public final class AutofillManager { AutofillCallback callback = null; synchronized (mLock) { if (mSessionId == sessionId) { - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (client != null) { if (client.autofillCallbackRequestShowFillUi(anchor, width, height, @@ -1281,7 +1354,7 @@ public final class AutofillManager { Intent fillInIntent) { synchronized (mLock) { if (sessionId == mSessionId) { - AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client != null) { client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent); } @@ -1346,7 +1419,7 @@ public final class AutofillManager { return; } - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { return; } @@ -1523,7 +1596,7 @@ public final class AutofillManager { // 1. If local and remote session id are off sync the UI would be stuck shown // 2. There is a race between the user state being destroyed due the fill // service being uninstalled and the UI being dismissed. - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (client != null) { if (client.autofillCallbackRequestHideFillUi() && mCallback != null) { callback = mCallback; @@ -1553,7 +1626,7 @@ public final class AutofillManager { AutofillCallback callback = null; synchronized (mLock) { - if (mSessionId == sessionId && getClientLocked() != null) { + if (mSessionId == sessionId && getClient() != null) { callback = mCallback; } } @@ -1610,7 +1683,7 @@ public final class AutofillManager { * @return The view or {@code null} if view was not found */ private View findView(@NonNull AutofillId autofillId) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { return null; @@ -1644,7 +1717,7 @@ public final class AutofillManager { pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked()); pw.print(pfx); pw.print("context: "); pw.println(mContext); - pw.print(pfx); pw.print("client: "); pw.println(getClientLocked()); + pw.print(pfx); pw.print("client: "); pw.println(getClient()); pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled); pw.print(pfx); pw.print("hasService: "); pw.println(mService != null); pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null); @@ -1686,12 +1759,16 @@ public final class AutofillManager { return mState == STATE_ACTIVE; } - private boolean isDisabledByService() { + private boolean isDisabledByServiceLocked() { return mState == STATE_DISABLED_BY_SERVICE; } + private boolean isFinishedLocked() { + return mState == STATE_FINISHED; + } + private void post(Runnable runnable) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { if (sVerbose) Log.v(TAG, "ignoring post() because client is null"); return; @@ -1774,7 +1851,7 @@ public final class AutofillManager { * @param trackedIds The views to be tracked */ TrackedViews(@Nullable AutofillId[] trackedIds) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (trackedIds != null && client != null) { final boolean[] isVisible; @@ -1815,7 +1892,7 @@ public final class AutofillManager { * @param isVisible visible if the view is visible in the view hierarchy. */ void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) { - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (sDebug) { Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible=" @@ -1852,7 +1929,7 @@ public final class AutofillManager { void onVisibleForAutofillLocked() { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); ArraySet updatedVisibleTrackedIds = null; ArraySet updatedInvisibleTrackedIds = null; if (client != null) { @@ -1918,7 +1995,11 @@ public final class AutofillManager { public abstract static class AutofillCallback { /** @hide */ - @IntDef({EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN, EVENT_INPUT_UNAVAILABLE}) + @IntDef(prefix = { "EVENT_INPUT_" }, value = { + EVENT_INPUT_SHOWN, + EVENT_INPUT_HIDDEN, + EVENT_INPUT_UNAVAILABLE + }) @Retention(RetentionPolicy.SOURCE) public @interface AutofillEventType {} diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java index b4688bb1..5cba21e3 100644 --- a/android/view/autofill/AutofillPopupWindow.java +++ b/android/view/autofill/AutofillPopupWindow.java @@ -108,11 +108,12 @@ public class AutofillPopupWindow extends PopupWindow { // symmetrically when the dropdown is below and above the anchor. final View actualAnchor; if (virtualBounds != null) { + final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top}; actualAnchor = new View(anchor.getContext()) { @Override public void getLocationOnScreen(int[] location) { - location[0] = virtualBounds.left; - location[1] = virtualBounds.top; + location[0] = mLocationOnScreen[0]; + location[1] = mLocationOnScreen[1]; } @Override @@ -178,6 +179,12 @@ public class AutofillPopupWindow extends PopupWindow { virtualBounds.right, virtualBounds.bottom); actualAnchor.setScrollX(anchor.getScrollX()); actualAnchor.setScrollY(anchor.getScrollY()); + + anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX); + mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY); + }); + actualAnchor.setWillNotDraw(true); } else { actualAnchor = anchor; } diff --git a/android/view/autofill/AutofillValue.java b/android/view/autofill/AutofillValue.java index 3beae11c..8e649de5 100644 --- a/android/view/autofill/AutofillValue.java +++ b/android/view/autofill/AutofillValue.java @@ -177,7 +177,7 @@ public final class AutofillValue implements Parcelable { .append("[type=").append(mType) .append(", value="); if (isText()) { - string.append(((CharSequence) mValue).length()).append("_chars"); + Helper.appendRedacted(string, (CharSequence) mValue); } else { string.append(mValue); } diff --git a/android/view/autofill/Helper.java b/android/view/autofill/Helper.java index 829e7f3a..4b2c53c7 100644 --- a/android/view/autofill/Helper.java +++ b/android/view/autofill/Helper.java @@ -16,11 +16,8 @@ package android.view.autofill; -import android.os.Bundle; - -import java.util.Arrays; -import java.util.Objects; -import java.util.Set; +import android.annotation.NonNull; +import android.annotation.Nullable; /** @hide */ public final class Helper { @@ -29,25 +26,37 @@ public final class Helper { public static boolean sDebug = false; public static boolean sVerbose = false; - public static final String REDACTED = "[REDACTED]"; + /** + * Appends {@code value} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, + @Nullable CharSequence value) { + builder.append(getRedacted(value)); + } - static StringBuilder append(StringBuilder builder, Bundle bundle) { - if (bundle == null || !sDebug) { + /** + * Gets the redacted version of a value. + */ + @NonNull + public static String getRedacted(@Nullable CharSequence value) { + return (value == null) ? "null" : value.length() + "_chars"; + } + + /** + * Appends {@code values} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) { + if (values == null) { builder.append("N/A"); - } else if (!sVerbose) { - builder.append(REDACTED); - } else { - final Set keySet = bundle.keySet(); - builder.append("[Bundle with ").append(keySet.size()).append(" extras:"); - for (String key : keySet) { - final Object value = bundle.get(key); - builder.append(' ').append(key).append('='); - builder.append((value instanceof Object[]) - ? Arrays.toString((Objects[]) value) : value); - } - builder.append(']'); + return; + } + builder.append("["); + for (String value : values) { + builder.append(" '"); + appendRedacted(builder, value); + builder.append("'"); } - return builder; + builder.append(" ]"); } private Helper() { diff --git a/android/view/inputmethod/InputMethodInfo.java b/android/view/inputmethod/InputMethodInfo.java index f0645b89..c69543f6 100644 --- a/android/view/inputmethod/InputMethodInfo.java +++ b/android/view/inputmethod/InputMethodInfo.java @@ -66,6 +66,11 @@ public final class InputMethodInfo implements Parcelable { */ final ResolveInfo mService; + /** + * IME only supports VR mode. + */ + final boolean mIsVrOnly; + /** * The unique string Id to identify the input method. This is generated * from the input method component. @@ -149,6 +154,7 @@ public final class InputMethodInfo implements Parcelable { PackageManager pm = context.getPackageManager(); String settingsActivityComponent = null; + boolean isVrOnly; int isDefaultResId = 0; XmlResourceParser parser = null; @@ -179,6 +185,7 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod); settingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_settingsActivity); + isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false); isDefaultResId = sa.getResourceId( com.android.internal.R.styleable.InputMethod_isDefault, 0); supportsSwitchingToNextInputMethod = sa.getBoolean( @@ -254,6 +261,8 @@ public final class InputMethodInfo implements Parcelable { mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; + // TODO(b/68948291): remove this meta-data before release. + mIsVrOnly = isVrOnly || service.serviceInfo.metaData.getBoolean("isVrOnly", false); } InputMethodInfo(Parcel source) { @@ -262,6 +271,7 @@ public final class InputMethodInfo implements Parcelable { mIsDefaultResId = source.readInt(); mIsAuxIme = source.readInt() == 1; mSupportsSwitchingToNextInputMethod = source.readInt() == 1; + mIsVrOnly = source.readBoolean(); mService = ResolveInfo.CREATOR.createFromParcel(source); mSubtypes = new InputMethodSubtypeArray(source); mForceDefault = false; @@ -274,7 +284,8 @@ public final class InputMethodInfo implements Parcelable { CharSequence label, String settingsActivity) { this(buildDummyResolveInfo(packageName, className, label), false /* isAuxIme */, settingsActivity, null /* subtypes */, 0 /* isDefaultResId */, - false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */); + false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */, + false /* isVrOnly */); } /** @@ -285,7 +296,7 @@ public final class InputMethodInfo implements Parcelable { String settingsActivity, List subtypes, int isDefaultResId, boolean forceDefault) { this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault, - true /* supportsSwitchingToNextInputMethod */); + true /* supportsSwitchingToNextInputMethod */, false /* isVrOnly */); } /** @@ -294,7 +305,7 @@ public final class InputMethodInfo implements Parcelable { */ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity, List subtypes, int isDefaultResId, boolean forceDefault, - boolean supportsSwitchingToNextInputMethod) { + boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) { final ServiceInfo si = ri.serviceInfo; mService = ri; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); @@ -304,6 +315,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes = new InputMethodSubtypeArray(subtypes); mForceDefault = forceDefault; mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; + mIsVrOnly = isVrOnly; } private static ResolveInfo buildDummyResolveInfo(String packageName, String className, @@ -397,6 +409,14 @@ public final class InputMethodInfo implements Parcelable { return mSettingsActivityName; } + /** + * Returns true if IME supports VR mode only. + * @hide + */ + public boolean isVrOnly() { + return mIsVrOnly; + } + /** * Return the count of the subtypes of Input Method. */ @@ -444,6 +464,7 @@ public final class InputMethodInfo implements Parcelable { public void dump(Printer pw, String prefix) { pw.println(prefix + "mId=" + mId + " mSettingsActivityName=" + mSettingsActivityName + + " mIsVrOnly=" + mIsVrOnly + " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod); pw.println(prefix + "mIsDefaultResId=0x" + Integer.toHexString(mIsDefaultResId)); @@ -509,6 +530,7 @@ public final class InputMethodInfo implements Parcelable { dest.writeInt(mIsDefaultResId); dest.writeInt(mIsAuxIme ? 1 : 0); dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0); + dest.writeBoolean(mIsVrOnly); mService.writeToParcel(dest, flags); mSubtypes.writeToParcel(dest); } diff --git a/android/view/inputmethod/InputMethodManager.java b/android/view/inputmethod/InputMethodManager.java index 92d1de8e..80d7b6b7 100644 --- a/android/view/inputmethod/InputMethodManager.java +++ b/android/view/inputmethod/InputMethodManager.java @@ -24,6 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.content.Context; import android.graphics.Rect; +import android.inputmethodservice.InputMethodService; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -280,10 +281,10 @@ public final class InputMethodManager { boolean mActive = false; /** - * Set whenever this client becomes inactive, to know we need to reset - * state with the IME the next time we receive focus. + * {@code true} if next {@link #onPostWindowFocus(View, View, int, boolean, int)} needs to + * restart input. */ - boolean mHasBeenInactive = true; + boolean mRestartOnNextWindowFocus = true; /** * As reported by IME through InputConnection. @@ -488,7 +489,7 @@ public final class InputMethodManager { // Some other client has starting using the IME, so note // that this happened and make sure our own editor's // state is reset. - mHasBeenInactive = true; + mRestartOnNextWindowFocus = true; try { // Note that finishComposingText() is allowed to run // even when we are not active. @@ -499,7 +500,7 @@ public final class InputMethodManager { // Check focus again in case that "onWindowFocus" is called before // handling this message. if (mServedView != null && mServedView.hasWindowFocus()) { - if (checkFocusNoStartInput(mHasBeenInactive)) { + if (checkFocusNoStartInput(mRestartOnNextWindowFocus)) { final int reason = active ? InputMethodClient.START_INPUT_REASON_ACTIVATED_BY_IMMS : InputMethodClient.START_INPUT_REASON_DEACTIVATED_BY_IMMS; @@ -697,6 +698,19 @@ public final class InputMethodManager { } } + /** + * Returns a list of VR InputMethod currently installed. + * @hide + */ + @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + public List getVrInputMethodList() { + try { + return mService.getVrInputMethodList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + public List getEnabledInputMethodList() { try { return mService.getEnabledInputMethodList(); @@ -722,7 +736,20 @@ public final class InputMethodManager { } } + /** + * @deprecated Use {@link InputMethodService#showStatusIcon(int)} instead. This method was + * intended for IME developers who should be accessing APIs through the service. APIs in this + * class are intended for app developers interacting with the IME. + */ + @Deprecated public void showStatusIcon(IBinder imeToken, String packageName, int iconId) { + showStatusIconInternal(imeToken, packageName, iconId); + } + + /** + * @hide + */ + public void showStatusIconInternal(IBinder imeToken, String packageName, int iconId) { try { mService.updateStatusIcon(imeToken, packageName, iconId); } catch (RemoteException e) { @@ -730,7 +757,20 @@ public final class InputMethodManager { } } + /** + * @deprecated Use {@link InputMethodService#hideStatusIcon()} instead. This method was + * intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. + */ + @Deprecated public void hideStatusIcon(IBinder imeToken) { + hideStatusIconInternal(imeToken); + } + + /** + * @hide + */ + public void hideStatusIconInternal(IBinder imeToken) { try { mService.updateStatusIcon(imeToken, null, 0); } catch (RemoteException e) { @@ -1108,7 +1148,6 @@ public final class InputMethodManager { } } - /** * This method toggles the input method window display. * If the input window is already displayed, it gets hidden. @@ -1294,48 +1333,31 @@ public final class InputMethodManager { + Integer.toHexString(controlFlags)); final InputBindResult res = mService.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode, - windowFlags, tba, servedContext, missingMethodFlags); + windowFlags, tba, servedContext, missingMethodFlags, + view.getContext().getApplicationInfo().targetSdkVersion); if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); - if (res != null) { - if (res.id != null) { - setInputChannelLocked(res.channel); - mBindSequence = res.sequence; - mCurMethod = res.method; - mCurId = res.id; - mNextUserActionNotificationSequenceNumber = - res.userActionNotificationSequenceNumber; - } else { - if (res.channel != null && res.channel != mCurChannel) { - res.channel.dispose(); - } - if (mCurMethod == null) { - // This means there is no input method available. - if (DEBUG) Log.v(TAG, "ABORT input: no input method!"); - return true; - } - } - } else { - if (startInputReason - == InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN) { - // We are here probably because of an obsolete window-focus-in message sent - // to windowGainingFocus. Since IMMS determines whether a Window can have - // IME focus or not by using the latest window focus state maintained in the - // WMS, this kind of race condition cannot be avoided. One obvious example - // would be that we have already received a window-focus-out message but the - // UI thread is still handling previous window-focus-in message here. - // TODO: InputBindResult should have the error code. - if (DEBUG) Log.w(TAG, "startInputOrWindowGainedFocus failed. " - + "Window focus may have already been lost. " - + "win=" + windowGainingFocus + " view=" + dumpViewInfo(view)); - if (!mActive) { - // mHasBeenInactive is a latch switch to forcefully refresh IME focus - // state when an inactive (mActive == false) client is gaining window - // focus. In case we have unnecessary disable the latch due to this - // spurious wakeup, we re-enable the latch here. - // TODO: Come up with more robust solution. - mHasBeenInactive = true; - } - } + if (res == null) { + Log.wtf(TAG, "startInputOrWindowGainedFocus must not return" + + " null. startInputReason=" + + InputMethodClient.getStartInputReason(startInputReason) + + " editorInfo=" + tba + + " controlFlags=#" + Integer.toHexString(controlFlags)); + return false; + } + if (res.id != null) { + setInputChannelLocked(res.channel); + mBindSequence = res.sequence; + mCurMethod = res.method; + mCurId = res.id; + mNextUserActionNotificationSequenceNumber = + res.userActionNotificationSequenceNumber; + } else if (res.channel != null && res.channel != mCurChannel) { + res.channel.dispose(); + } + switch (res.result) { + case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW: + mRestartOnNextWindowFocus = true; + break; } if (mCurMethod != null && mCompletions != null) { try { @@ -1511,9 +1533,9 @@ public final class InputMethodManager { + " softInputMode=" + InputMethodClient.softInputModeToString(softInputMode) + " first=" + first + " flags=#" + Integer.toHexString(windowFlags)); - if (mHasBeenInactive) { - if (DEBUG) Log.v(TAG, "Has been inactive! Starting fresh"); - mHasBeenInactive = false; + if (mRestartOnNextWindowFocus) { + if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus"); + mRestartOnNextWindowFocus = false; forceNewFocus = true; } focusInLocked(focusedView != null ? focusedView : rootView); @@ -1549,7 +1571,8 @@ public final class InputMethodManager { mService.startInputOrWindowGainedFocus( InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null, - null, 0 /* missingMethodFlags */); + null, 0 /* missingMethodFlags */, + rootView.getContext().getApplicationInfo().targetSdkVersion); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1787,8 +1810,19 @@ public final class InputMethodManager { * when it was started, which allows it to perform this operation on * itself. * @param id The unique identifier for the new input method to be switched to. + * @deprecated Use {@link InputMethodService#setInputMethod(String)} instead. This method + * was intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. */ + @Deprecated public void setInputMethod(IBinder token, String id) { + setInputMethodInternal(token, id); + } + + /** + * @hide + */ + public void setInputMethodInternal(IBinder token, String id) { try { mService.setInputMethod(token, id); } catch (RemoteException e) { @@ -1804,8 +1838,21 @@ public final class InputMethodManager { * itself. * @param id The unique identifier for the new input method to be switched to. * @param subtype The new subtype of the new input method to be switched to. + * @deprecated Use + * {@link InputMethodService#setInputMethodAndSubtype(String, InputMethodSubtype)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype) { + setInputMethodAndSubtypeInternal(token, id, subtype); + } + + /** + * @hide + */ + public void setInputMethodAndSubtypeInternal( + IBinder token, String id, InputMethodSubtype subtype) { try { mService.setInputMethodAndSubtype(token, id, subtype); } catch (RemoteException e) { @@ -1824,8 +1871,19 @@ public final class InputMethodManager { * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #HIDE_IMPLICIT_ONLY}, * {@link #HIDE_NOT_ALWAYS} bit set. + * @deprecated Use {@link InputMethodService#hideSoftInputFromInputMethod(int)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void hideSoftInputFromInputMethod(IBinder token, int flags) { + hideSoftInputFromInputMethodInternal(token, flags); + } + + /** + * @hide + */ + public void hideSoftInputFromInputMethodInternal(IBinder token, int flags) { try { mService.hideMySoftInput(token, flags); } catch (RemoteException e) { @@ -1845,8 +1903,19 @@ public final class InputMethodManager { * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #SHOW_IMPLICIT} or * {@link #SHOW_FORCED} bit set. + * @deprecated Use {@link InputMethodService#showSoftInputFromInputMethod(int)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void showSoftInputFromInputMethod(IBinder token, int flags) { + showSoftInputFromInputMethodInternal(token, flags); + } + + /** + * @hide + */ + public void showSoftInputFromInputMethodInternal(IBinder token, int flags) { try { mService.showMySoftInput(token, flags); } catch (RemoteException e) { @@ -2226,8 +2295,19 @@ public final class InputMethodManager { * which allows it to perform this operation on itself. * @return true if the current input method and subtype was successfully switched to the last * used input method and subtype. + * @deprecated Use {@link InputMethodService#switchToLastInputMethod()} instead. This method + * was intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean switchToLastInputMethod(IBinder imeToken) { + return switchToLastInputMethodInternal(imeToken); + } + + /** + * @hide + */ + public boolean switchToLastInputMethodInternal(IBinder imeToken) { synchronized (mH) { try { return mService.switchToLastInputMethod(imeToken); @@ -2246,8 +2326,19 @@ public final class InputMethodManager { * belongs to the current IME * @return true if the current input method and subtype was successfully switched to the next * input method and subtype. + * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This + * method was intended for IME developers who should be accessing APIs through the service. + * APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) { + return switchToNextInputMethodInternal(imeToken, onlyCurrentIme); + } + + /** + * @hide + */ + public boolean switchToNextInputMethodInternal(IBinder imeToken, boolean onlyCurrentIme) { synchronized (mH) { try { return mService.switchToNextInputMethod(imeToken, onlyCurrentIme); @@ -2267,8 +2358,19 @@ public final class InputMethodManager { * between IMEs and subtypes. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. + * @deprecated Use {@link InputMethodService#shouldOfferSwitchingToNextInputMethod()} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean shouldOfferSwitchingToNextInputMethod(IBinder imeToken) { + return shouldOfferSwitchingToNextInputMethodInternal(imeToken); + } + + /** + * @hide + */ + public boolean shouldOfferSwitchingToNextInputMethodInternal(IBinder imeToken) { synchronized (mH) { try { return mService.shouldOfferSwitchingToNextInputMethod(imeToken); @@ -2365,7 +2467,7 @@ public final class InputMethodManager { p.println(" mMainLooper=" + mMainLooper); p.println(" mIInputContext=" + mIInputContext); p.println(" mActive=" + mActive - + " mHasBeenInactive=" + mHasBeenInactive + + " mRestartOnNextWindowFocus=" + mRestartOnNextWindowFocus + " mBindSequence=" + mBindSequence + " mCurId=" + mCurId); p.println(" mFullscreenMode=" + mFullscreenMode); diff --git a/android/view/inputmethod/InputMethodManagerInternal.java b/android/view/inputmethod/InputMethodManagerInternal.java index 77df4e38..e13813e5 100644 --- a/android/view/inputmethod/InputMethodManagerInternal.java +++ b/android/view/inputmethod/InputMethodManagerInternal.java @@ -16,6 +16,8 @@ package android.view.inputmethod; +import android.content.ComponentName; + /** * Input method manager local system service interface. * @@ -37,4 +39,9 @@ public interface InputMethodManagerInternal { * Hides the current input method, if visible. */ void hideCurrentInputMethod(); + + /** + * Switches to VR InputMethod defined in the packageName of {@param componentName}. + */ + void startVrInputMethodNoCheck(ComponentName componentName); } diff --git a/android/view/textclassifier/EntityConfidence.java b/android/view/textclassifier/EntityConfidence.java index 0589d204..19660d95 100644 --- a/android/view/textclassifier/EntityConfidence.java +++ b/android/view/textclassifier/EntityConfidence.java @@ -18,13 +18,12 @@ package android.view.textclassifier; import android.annotation.FloatRange; import android.annotation.NonNull; +import android.util.ArrayMap; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,42 +35,43 @@ import java.util.Map; */ final class EntityConfidence { - private final Map mEntityConfidence = new HashMap<>(); - - private final Comparator mEntityComparator = (e1, e2) -> { - float score1 = mEntityConfidence.get(e1); - float score2 = mEntityConfidence.get(e2); - if (score1 > score2) { - return -1; - } - if (score1 < score2) { - return 1; - } - return 0; - }; + private final ArrayMap mEntityConfidence = new ArrayMap<>(); + private final ArrayList mSortedEntities = new ArrayList<>(); EntityConfidence() {} EntityConfidence(@NonNull EntityConfidence source) { Preconditions.checkNotNull(source); mEntityConfidence.putAll(source.mEntityConfidence); + mSortedEntities.addAll(source.mSortedEntities); } /** - * Sets an entity type for the classified text and assigns a confidence score. + * Constructs an EntityConfidence from a map of entity to confidence. * - * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). - * 0 implies the entity does not exist for the classified text. - * Values greater than 1 are clamped to 1. + * Map entries that have 0 confidence are removed, and values greater than 1 are clamped to 1. + * + * @param source a map from entity to a confidence value in the range 0 (low confidence) to + * 1 (high confidence). */ - public void setEntityType( - @NonNull T type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - Preconditions.checkNotNull(type); - if (confidenceScore > 0) { - mEntityConfidence.put(type, Math.min(1, confidenceScore)); - } else { - mEntityConfidence.remove(type); + EntityConfidence(@NonNull Map source) { + Preconditions.checkNotNull(source); + + // Prune non-existent entities and clamp to 1. + mEntityConfidence.ensureCapacity(source.size()); + for (Map.Entry it : source.entrySet()) { + if (it.getValue() <= 0) continue; + mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue())); } + + // Create a list of entities sorted by decreasing confidence for getEntities(). + mSortedEntities.ensureCapacity(mEntityConfidence.size()); + mSortedEntities.addAll(mEntityConfidence.keySet()); + mSortedEntities.sort((e1, e2) -> { + float score1 = mEntityConfidence.get(e1); + float score2 = mEntityConfidence.get(e2); + return Float.compare(score2, score1); + }); } /** @@ -80,10 +80,7 @@ final class EntityConfidence { */ @NonNull public List getEntities() { - List entities = new ArrayList<>(mEntityConfidence.size()); - entities.addAll(mEntityConfidence.keySet()); - entities.sort(mEntityComparator); - return Collections.unmodifiableList(entities); + return Collections.unmodifiableList(mSortedEntities); } /** diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index f675c355..7ffbf635 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.LocaleList; +import android.util.ArrayMap; import android.view.View.OnClickListener; import android.view.textclassifier.TextClassifier.EntityType; @@ -32,12 +33,14 @@ import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; /** * Information for generating a widget to handle classified text. * *

    A TextClassification object contains icons, labels, onClickListeners and intents that may - * be used to build a widget that can be used to act on classified text. + * be used to build a widget that can be used to act on classified text. There is the concept of a + * primary action and other secondary actions. * *

    e.g. building a view that, when clicked, shares the classified text with the preferred app: * @@ -62,11 +65,18 @@ import java.util.Locale; * view.startActionMode(new ActionMode.Callback() { * * public boolean onCreateActionMode(ActionMode mode, Menu menu) { - * for (int i = 0; i < classification.getActionCount(); i++) { - * if (thisAppHasPermissionToInvokeIntent(classification.getIntent(i))) { - * menu.add(Menu.NONE, i, 20, classification.getLabel(i)) - * .setIcon(classification.getIcon(i)) - * .setIntent(classification.getIntent(i)); + * // Add the "primary" action. + * if (thisAppHasPermissionToInvokeIntent(classification.getIntent())) { + * menu.add(Menu.NONE, 0, 20, classification.getLabel()) + * .setIcon(classification.getIcon()) + * .setIntent(classification.getIntent()); + * } + * // Add the "secondary" actions. + * for (int i = 0; i < classification.getSecondaryActionsCount(); i++) { + * if (thisAppHasPermissionToInvokeIntent(classification.getSecondaryIntent(i))) { + * menu.add(Menu.NONE, i + 1, 20, classification.getSecondaryLabel(i)) + * .setIcon(classification.getSecondaryIcon(i)) + * .setIntent(classification.getSecondaryIntent(i)); * } * } * return true; @@ -90,36 +100,43 @@ public final class TextClassification { static final TextClassification EMPTY = new TextClassification.Builder().build(); @NonNull private final String mText; - @NonNull private final List mIcons; - @NonNull private final List mLabels; - @NonNull private final List mIntents; - @NonNull private final List mOnClickListeners; + @Nullable private final Drawable mPrimaryIcon; + @Nullable private final String mPrimaryLabel; + @Nullable private final Intent mPrimaryIntent; + @Nullable private final OnClickListener mPrimaryOnClickListener; + @NonNull private final List mSecondaryIcons; + @NonNull private final List mSecondaryLabels; + @NonNull private final List mSecondaryIntents; + @NonNull private final List mSecondaryOnClickListeners; @NonNull private final EntityConfidence mEntityConfidence; - @NonNull private final List mEntities; - private int mLogType; - @NonNull private final String mVersionInfo; + @NonNull private final String mSignature; private TextClassification( @Nullable String text, - @NonNull List icons, - @NonNull List labels, - @NonNull List intents, - @NonNull List onClickListeners, - @NonNull EntityConfidence entityConfidence, - int logType, - @NonNull String versionInfo) { - Preconditions.checkArgument(labels.size() == intents.size()); - Preconditions.checkArgument(icons.size() == intents.size()); - Preconditions.checkArgument(onClickListeners.size() == intents.size()); + @Nullable Drawable primaryIcon, + @Nullable String primaryLabel, + @Nullable Intent primaryIntent, + @Nullable OnClickListener primaryOnClickListener, + @NonNull List secondaryIcons, + @NonNull List secondaryLabels, + @NonNull List secondaryIntents, + @NonNull List secondaryOnClickListeners, + @NonNull Map entityConfidence, + @NonNull String signature) { + Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size()); + Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size()); + Preconditions.checkArgument(secondaryOnClickListeners.size() == secondaryIntents.size()); mText = text; - mIcons = icons; - mLabels = labels; - mIntents = intents; - mOnClickListeners = onClickListeners; + mPrimaryIcon = primaryIcon; + mPrimaryLabel = primaryLabel; + mPrimaryIntent = primaryIntent; + mPrimaryOnClickListener = primaryOnClickListener; + mSecondaryIcons = secondaryIcons; + mSecondaryLabels = secondaryLabels; + mSecondaryIntents = secondaryIntents; + mSecondaryOnClickListeners = secondaryOnClickListeners; mEntityConfidence = new EntityConfidence<>(entityConfidence); - mEntities = mEntityConfidence.getEntities(); - mLogType = logType; - mVersionInfo = versionInfo; + mSignature = signature; } /** @@ -135,7 +152,7 @@ public final class TextClassification { */ @IntRange(from = 0) public int getEntityCount() { - return mEntities.size(); + return mEntityConfidence.getEntities().size(); } /** @@ -147,7 +164,7 @@ public final class TextClassification { */ @NonNull public @EntityType String getEntity(int index) { - return mEntities.get(index); + return mEntityConfidence.getEntities().get(index); } /** @@ -161,130 +178,160 @@ public final class TextClassification { } /** - * Returns the number of actions that are available to act on the classified text. - * @see #getIntent(int) - * @see #getLabel(int) - * @see #getIcon(int) - * @see #getOnClickListener(int) + * Returns the number of secondary actions that are available to act on the classified + * text. + * + *

    Note: that there may or may not be a primary action. + * + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) */ @IntRange(from = 0) - public int getActionCount() { - return mIntents.size(); + public int getSecondaryActionsCount() { + return mSecondaryIntents.size(); } /** - * Returns one of the icons that maybe rendered on a widget used to act on the classified text. + * Returns one of the secondary icons that maybe rendered on a widget used to act on the + * classified text. + * * @param index Index of the action to get the icon for. + * * @throws IndexOutOfBoundsException if the specified index is out of range. - * @see #getActionCount() for the number of entities available. - * @see #getIntent(int) - * @see #getLabel(int) - * @see #getOnClickListener(int) + * + * @see #getSecondaryActionsCount() for the number of actions available. + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryOnClickListener(int) + * @see #getIcon() */ @Nullable - public Drawable getIcon(int index) { - return mIcons.get(index); + public Drawable getSecondaryIcon(int index) { + return mSecondaryIcons.get(index); } /** - * Returns an icon for the default intent that may be rendered on a widget used to act on the - * classified text. + * Returns an icon for the primary intent that may be rendered on a widget used to act + * on the classified text. + * + * @see #getSecondaryIcon(int) */ @Nullable public Drawable getIcon() { - return mIcons.isEmpty() ? null : mIcons.get(0); + return mPrimaryIcon; } /** - * Returns one of the labels that may be rendered on a widget used to act on the classified - * text. + * Returns one of the secondary labels that may be rendered on a widget used to act on + * the classified text. + * * @param index Index of the action to get the label for. + * * @throws IndexOutOfBoundsException if the specified index is out of range. - * @see #getActionCount() - * @see #getIntent(int) - * @see #getIcon(int) - * @see #getOnClickListener(int) + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryIntent(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) + * @see #getLabel() */ @Nullable - public CharSequence getLabel(int index) { - return mLabels.get(index); + public CharSequence getSecondaryLabel(int index) { + return mSecondaryLabels.get(index); } /** - * Returns a label for the default intent that may be rendered on a widget used to act on the - * classified text. + * Returns a label for the primary intent that may be rendered on a widget used to act + * on the classified text. + * + * @see #getSecondaryLabel(int) */ @Nullable public CharSequence getLabel() { - return mLabels.isEmpty() ? null : mLabels.get(0); + return mPrimaryLabel; } /** - * Returns one of the intents that may be fired to act on the classified text. + * Returns one of the secondary intents that may be fired to act on the classified text. + * * @param index Index of the action to get the intent for. + * * @throws IndexOutOfBoundsException if the specified index is out of range. - * @see #getActionCount() - * @see #getLabel(int) - * @see #getIcon(int) - * @see #getOnClickListener(int) + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) + * @see #getIntent() */ @Nullable - public Intent getIntent(int index) { - return mIntents.get(index); + public Intent getSecondaryIntent(int index) { + return mSecondaryIntents.get(index); } /** - * Returns the default intent that may be fired to act on the classified text. + * Returns the primary intent that may be fired to act on the classified text. + * + * @see #getSecondaryIntent(int) */ @Nullable public Intent getIntent() { - return mIntents.isEmpty() ? null : mIntents.get(0); + return mPrimaryIntent; } /** - * Returns one of the OnClickListeners that may be triggered to act on the classified text. + * Returns one of the secondary OnClickListeners that may be triggered to act on the + * classified text. + * * @param index Index of the action to get the click listener for. + * * @throws IndexOutOfBoundsException if the specified index is out of range. - * @see #getActionCount() - * @see #getIntent(int) - * @see #getLabel(int) - * @see #getIcon(int) + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getOnClickListener() */ @Nullable - public OnClickListener getOnClickListener(int index) { - return mOnClickListeners.get(index); + public OnClickListener getSecondaryOnClickListener(int index) { + return mSecondaryOnClickListeners.get(index); } /** - * Returns the default OnClickListener that may be triggered to act on the classified text. + * Returns the primary OnClickListener that may be triggered to act on the classified + * text. + * + * @see #getSecondaryOnClickListener(int) */ @Nullable public OnClickListener getOnClickListener() { - return mOnClickListeners.isEmpty() ? null : mOnClickListeners.get(0); + return mPrimaryOnClickListener; } /** - * Returns the MetricsLogger subtype for the action that is performed for this result. - * @hide - */ - public int getLogType() { - return mLogType; - } - - /** - * Returns information about the classifier model used to generate this TextClassification. - * @hide + * Returns the signature for this object. + * The TextClassifier that generates this object may use it as a way to internally identify + * this object. */ @NonNull - public String getVersionInfo() { - return mVersionInfo; + public String getSignature() { + return mSignature; } @Override public String toString() { - return String.format(Locale.US, - "TextClassification {text=%s, entities=%s, labels=%s, intents=%s}", - mText, mEntityConfidence, mLabels, mIntents); + return String.format(Locale.US, "TextClassification {" + + "text=%s, entities=%s, " + + "primaryLabel=%s, secondaryLabels=%s, " + + "primaryIntent=%s, secondaryIntents=%s, " + + "signature=%s}", + mText, mEntityConfidence, + mPrimaryLabel, mSecondaryLabels, + mPrimaryIntent, mSecondaryIntents, + mSignature); } /** @@ -303,18 +350,33 @@ public final class TextClassification { /** * Builder for building {@link TextClassification} objects. + * + *

    e.g. + * + *

    {@code
    +     *   TextClassification classification = new TextClassification.Builder()
    +     *          .setText(classifiedText)
    +     *          .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
    +     *          .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
    +     *          .setPrimaryAction(intent, label, icon, onClickListener)
    +     *          .addSecondaryAction(intent1, label1, icon1, onClickListener1)
    +     *          .addSecondaryAction(intent2, label2, icon2, onClickListener2)
    +     *          .build();
    +     * }
    */ public static final class Builder { @NonNull private String mText; - @NonNull private final List mIcons = new ArrayList<>(); - @NonNull private final List mLabels = new ArrayList<>(); - @NonNull private final List mIntents = new ArrayList<>(); - @NonNull private final List mOnClickListeners = new ArrayList<>(); - @NonNull private final EntityConfidence mEntityConfidence = - new EntityConfidence<>(); - private int mLogType; - @NonNull private String mVersionInfo = ""; + @NonNull private final List mSecondaryIcons = new ArrayList<>(); + @NonNull private final List mSecondaryLabels = new ArrayList<>(); + @NonNull private final List mSecondaryIntents = new ArrayList<>(); + @NonNull private final List mSecondaryOnClickListeners = new ArrayList<>(); + @NonNull private final Map mEntityConfidence = new ArrayMap<>(); + @Nullable Drawable mPrimaryIcon; + @Nullable String mPrimaryLabel; + @Nullable Intent mPrimaryIntent; + @Nullable OnClickListener mPrimaryOnClickListener; + @NonNull private String mSignature = ""; /** * Sets the classified text. @@ -326,6 +388,8 @@ public final class TextClassification { /** * Sets an entity type for the classification result and assigns a confidence score. + * If a confidence score had already been set for the specified entity type, this will + * override that score. * * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). * 0 implies the entity does not exist for the classified text. @@ -334,110 +398,128 @@ public final class TextClassification { public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - mEntityConfidence.setEntityType(type, confidenceScore); + mEntityConfidence.put(type, confidenceScore); return this; } /** - * Adds an action that may be performed on the classified text. The label and icon are used - * for rendering of widgets that offer the intent. Actions should be added in order of - * priority and the first one will be treated as the default. + * Adds an secondary action that may be performed on the classified text. + * Secondary actions are in addition to the primary action which may or may not + * exist. + * + *

    The label and icon are used for rendering of widgets that offer the intent. + * Actions should be added in order of priority. + * + *

    Note: If all input parameters are set to null, this method will be a + * no-op. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder addAction( - Intent intent, @Nullable String label, @Nullable Drawable icon, + public Builder addSecondaryAction( + @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon, @Nullable OnClickListener onClickListener) { - mIntents.add(intent); - mLabels.add(label); - mIcons.add(icon); - mOnClickListeners.add(onClickListener); + if (intent != null || label != null || icon != null || onClickListener != null) { + mSecondaryIntents.add(intent); + mSecondaryLabels.add(label); + mSecondaryIcons.add(icon); + mSecondaryOnClickListeners.add(onClickListener); + } return this; } /** - * Removes all actions. + * Removes all the secondary actions. */ - public Builder clearActions() { - mIntents.clear(); - mOnClickListeners.clear(); - mLabels.clear(); - mIcons.clear(); + public Builder clearSecondaryActions() { + mSecondaryIntents.clear(); + mSecondaryOnClickListeners.clear(); + mSecondaryLabels.clear(); + mSecondaryIcons.clear(); return this; } /** - * Sets the icon for the default action that may be rendered on a widget used to act on the - * classified text. + * Sets the primary action that may be performed on the classified text. This is + * equivalent to calling {@code + * setIntent(intent).setLabel(label).setIcon(icon).setOnClickListener(onClickListener)}. + * + *

    Note: If all input parameters are null, there will be no + * primary action but there may still be secondary actions. + * + * @see #addSecondaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setIcon(@Nullable Drawable icon) { - ensureDefaultActionAvailable(); - mIcons.set(0, icon); - return this; + public Builder setPrimaryAction( + @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon, + @Nullable OnClickListener onClickListener) { + return setIntent(intent).setLabel(label).setIcon(icon) + .setOnClickListener(onClickListener); } /** - * Sets the label for the default action that may be rendered on a widget used to act on the - * classified text. + * Sets the icon for the primary action that may be rendered on a widget used to act + * on the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setLabel(@Nullable String label) { - ensureDefaultActionAvailable(); - mLabels.set(0, label); + public Builder setIcon(@Nullable Drawable icon) { + mPrimaryIcon = icon; return this; } /** - * Sets the intent for the default action that may be fired to act on the classified text. + * Sets the label for the primary action that may be rendered on a widget used to + * act on the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setIntent(@Nullable Intent intent) { - ensureDefaultActionAvailable(); - mIntents.set(0, intent); + public Builder setLabel(@Nullable String label) { + mPrimaryLabel = label; return this; } /** - * Sets the MetricsLogger subtype for the action that is performed for this result. - * @hide + * Sets the intent for the primary action that may be fired to act on the classified + * text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setLogType(int type) { - mLogType = type; + public Builder setIntent(@Nullable Intent intent) { + mPrimaryIntent = intent; return this; } /** - * Sets the OnClickListener for the default action that may be triggered to act on the - * classified text. + * Sets the OnClickListener for the primary action that may be triggered to act on + * the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { - ensureDefaultActionAvailable(); - mOnClickListeners.set(0, onClickListener); + mPrimaryOnClickListener = onClickListener; return this; } /** - * Sets information about the classifier model used to generate this TextClassification. - * @hide + * Sets a signature for the TextClassification object. + * The TextClassifier that generates the TextClassification object may use it as a way to + * internally identify the TextClassification object. */ - Builder setVersionInfo(@NonNull String versionInfo) { - mVersionInfo = Preconditions.checkNotNull(versionInfo); + public Builder setSignature(@NonNull String signature) { + mSignature = Preconditions.checkNotNull(signature); return this; } - /** - * Ensures that we have storage for the default action. - */ - private void ensureDefaultActionAvailable() { - if (mIntents.isEmpty()) mIntents.add(null); - if (mLabels.isEmpty()) mLabels.add(null); - if (mIcons.isEmpty()) mIcons.add(null); - if (mOnClickListeners.isEmpty()) mOnClickListeners.add(null); - } - /** * Builds and returns a {@link TextClassification} object. */ public TextClassification build() { return new TextClassification( - mText, mIcons, mLabels, mIntents, mOnClickListeners, mEntityConfidence, - mLogType, mVersionInfo); + mText, + mPrimaryIcon, mPrimaryLabel, + mPrimaryIntent, mPrimaryOnClickListener, + mSecondaryIcons, mSecondaryLabels, + mSecondaryIntents, mSecondaryOnClickListeners, + mEntityConfidence, mSignature); } } diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java index 5aaa5ad1..ed604303 100644 --- a/android/view/textclassifier/TextClassifier.java +++ b/android/view/textclassifier/TextClassifier.java @@ -16,17 +16,23 @@ package android.view.textclassifier; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; import android.annotation.WorkerThread; import android.os.LocaleList; +import android.util.ArraySet; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; /** * Interface for providing text classification related features. @@ -37,7 +43,7 @@ import java.lang.annotation.RetentionPolicy; public interface TextClassifier { /** @hide */ - String DEFAULT_LOG_TAG = "TextClassifierImpl"; + String DEFAULT_LOG_TAG = "androidtc"; String TYPE_UNKNOWN = ""; String TYPE_OTHER = "other"; @@ -48,11 +54,30 @@ public interface TextClassifier { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @StringDef({ - TYPE_UNKNOWN, TYPE_OTHER, TYPE_EMAIL, TYPE_PHONE, TYPE_ADDRESS, TYPE_URL + @StringDef(prefix = { "TYPE_" }, value = { + TYPE_UNKNOWN, + TYPE_OTHER, + TYPE_EMAIL, + TYPE_PHONE, + TYPE_ADDRESS, + TYPE_URL, }) @interface EntityType {} + /** Designates that the TextClassifier should identify all entity types it can. **/ + int ENTITY_PRESET_ALL = 0; + /** Designates that the TextClassifier should identify no entities. **/ + int ENTITY_PRESET_NONE = 1; + /** Designates that the TextClassifier should identify a base set of entities determined by the + * TextClassifier. **/ + int ENTITY_PRESET_BASE = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ENTITY_CONFIG_" }, + value = {ENTITY_PRESET_ALL, ENTITY_PRESET_NONE, ENTITY_PRESET_BASE}) + @interface EntityPreset {} + /** * No-op TextClassifier. * This may be used to turn off TextClassifier features. @@ -212,6 +237,8 @@ public interface TextClassifier { * Returns a {@link TextLinks} that may be applied to the text to annotate it with links * information. * + * If no options are supplied, default values will be used, determined by the TextClassifier. + * * @param text the text to generate annotations for * @param options configuration for link generation * @@ -245,6 +272,16 @@ public interface TextClassifier { return generateLinks(text, null); } + /** + * Returns a {@link Collection} of the entity types in the specified preset. + * + * @see #ENTITIES_ALL + * @see #ENTITIES_NONE + */ + default Collection getEntitiesForPreset(@EntityPreset int entityPreset) { + return Collections.EMPTY_LIST; + } + /** * Logs a TextClassifier event. * @@ -263,6 +300,62 @@ public interface TextClassifier { return TextClassifierConstants.DEFAULT; } + /** + * Configuration object for specifying what entities to identify. + * + * Configs are initially based on a predefined preset, and can be modified from there. + */ + final class EntityConfig { + private final @TextClassifier.EntityPreset int mEntityPreset; + private final Collection mExcludedEntityTypes; + private final Collection mIncludedEntityTypes; + + public EntityConfig(@TextClassifier.EntityPreset int mEntityPreset) { + this.mEntityPreset = mEntityPreset; + mExcludedEntityTypes = new ArraySet<>(); + mIncludedEntityTypes = new ArraySet<>(); + } + + /** + * Specifies an entity to include in addition to any specified by the enity preset. + * + * Note that if an entity has been excluded, the exclusion will take precedence. + */ + public EntityConfig includeEntities(String... entities) { + for (String entity : entities) { + mIncludedEntityTypes.add(entity); + } + return this; + } + + /** + * Specifies an entity to be excluded. + */ + public EntityConfig excludeEntities(String... entities) { + for (String entity : entities) { + mExcludedEntityTypes.add(entity); + } + return this; + } + + /** + * Returns an unmodifiable list of the final set of entities to find. + */ + public List getEntities(TextClassifier textClassifier) { + ArrayList entities = new ArrayList<>(); + for (String entity : textClassifier.getEntitiesForPreset(mEntityPreset)) { + if (!mExcludedEntityTypes.contains(entity)) { + entities.add(entity); + } + } + for (String entity : mIncludedEntityTypes) { + if (!mExcludedEntityTypes.contains(entity) && !entities.contains(entity)) { + entities.add(entity); + } + } + return Collections.unmodifiableList(entities); + } + } /** * Utility functions for TextClassifier methods. diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index df5e35f0..aea3cb06 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -32,7 +32,7 @@ import android.provider.ContactsContract; import android.provider.Settings; import android.text.util.Linkify; import android.util.Patterns; -import android.widget.TextViewMetrics; +import android.view.View.OnClickListener; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; @@ -42,6 +42,9 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -66,6 +69,18 @@ final class TextClassifierImpl implements TextClassifier { private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model"; private static final String UPDATED_MODEL_FILE_PATH = "/data/misc/textclassifier/textclassifier.smartselection.model"; + private static final List ENTITY_TYPES_ALL = + Collections.unmodifiableList(Arrays.asList( + TextClassifier.TYPE_ADDRESS, + TextClassifier.TYPE_EMAIL, + TextClassifier.TYPE_PHONE, + TextClassifier.TYPE_URL)); + private static final List ENTITY_TYPES_BASE = + Collections.unmodifiableList(Arrays.asList( + TextClassifier.TYPE_ADDRESS, + TextClassifier.TYPE_EMAIL, + TextClassifier.TYPE_PHONE, + TextClassifier.TYPE_URL)); private final Context mContext; @@ -121,8 +136,8 @@ final class TextClassifierImpl implements TextClassifier { tsBuilder.setEntityType(results[i].mCollection, results[i].mScore); } return tsBuilder - .setLogSource(LOG_TAG) - .setVersionInfo(getVersionInfo()) + .setSignature( + getSignature(string, selectionStartIndex, selectionEndIndex)) .build(); } else { // We can not trust the result. Log the issue and ignore the result. @@ -154,8 +169,7 @@ final class TextClassifierImpl implements TextClassifier { getHintFlags(string, startIndex, endIndex)); if (results.length > 0) { final TextClassification classificationResult = - createClassificationResult( - results, string.subSequence(startIndex, endIndex)); + createClassificationResult(results, string, startIndex, endIndex); return classificationResult; } } @@ -169,17 +183,23 @@ final class TextClassifierImpl implements TextClassifier { @Override public TextLinks generateLinks( - @NonNull CharSequence text, @NonNull TextLinks.Options options) { + @NonNull CharSequence text, @Nullable TextLinks.Options options) { Utils.validateInput(text); final String textString = text.toString(); final TextLinks.Builder builder = new TextLinks.Builder(textString); try { - LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; + final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; + final Collection entitiesToIdentify = + options != null && options.getEntityConfig() != null + ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL; final SmartSelection smartSelection = getSmartSelection(defaultLocales); final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString); for (SmartSelection.AnnotatedSpan span : annotations) { - final Map entityScores = new HashMap<>(); final SmartSelection.ClassificationResult[] results = span.getClassification(); + if (results.length == 0 || !entitiesToIdentify.contains(results[0].mCollection)) { + continue; + } + final Map entityScores = new HashMap<>(); for (int i = 0; i < results.length; i++) { entityScores.put(results[i].mCollection, results[i].mScore); } @@ -193,6 +213,20 @@ final class TextClassifierImpl implements TextClassifier { return builder.build(); } + @Override + public Collection getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) { + switch (entityPreset) { + case TextClassifier.ENTITY_PRESET_NONE: + return Collections.emptyList(); + case TextClassifier.ENTITY_PRESET_BASE: + return ENTITY_TYPES_BASE; + case TextClassifier.ENTITY_PRESET_ALL: + // fall through + default: + return ENTITY_TYPES_ALL; + } + } + @Override public void logEvent(String source, String event) { if (LOG_TAG.equals(source)) { @@ -229,13 +263,13 @@ final class TextClassifierImpl implements TextClassifier { } } - @NonNull - private String getVersionInfo() { + private String getSignature(String text, int start, int end) { synchronized (mSmartSelectionLock) { - if (mLocale != null) { - return String.format("%s_v%d", mLocale.toLanguageTag(), mVersion); - } - return ""; + final String versionInfo = (mLocale != null) + ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion) + : ""; + final int hash = Objects.hash(text, start, end, mContext.getPackageName()); + return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash); } } @@ -371,9 +405,11 @@ final class TextClassifierImpl implements TextClassifier { } private TextClassification createClassificationResult( - SmartSelection.ClassificationResult[] classifications, CharSequence text) { + SmartSelection.ClassificationResult[] classifications, + String text, int start, int end) { + final String classifiedText = text.substring(start, end); final TextClassification.Builder builder = new TextClassification.Builder() - .setText(text.toString()); + .setText(classifiedText); final int size = classifications.length; for (int i = 0; i < size; i++) { @@ -381,50 +417,54 @@ final class TextClassifierImpl implements TextClassifier { } final String type = getHighestScoringType(classifications); - builder.setLogType(IntentFactory.getLogType(type)); - - final List intents = IntentFactory.create(mContext, type, text.toString()); - for (Intent intent : intents) { - extendClassificationWithIntent(intent, builder); - } + addActions(builder, IntentFactory.create(mContext, type, classifiedText)); - return builder.setVersionInfo(getVersionInfo()).build(); + return builder.setSignature(getSignature(text, start, end)).build(); } - /** Extends the classification with the intent if it can be resolved. */ - private void extendClassificationWithIntent(Intent intent, TextClassification.Builder builder) { - final PackageManager pm; - final ResolveInfo resolveInfo; - if (intent != null) { - pm = mContext.getPackageManager(); - resolveInfo = pm.resolveActivity(intent, 0); - } else { - pm = null; - resolveInfo = null; - } - if (resolveInfo != null && resolveInfo.activityInfo != null) { - final String packageName = resolveInfo.activityInfo.packageName; - CharSequence label; - Drawable icon; - if ("android".equals(packageName)) { - // Requires the chooser to find an activity to handle the intent. - label = IntentFactory.getLabel(mContext, intent); - icon = null; + /** Extends the classification with the intents that can be resolved. */ + private void addActions( + TextClassification.Builder builder, List intents) { + final PackageManager pm = mContext.getPackageManager(); + final int size = intents.size(); + for (int i = 0; i < size; i++) { + final Intent intent = intents.get(i); + final ResolveInfo resolveInfo; + if (intent != null) { + resolveInfo = pm.resolveActivity(intent, 0); } else { - // A default activity will handle the intent. - intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name)); - icon = resolveInfo.activityInfo.loadIcon(pm); - if (icon == null) { - icon = resolveInfo.loadIcon(pm); + resolveInfo = null; + } + if (resolveInfo != null && resolveInfo.activityInfo != null) { + final String packageName = resolveInfo.activityInfo.packageName; + CharSequence label; + Drawable icon; + if ("android".equals(packageName)) { + // Requires the chooser to find an activity to handle the intent. + label = IntentFactory.getLabel(mContext, intent); + icon = null; + } else { + // A default activity will handle the intent. + intent.setComponent( + new ComponentName(packageName, resolveInfo.activityInfo.name)); + icon = resolveInfo.activityInfo.loadIcon(pm); + if (icon == null) { + icon = resolveInfo.loadIcon(pm); + } + label = resolveInfo.activityInfo.loadLabel(pm); + if (label == null) { + label = resolveInfo.loadLabel(pm); + } } - label = resolveInfo.activityInfo.loadLabel(pm); - if (label == null) { - label = resolveInfo.loadLabel(pm); + final String labelString = (label != null) ? label.toString() : null; + final OnClickListener onClickListener = + TextClassification.createStartActivityOnClickListener(mContext, intent); + if (i == 0) { + builder.setPrimaryAction(intent, labelString, icon, onClickListener); + } else { + builder.addSecondaryAction(intent, labelString, icon, onClickListener); } } - builder.addAction( - intent, label != null ? label.toString() : null, icon, - TextClassification.createStartActivityOnClickListener(mContext, intent)); } } @@ -557,22 +597,5 @@ final class TextClassifierImpl implements TextClassifier { return null; } } - - @Nullable - public static int getLogType(String type) { - type = type.trim().toLowerCase(Locale.ENGLISH); - switch (type) { - case TextClassifier.TYPE_EMAIL: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_EMAIL; - case TextClassifier.TYPE_PHONE: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_PHONE; - case TextClassifier.TYPE_ADDRESS: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_ADDRESS; - case TextClassifier.TYPE_URL: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_URL; - default: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_OTHER; - } - } } } diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java index 76748d2b..6c587cf9 100644 --- a/android/view/textclassifier/TextLinks.java +++ b/android/view/textclassifier/TextLinks.java @@ -22,6 +22,8 @@ import android.annotation.Nullable; import android.os.LocaleList; import android.text.SpannableString; import android.text.style.ClickableSpan; +import android.view.View; +import android.widget.TextView; import com.android.internal.util.Preconditions; @@ -103,11 +105,7 @@ public final class TextLinks { mOriginalText = originalText; mStart = start; mEnd = end; - mEntityScores = new EntityConfidence<>(); - - for (Map.Entry entry : entityScores.entrySet()) { - mEntityScores.setEntityType(entry.getKey(), entry.getValue()); - } + mEntityScores = new EntityConfidence<>(entityScores); } /** @@ -163,17 +161,29 @@ public final class TextLinks { public static final class Options { private LocaleList mDefaultLocales; + private TextClassifier.EntityConfig mEntityConfig; /** - * @param defaultLocales ordered list of locale preferences that may be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty - * locale list. + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, + * set this to null or an empty locale list. */ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { mDefaultLocales = defaultLocales; 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; + } + /** * @return ordered list of locale preferences that can be used to disambiguate * the provided text. @@ -182,6 +192,15 @@ public final class TextLinks { public LocaleList getDefaultLocales() { return mDefaultLocales; } + + /** + * @return The config representing the set of entities to look for. + * @see #setEntityConfig(TextClassifier.EntityConfig) + */ + @Nullable + public TextClassifier.EntityConfig getEntityConfig() { + return mEntityConfig; + } } /** @@ -193,9 +212,14 @@ public final class TextLinks { * @hide */ public static final Function DEFAULT_SPAN_FACTORY = - textLink -> { - // TODO: Implement. - throw new UnsupportedOperationException("Not yet implemented"); + textLink -> new ClickableSpan() { + @Override + public void onClick(View widget) { + if (widget instanceof TextView) { + final TextView textView = (TextView) widget; + textView.requestActionMode(textLink); + } + } }; /** diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java index 480b27a7..25e9e7ec 100644 --- a/android/view/textclassifier/TextSelection.java +++ b/android/view/textclassifier/TextSelection.java @@ -21,12 +21,13 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.LocaleList; +import android.util.ArrayMap; import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; -import java.util.List; import java.util.Locale; +import java.util.Map; /** * Information about where text selection should be. @@ -36,19 +37,15 @@ public final class TextSelection { private final int mStartIndex; private final int mEndIndex; @NonNull private final EntityConfidence mEntityConfidence; - @NonNull private final List mEntities; - @NonNull private final String mLogSource; - @NonNull private final String mVersionInfo; + @NonNull private final String mSignature; private TextSelection( - int startIndex, int endIndex, @NonNull EntityConfidence entityConfidence, - @NonNull String logSource, @NonNull String versionInfo) { + int startIndex, int endIndex, @NonNull Map entityConfidence, + @NonNull String signature) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence<>(entityConfidence); - mEntities = mEntityConfidence.getEntities(); - mLogSource = logSource; - mVersionInfo = versionInfo; + mSignature = signature; } /** @@ -70,7 +67,7 @@ public final class TextSelection { */ @IntRange(from = 0) public int getEntityCount() { - return mEntities.size(); + return mEntityConfidence.getEntities().size(); } /** @@ -82,7 +79,7 @@ public final class TextSelection { */ @NonNull public @EntityType String getEntity(int index) { - return mEntities.get(index); + return mEntityConfidence.getEntities().get(index); } /** @@ -96,27 +93,21 @@ public final class TextSelection { } /** - * Returns a tag for the source classifier used to generate this result. - * @hide + * Returns the signature for this object. + * The TextClassifier that generates this object may use it as a way to internally identify + * this object. */ @NonNull - public String getSourceClassifier() { - return mLogSource; - } - - /** - * Returns information about the classifier model used to generate this TextSelection. - * @hide - */ - @NonNull - public String getVersionInfo() { - return mVersionInfo; + public String getSignature() { + return mSignature; } @Override public String toString() { - return String.format(Locale.US, - "TextSelection {%d, %d, %s}", mStartIndex, mEndIndex, mEntityConfidence); + return String.format( + Locale.US, + "TextSelection {startIndex=%d, endIndex=%d, entities=%s, signature=%s}", + mStartIndex, mEndIndex, mEntityConfidence, mSignature); } /** @@ -126,10 +117,8 @@ public final class TextSelection { private final int mStartIndex; private final int mEndIndex; - @NonNull private final EntityConfidence mEntityConfidence = - new EntityConfidence<>(); - @NonNull private String mLogSource = ""; - @NonNull private String mVersionInfo = ""; + @NonNull private final Map mEntityConfidence = new ArrayMap<>(); + @NonNull private String mSignature = ""; /** * Creates a builder used to build {@link TextSelection} objects. @@ -154,25 +143,18 @@ public final class TextSelection { public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - mEntityConfidence.setEntityType(type, confidenceScore); + mEntityConfidence.put(type, confidenceScore); return this; } /** - * Sets a tag for the source classifier used to generate this result. - * @hide - */ - Builder setLogSource(@NonNull String logSource) { - mLogSource = Preconditions.checkNotNull(logSource); - return this; - } - - /** - * Sets information about the classifier model used to generate this TextSelection. - * @hide + * Sets a signature for the TextSelection object. + * + * The TextClassifier that generates the TextSelection object may use it as a way to + * internally identify the TextSelection object. */ - Builder setVersionInfo(@NonNull String versionInfo) { - mVersionInfo = Preconditions.checkNotNull(versionInfo); + public Builder setSignature(@NonNull String signature) { + mSignature = Preconditions.checkNotNull(signature); return this; } @@ -181,7 +163,7 @@ public final class TextSelection { */ public TextSelection build() { return new TextSelection( - mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo); + mStartIndex, mEndIndex, mEntityConfidence, mSignature); } } diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 2833564f..157b3d82 100644 --- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -473,7 +473,7 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = classification.getVersionInfo(); + final String versionTag = getVersionInfo(classification.getSignature()); return new SelectionEvent( start, end, EventType.SELECTION_MODIFIED, entityType, versionTag); } @@ -489,7 +489,7 @@ public final class SmartSelectionEventTracker { */ public static SelectionEvent selectionModified( int start, int end, @NonNull TextSelection selection) { - final boolean smartSelection = selection.getSourceClassifier() + final boolean smartSelection = getSourceClassifier(selection.getSignature()) .equals(TextClassifier.DEFAULT_LOG_TAG); final int eventType; if (smartSelection) { @@ -503,7 +503,7 @@ public final class SmartSelectionEventTracker { final String entityType = selection.getEntityCount() > 0 ? selection.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = selection.getVersionInfo(); + final String versionTag = getVersionInfo(selection.getSignature()); return new SelectionEvent(start, end, eventType, entityType, versionTag); } @@ -538,26 +538,25 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = classification.getVersionInfo(); + final String versionTag = getVersionInfo(classification.getSignature()); return new SelectionEvent(start, end, actionType, entityType, versionTag); } - private boolean isActionType() { - switch (mEventType) { - case ActionType.OVERTYPE: // fall through - case ActionType.COPY: // fall through - case ActionType.PASTE: // fall through - case ActionType.CUT: // fall through - case ActionType.SHARE: // fall through - case ActionType.SMART_SHARE: // fall through - case ActionType.DRAG: // fall through - case ActionType.ABANDON: // fall through - case ActionType.SELECT_ALL: // fall through - case ActionType.RESET: // fall through - return true; - default: - return false; + private static String getVersionInfo(String signature) { + final int start = signature.indexOf("|"); + final int end = signature.indexOf("|", start); + if (start >= 0 && end >= start) { + return signature.substring(start, end); + } + return ""; + } + + private static String getSourceClassifier(String signature) { + final int end = signature.indexOf("|"); + if (end >= 0) { + return signature.substring(0, end); } + return ""; } private boolean isTerminal() { diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index f368c74a..8e1f2183 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,213 +1,58 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2016 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package android.view.textservice; -import android.annotation.SystemService; -import android.content.Context; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.ServiceManager.ServiceNotFoundException; -import android.util.Log; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; -import com.android.internal.textservice.ITextServicesManager; - import java.util.Locale; /** - * System API to the overall text services, which arbitrates interaction between applications - * and text services. - * - * The user can change the current text services in Settings. And also applications can specify - * the target text services. - * - *

    Architecture Overview

    - * - *

    There are three primary parties involved in the text services - * framework (TSF) architecture:

    - * - *
      - *
    • The text services manager as expressed by this class - * is the central point of the system that manages interaction between all - * other parts. It is expressed as the client-side API here which exists - * in each application context and communicates with a global system service - * that manages the interaction across all processes. - *
    • A text service implements a particular - * interaction model allowing the client application to retrieve information of text. - * The system binds to the current text service that is in use, causing it to be created and run. - *
    • Multiple client applications arbitrate with the text service - * manager for connections to text services. - *
    - * - *

    Text services sessions

    - *
      - *
    • The spell checker session is one of the text services. - * {@link android.view.textservice.SpellCheckerSession}
    • - *
    - * + * A stub class of TextServicesManager for Layout-Lib. */ -@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final String TAG = TextServicesManager.class.getSimpleName(); - private static final boolean DBG = false; - - private static TextServicesManager sInstance; - - private final ITextServicesManager mService; - - private TextServicesManager() throws ServiceNotFoundException { - mService = ITextServicesManager.Stub.asInterface( - ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); - } + private static final TextServicesManager sInstance = new TextServicesManager(); + private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - synchronized (TextServicesManager.class) { - if (sInstance == null) { - try { - sInstance = new TextServicesManager(); - } catch (ServiceNotFoundException e) { - throw new IllegalStateException(e); - } - } - return sInstance; - } - } - - /** - * Returns the language component of a given locale string. - */ - private static String parseLanguageFromLocaleString(String locale) { - final int idx = locale.indexOf('_'); - if (idx < 0) { - return locale; - } else { - return locale.substring(0, idx); - } + return sInstance; } - /** - * Get a spell checker session for the specified spell checker - * @param locale the locale for the spell checker. If {@code locale} is null and - * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be - * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, - * the locale specified in Settings will be returned only when it is same as {@code locale}. - * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is - * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be - * selected. - * @param listener a spell checker session lister for getting results from a spell checker. - * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled - * languages in settings will be returned. - * @return the spell checker session of the spell checker - */ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale, SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - if (listener == null) { - throw new NullPointerException(); - } - if (!referToSpellCheckerLanguageSettings && locale == null) { - throw new IllegalArgumentException("Locale should not be null if you don't refer" - + " settings."); - } - - if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) { - return null; - } - - final SpellCheckerInfo sci; - try { - sci = mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - return null; - } - if (sci == null) { - return null; - } - SpellCheckerSubtype subtypeInUse = null; - if (referToSpellCheckerLanguageSettings) { - subtypeInUse = getCurrentSpellCheckerSubtype(true); - if (subtypeInUse == null) { - return null; - } - if (locale != null) { - final String subtypeLocale = subtypeInUse.getLocale(); - final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); - if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { - return null; - } - } - } else { - final String localeStr = locale.toString(); - for (int i = 0; i < sci.getSubtypeCount(); ++i) { - final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); - final String tempSubtypeLocale = subtype.getLocale(); - final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); - if (tempSubtypeLocale.equals(localeStr)) { - subtypeInUse = subtype; - break; - } else if (tempSubtypeLanguage.length() >= 2 && - locale.getLanguage().equals(tempSubtypeLanguage)) { - subtypeInUse = subtype; - } - } - } - if (subtypeInUse == null) { - return null; - } - final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener); - try { - mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), - session.getTextServicesSessionListener(), - session.getSpellCheckerSessionListener(), bundle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - return session; + return null; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - try { - final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(); - if (DBG) { - Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); - } - return retval; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return EMPTY_SPELL_CHECKER_INFO; } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - try { - // Passing null as a locale for ICS - return mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** @@ -215,22 +60,13 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - try { - // Passing null as a locale until we support multiple enabled spell checker subtypes. - return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** * @hide */ public boolean isSpellCheckerEnabled() { - try { - return mService.isSpellCheckerEnabled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return false; } } diff --git a/android/webkit/FilterMethods.java b/android/webkit/FilterMethods.java new file mode 100644 index 00000000..76552b7f --- /dev/null +++ b/android/webkit/FilterMethods.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +public class FilterMethods { + + public void method1(boolean param) { + } + + public void method2(int x) { + } + + public void method3(WebViewClient client) { + } +} diff --git a/android/webkit/SafeBrowsingResponse.java b/android/webkit/SafeBrowsingResponse.java index 1d3a617a..960b56bd 100644 --- a/android/webkit/SafeBrowsingResponse.java +++ b/android/webkit/SafeBrowsingResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,36 +16,5 @@ package android.webkit; -/** - * Used to indicate an action to take when hitting a malicious URL. Instances of this class are - * created by the WebView and passed to {@link android.webkit.WebViewClient#onSafeBrowsingHit}. The - * host application must call {@link #showInterstitial(boolean)}, {@link #proceed(boolean)}, or - * {@link #backToSafety(boolean)} to set the WebView's response to the Safe Browsing hit. - * - *

    - * If reporting is enabled, all reports will be sent according to the privacy policy referenced by - * {@link android.webkit.WebView#getSafeBrowsingPrivacyPolicyUrl()}. - */ -public abstract class SafeBrowsingResponse { - - /** - * Display the default interstitial. - * - * @param allowReporting {@code true} if the interstitial should show a reporting checkbox. - */ - public abstract void showInterstitial(boolean allowReporting); - - /** - * Act as if the user clicked "visit this unsafe site." - * - * @param report {@code true} to enable Safe Browsing reporting. - */ - public abstract void proceed(boolean report); - - /** - * Act as if the user clicked "back to safety." - * - * @param report {@code true} to enable Safe Browsing reporting. - */ - public abstract void backToSafety(boolean report); +public class SafeBrowsingResponse { } diff --git a/android/webkit/SingleClassAndMethod.java b/android/webkit/SingleClassAndMethod.java new file mode 100644 index 00000000..3100ca80 --- /dev/null +++ b/android/webkit/SingleClassAndMethod.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +public class SingleClassAndMethod { + + public void method(boolean param) { + File x = new File(""); + } +} diff --git a/android/webkit/TracingConfig.java b/android/webkit/TracingConfig.java new file mode 100644 index 00000000..75e2bf70 --- /dev/null +++ b/android/webkit/TracingConfig.java @@ -0,0 +1,203 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Holds tracing configuration information and predefined settings. + */ +public class TracingConfig { + + private final String mCustomCategoryPattern; + private final @PresetCategories int mPresetCategories; + private @TracingMode int mTracingMode; + + /** @hide */ + @IntDef({CATEGORIES_NONE, CATEGORIES_WEB_DEVELOPER, CATEGORIES_INPUT_LATENCY, + CATEGORIES_RENDERING, CATEGORIES_JAVASCRIPT_AND_RENDERING, CATEGORIES_FRAME_VIEWER}) + @Retention(RetentionPolicy.SOURCE) + public @interface PresetCategories {} + + /** + * Indicates that there are no preset categories. + */ + public static final int CATEGORIES_NONE = -1; + + /** + * Predefined categories typically useful for web developers. + * Typically includes blink, compositor, renderer.scheduler and v8 categories. + */ + public static final int CATEGORIES_WEB_DEVELOPER = 0; + + /** + * Predefined categories for analyzing input latency issues. + * Typically includes input, renderer.scheduler categories. + */ + public static final int CATEGORIES_INPUT_LATENCY = 1; + + /** + * Predefined categories for analyzing rendering issues. + * Typically includes blink, compositor and gpu categories. + */ + public static final int CATEGORIES_RENDERING = 2; + + /** + * Predefined categories for analyzing javascript and rendering issues. + * Typically includes blink, compositor, gpu, renderer.schduler and v8 categories. + */ + public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 3; + + /** + * Predefined categories for studying difficult rendering performance problems. + * Typically includes blink, compositor, gpu, renderer.scheduler, v8 and + * some other compositor categories which are disabled by default. + */ + public static final int CATEGORIES_FRAME_VIEWER = 4; + + /** @hide */ + @IntDef({RECORD_UNTIL_FULL, RECORD_CONTINUOUSLY, RECORD_UNTIL_FULL_LARGE_BUFFER, + RECORD_TO_CONSOLE}) + @Retention(RetentionPolicy.SOURCE) + public @interface TracingMode {} + + /** + * Record trace events until the internal tracing buffer is full. Default tracing mode. + * Typically the buffer memory usage is between {@link #RECORD_CONTINUOUSLY} and the + * {@link #RECORD_UNTIL_FULL_LARGE_BUFFER}. Depending on the implementation typically allows + * up to 256k events to be stored. + */ + public static final int RECORD_UNTIL_FULL = 0; + + /** + * Record trace events continuously using an internal ring buffer. Overwrites + * old events if they exceed buffer capacity. Uses less memory than both + * {@link #RECORD_UNTIL_FULL} and {@link #RECORD_UNTIL_FULL_LARGE_BUFFER} modes. + * Depending on the implementation typically allows up to 64k events to be stored. + */ + public static final int RECORD_CONTINUOUSLY = 1; + + /** + * Record trace events using a larger internal tracing buffer until it is full. + * Uses more memory than the other modes and may not be suitable on devices + * with smaller RAM. Depending on the implementation typically allows up to + * 512 million events to be stored. + */ + public static final int RECORD_UNTIL_FULL_LARGE_BUFFER = 2; + + /** + * Record trace events to console (logcat). The events are discarded and nothing + * is sent back to the caller. Uses the least memory as compared to the other modes. + */ + public static final int RECORD_TO_CONSOLE = 3; + + /** + * Create config with the preset categories. + *

    + * Example: + * TracingConfig(CATEGORIES_WEB_DEVELOPER) -- records trace events from the "web developer" + * preset categories. + * + * @param presetCategories preset categories to use, one of {@link #CATEGORIES_WEB_DEVELOPER}, + * {@link #CATEGORIES_INPUT_LATENCY}, {@link #CATEGORIES_RENDERING}, + * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or + * {@link #CATEGORIES_FRAME_VIEWER}. + * + * Note: for specifying custom categories without presets use + * {@link #TracingConfig(int, String, int)}. + * + */ + public TracingConfig(@PresetCategories int presetCategories) { + this(presetCategories, "", RECORD_UNTIL_FULL); + } + + /** + * Create a configuration with both preset categories and custom categories. + * Also allows to specify the tracing mode. + * + * Note that the categories are defined by the currently-in-use version of WebView. They live + * in chromium code and are not part of the Android API. See + * See + * chromium documentation on tracing for more details. + * + *

    + * Examples: + * + * Preset category with a specified trace mode: + * TracingConfig(CATEGORIES_WEB_DEVELOPER, "", RECORD_UNTIL_FULL_LARGE_BUFFER); + * Custom categories: + * TracingConfig(CATEGORIES_NONE, "browser", RECORD_UNTIL_FULL) + * -- records only the trace events from the "browser" category. + * TraceConfig(CATEGORIES_NONE, "-input,-gpu", RECORD_UNTIL_FULL) + * -- records all trace events excluding the events from the "input" and 'gpu' categories. + * TracingConfig(CATEGORIES_NONE, "blink*,devtools*", RECORD_UNTIL_FULL) + * -- records only the trace events matching the "blink*" and "devtools*" patterns + * (e.g. "blink_gc" and "devtools.timeline" categories). + * + * Combination of preset and additional custom categories: + * TracingConfig(CATEGORIES_WEB_DEVELOPER, "memory-infra", RECORD_CONTINUOUSLY) + * -- records events from the "web developer" categories and events from the "memory-infra" + * category to understand where memory is being used. + * + * @param presetCategories preset categories to use, one of {@link #CATEGORIES_WEB_DEVELOPER}, + * {@link #CATEGORIES_INPUT_LATENCY}, {@link #CATEGORIES_RENDERING}, + * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or + * {@link #CATEGORIES_FRAME_VIEWER}. + * @param customCategories a comma-delimited list of category wildcards. A category can + * have an optional '-' prefix to make it an excluded category. + * @param tracingMode tracing mode to use, one of {@link #RECORD_UNTIL_FULL}, + * {@link #RECORD_CONTINUOUSLY}, {@link #RECORD_UNTIL_FULL_LARGE_BUFFER} + * or {@link #RECORD_TO_CONSOLE}. + */ + public TracingConfig(@PresetCategories int presetCategories, + @NonNull String customCategories, @TracingMode int tracingMode) { + mPresetCategories = presetCategories; + mCustomCategoryPattern = customCategories; + mTracingMode = RECORD_UNTIL_FULL; + } + + /** + * Returns the custom category pattern for this configuration. + * + * @return empty string if no custom category pattern is specified. + */ + @NonNull + public String getCustomCategoryPattern() { + return mCustomCategoryPattern; + } + + /** + * Returns the preset categories value of this configuration. + */ + @PresetCategories + public int getPresetCategories() { + return mPresetCategories; + } + + /** + * Returns the tracing mode of this configuration. + */ + @TracingMode + public int getTracingMode() { + return mTracingMode; + } + +} diff --git a/android/webkit/TracingController.java b/android/webkit/TracingController.java new file mode 100644 index 00000000..cadb8a18 --- /dev/null +++ b/android/webkit/TracingController.java @@ -0,0 +1,126 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; + +/** + * Manages tracing of WebViews. In particular provides functionality for the app + * to enable/disable tracing of parts of code and to collect tracing data. + * This is useful for profiling performance issues, debugging and memory usage + * analysis in production and real life scenarios. + *

    + * The resulting trace data is sent back as a byte sequence in json format. This + * file can be loaded in "chrome://tracing" for further analysis. + *

    + * Note: All methods in this class must be called on the UI thread. All callbacks + * are also called on the UI thread. + *

    + * Example usage: + *

    + * TracingController tracingController = TracingController.getInstance();
    + * tracingController.start(new TraceConfig(CATEGORIES_WEB_DEVELOPER));
    + * [..]
    + * tracingController.stopAndFlush(new TraceFileOutput("trace.json"), null);
    + * 

    + */ +public abstract class TracingController { + + /** + * Interface for capturing tracing data. + */ + public interface TracingOutputStream { + /** + * Will be called to return tracing data in chunks. + * Tracing data is returned in json format an array of bytes. + */ + void write(byte[] chunk); + + /** + * Called when tracing is finished and the data collection is over. + * There will be no calls to #write after #complete is called. + */ + void complete(); + } + + /** + * Returns the default TracingController instance. At present there is + * only one TracingController instance for all WebView instances, + * however this restriction may be relaxed in the future. + * + * @return the default TracingController instance + */ + @NonNull + public static TracingController getInstance() { + return WebViewFactory.getProvider().getTracingController(); + } + + /** + * Starts tracing all webviews. Depeding on the trace mode in traceConfig + * specifies how the trace events are recorded. + * + * For tracing modes {@link TracingConfig#RECORD_UNTIL_FULL}, + * {@link TracingConfig#RECORD_CONTINUOUSLY} and + * {@link TracingConfig#RECORD_UNTIL_FULL_LARGE_BUFFER} the events are recorded + * using an internal buffer and flushed to the outputStream when + * {@link #stopAndFlush(TracingOutputStream, Handler)} is called. + * + * @param tracingConfig configuration options to use for tracing + * @return false if the system is already tracing, true otherwise. + */ + public abstract boolean start(TracingConfig tracingConfig); + + /** + * Stops tracing and discards all tracing data. + * + * This method is particularly useful in conjunction with the + * {@link TracingConfig#RECORD_TO_CONSOLE} tracing mode because tracing data is logged to + * console and not sent to an outputStream as with + * {@link #stopAndFlush(TracingOutputStream, Handler)}. + * + * @return false if the system was not tracing at the time of the call, true + * otherwise. + */ + public abstract boolean stop(); + + /** + * Stops tracing and flushes tracing data to the specifid outputStream. + * + * Note that if the {@link TracingConfig#RECORD_TO_CONSOLE} tracing mode is used + * nothing will be sent to the outputStream and no TracingOuputStream methods will be + * called. In that case it is more convenient to just use {@link #stop()} instead. + * + * @param outputStream the output steam the tracing data will be sent to. + * @param handler the {@link android.os.Handler} on which the outputStream callbacks + * will be invoked. If the handler is null the current thread's Looper + * will be used. + * @return false if the system was not tracing at the time of the call, true + * otherwise. + */ + public abstract boolean stopAndFlush(TracingOutputStream outputStream, + @Nullable Handler handler); + + /** True if the system is tracing */ + public abstract boolean isTracing(); + + // TODO: consider adding getTraceBufferUsage, percentage and approx event count. + // TODO: consider adding String getCategories(), for obtaining the actual list + // of categories used (given that presets are ints). + +} diff --git a/android/webkit/TracingFileOutputStream.java b/android/webkit/TracingFileOutputStream.java new file mode 100644 index 00000000..8a5fa36c --- /dev/null +++ b/android/webkit/TracingFileOutputStream.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.annotation.NonNull; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Simple TracingOutputStream implementation which writes the trace data from + * {@link TracingController} to a new file. + * + */ +public class TracingFileOutputStream implements TracingController.TracingOutputStream { + + private FileOutputStream mFileOutput; + + public TracingFileOutputStream(@NonNull String filename) throws FileNotFoundException { + mFileOutput = new FileOutputStream(filename); + } + + /** + * Writes bytes chunk to the file. + */ + public void write(byte[] chunk) { + try { + mFileOutput.write(chunk); + } catch (IOException e) { + onIOException(e); + } + } + + /** + * Closes the file. + */ + public void complete() { + try { + mFileOutput.close(); + } catch (IOException e) { + onIOException(e); + } + } + + private void onIOException(IOException e) { + throw new RuntimeException(e); + } +} diff --git a/android/webkit/UserPackage.java b/android/webkit/UserPackage.java index 63519a65..03ff0ca6 100644 --- a/android/webkit/UserPackage.java +++ b/android/webkit/UserPackage.java @@ -34,7 +34,7 @@ public class UserPackage { private final UserInfo mUserInfo; private final PackageInfo mPackageInfo; - public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1; + public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.P; public UserPackage(UserInfo user, PackageInfo packageInfo) { this.mUserInfo = user; diff --git a/android/webkit/WebKitTypeAsMethodParameter.java b/android/webkit/WebKitTypeAsMethodParameter.java new file mode 100644 index 00000000..0b229944 --- /dev/null +++ b/android/webkit/WebKitTypeAsMethodParameter.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +public abstract class WebKitTypeAsMethodParameter { + public void method(WebViewClient webViewClient); + public void methodX(SafeBrowsingResponse response) { + return null; + } + public void urlCall(String url) { + return null; + } +} diff --git a/android/webkit/WebKitTypeAsMethodReturn.java b/android/webkit/WebKitTypeAsMethodReturn.java new file mode 100644 index 00000000..d8d8bd03 --- /dev/null +++ b/android/webkit/WebKitTypeAsMethodReturn.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +public class WebKitTypeAsMethodReturn { + public WebViewClient method() { + return null; + } + public SafeBrowsingResponse method2() { + return null; + } +} diff --git a/android/webkit/WebSettings.java b/android/webkit/WebSettings.java index e4937392..90cc4814 100644 --- a/android/webkit/WebSettings.java +++ b/android/webkit/WebSettings.java @@ -122,7 +122,13 @@ public abstract class WebSettings { } /** @hide */ - @IntDef({LOAD_DEFAULT, LOAD_NORMAL, LOAD_CACHE_ELSE_NETWORK, LOAD_NO_CACHE, LOAD_CACHE_ONLY}) + @IntDef(prefix = { "LOAD_" }, value = { + LOAD_DEFAULT, + LOAD_NORMAL, + LOAD_CACHE_ELSE_NETWORK, + LOAD_NO_CACHE, + LOAD_CACHE_ONLY + }) @Retention(RetentionPolicy.SOURCE) public @interface CacheMode {} @@ -1415,13 +1421,12 @@ public abstract class WebSettings { /** * @hide */ - @IntDef(flag = true, - value = { - MENU_ITEM_NONE, - MENU_ITEM_SHARE, - MENU_ITEM_WEB_SEARCH, - MENU_ITEM_PROCESS_TEXT - }) + @IntDef(flag = true, prefix = { "MENU_ITEM_" }, value = { + MENU_ITEM_NONE, + MENU_ITEM_SHARE, + MENU_ITEM_WEB_SEARCH, + MENU_ITEM_PROCESS_TEXT + }) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.PARAMETER, ElementType.METHOD}) private @interface MenuItemFlags {} diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index 6f992548..202f2046 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,3020 +16,223 @@ package android.webkit; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SystemApi; -import android.annotation.Widget; +import com.android.layoutlib.bridge.MockView; + import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.res.Configuration; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; import android.graphics.Picture; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.net.http.SslCertificate; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.Message; -import android.os.RemoteException; -import android.os.StrictMode; -import android.print.PrintDocumentAdapter; -import android.security.KeyChain; import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.DragEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; -import android.view.ViewHierarchyEncoder; -import android.view.ViewStructure; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeProvider; -import android.view.autofill.AutofillValue; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.textclassifier.TextClassifier; -import android.widget.AbsoluteLayout; - -import java.io.BufferedWriter; -import java.io.File; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.Map; /** - *

    A View that displays web pages. This class is the basis upon which you - * can roll your own web browser or simply display some online content within your Activity. - * It uses the WebKit rendering engine to display - * web pages and includes methods to navigate forward and backward - * through a history, zoom in and out, perform text searches and more. - * - *

    Note that, in order for your Activity to access the Internet and load web pages - * in a WebView, you must add the {@code INTERNET} permissions to your - * Android Manifest file: - * - *

    - * {@code }
    - * 
    - * - *

    This must be a child of the {@code } - * element. - * - *

    For more information, read - * Building Web Apps in WebView. - * - *

    Basic usage

    - * - *

    By default, a WebView provides no browser-like widgets, does not - * enable JavaScript and web page errors are ignored. If your goal is only - * to display some HTML as a part of your UI, this is probably fine; - * the user won't need to interact with the web page beyond reading - * it, and the web page won't need to interact with the user. If you - * actually want a full-blown web browser, then you probably want to - * invoke the Browser application with a URL Intent rather than show it - * with a WebView. For example: - *

    - * Uri uri = Uri.parse("https://www.example.com");
    - * Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    - * startActivity(intent);
    - * 
    - *

    See {@link android.content.Intent} for more information. - * - *

    To provide a WebView in your own Activity, include a {@code } in your layout, - * or set the entire Activity window as a WebView during {@link - * android.app.Activity#onCreate(Bundle) onCreate()}: - * - *

    - * WebView webview = new WebView(this);
    - * setContentView(webview);
    - * 
    - * - *

    Then load the desired web page: - * - *

    - * // Simplest usage: note that an exception will NOT be thrown
    - * // if there is an error loading this page (see below).
    - * webview.loadUrl("https://example.com/");
    - *
    - * // OR, you can also load from an HTML string:
    - * String summary = "<html><body>You scored <b>192</b> points.</body></html>";
    - * webview.loadData(summary, "text/html", null);
    - * // ... although note that there are restrictions on what this HTML can do.
    - * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link
    - * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info.
    - * 
    - * - *

    A WebView has several customization points where you can add your - * own behavior. These are: - * - *

      - *
    • Creating and setting a {@link android.webkit.WebChromeClient} subclass. - * This class is called when something that might impact a - * browser UI happens, for instance, progress updates and - * JavaScript alerts are sent here (see Debugging Tasks). - *
    • - *
    • Creating and setting a {@link android.webkit.WebViewClient} subclass. - * It will be called when things happen that impact the - * rendering of the content, eg, errors or form submissions. You - * can also intercept URL loading here (via {@link - * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String) - * shouldOverrideUrlLoading()}).
    • - *
    • Modifying the {@link android.webkit.WebSettings}, such as - * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) - * setJavaScriptEnabled()}.
    • - *
    • Injecting Java objects into the WebView using the - * {@link android.webkit.WebView#addJavascriptInterface} method. This - * method allows you to inject Java objects into a page's JavaScript - * context, so that they can be accessed by JavaScript in the page.
    • - *
    - * - *

    Here's a more complicated example, showing error handling, - * settings, and progress notification: - * - *

    - * // Let's display the progress in the activity title bar, like the
    - * // browser app does.
    - * getWindow().requestFeature(Window.FEATURE_PROGRESS);
    - *
    - * webview.getSettings().setJavaScriptEnabled(true);
    - *
    - * final Activity activity = this;
    - * webview.setWebChromeClient(new WebChromeClient() {
    - *   public void onProgressChanged(WebView view, int progress) {
    - *     // Activities and WebViews measure progress with different scales.
    - *     // The progress meter will automatically disappear when we reach 100%
    - *     activity.setProgress(progress * 1000);
    - *   }
    - * });
    - * webview.setWebViewClient(new WebViewClient() {
    - *   public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    - *     Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();
    - *   }
    - * });
    - *
    - * webview.loadUrl("https://developer.android.com/");
    - * 
    - * - *

    Zoom

    - * - *

    To enable the built-in zoom, set - * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} - * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}). - * - *

    Note: Using zoom if either the height or width is set to - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior - * and should be avoided. - * - *

    Cookie and window management

    - * - *

    For obvious security reasons, your application has its own - * cache, cookie store etc.—it does not share the Browser - * application's data. - * - *

    By default, requests by the HTML to open new windows are - * ignored. This is {@code true} whether they be opened by JavaScript or by - * the target attribute on a link. You can customize your - * {@link WebChromeClient} to provide your own behavior for opening multiple windows, - * and render them in whatever manner you want. - * - *

    The standard behavior for an Activity is to be destroyed and - * recreated when the device orientation or any other configuration changes. This will cause - * the WebView to reload the current page. If you don't want that, you - * can set your Activity to handle the {@code orientation} and {@code keyboardHidden} - * changes, and then just leave the WebView alone. It'll automatically - * re-orient itself as appropriate. Read Handling Runtime Changes for - * more information about how to handle configuration changes during runtime. - * - * - *

    Building web pages to support different screen densities

    - * - *

    The screen density of a device is based on the screen resolution. A screen with low density - * has fewer available pixels per inch, where a screen with high density - * has more — sometimes significantly more — pixels per inch. The density of a - * screen is important because, other things being equal, a UI element (such as a button) whose - * height and width are defined in terms of screen pixels will appear larger on the lower density - * screen and smaller on the higher density screen. - * For simplicity, Android collapses all actual screen densities into three generalized densities: - * high, medium, and low. - *

    By default, WebView scales a web page so that it is drawn at a size that matches the default - * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen - * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels - * are bigger). - * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS, - * and meta tag features to help you (as a web developer) target screens with different screen - * densities. - *

    Here's a summary of the features you can use to handle different screen densities: - *

      - *
    • The {@code window.devicePixelRatio} DOM property. The value of this property specifies the - * default scaling factor used for the current device. For example, if the value of {@code - * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device - * and default scaling is not applied to the web page; if the value is "1.5", then the device is - * considered a high density device (hdpi) and the page content is scaled 1.5x; if the - * value is "0.75", then the device is considered a low density device (ldpi) and the content is - * scaled 0.75x.
    • - *
    • The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen - * densities for which this style sheet is to be used. The corresponding value should be either - * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium - * density, or high density screens, respectively. For example: - *
      - * <link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" />
      - *

      The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ration of 1.5, - * which is the high density pixel ratio. - *

    • - *
    - * - *

    HTML5 Video support

    - * - *

    In order to support inline HTML5 video in your application you need to have hardware - * acceleration turned on. - * - *

    Full screen support

    - * - *

    In order to support full screen — for video or other HTML content — you need to set a - * {@link android.webkit.WebChromeClient} and implement both - * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} - * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is - * missing then the web contents will not be allowed to enter full screen. Optionally you can implement - * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video - * is loading. - * - *

    HTML5 Geolocation API support

    - * - *

    For applications targeting Android N and later releases - * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on - * secure origins such as https. For such applications requests to geolocation api on non-secure - * origins are automatically denied without invoking the corresponding - * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)} - * method. - * - *

    Layout size

    - *

    - * It is recommended to set the WebView layout height to a fixed value or to - * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. - * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} - * for the height none of the WebView's parents should use a - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in - * incorrect sizing of the views. - * - *

    Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} - * enables the following behaviors: - *

      - *
    • The HTML body layout height is set to a fixed value. This means that elements with a height - * relative to the HTML body may not be sized correctly.
    • - *
    • For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the - * HTML viewport meta tag will be ignored in order to preserve backwards compatibility.
    • - *
    - * - *

    - * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not - * supported. If such a width is used the WebView will attempt to use the width of the parent - * instead. - * - *

    Metrics

    - * - *

    - * WebView may upload anonymous diagnostic data to Google when the user has consented. This data - * helps Google improve WebView. Data is collected on a per-app basis for each app which has - * instantiated a WebView. An individual app can opt out of this feature by putting the following - * tag in its manifest's {@code } element: - *

    - * <manifest>
    - *     <application>
    - *         ...
    - *         <meta-data android:name="android.webkit.WebView.MetricsOptOut"
    - *             android:value="true" />
    - *     </application>
    - * </manifest>
    - * 
    - *

    - * Data will only be uploaded for a given app if the user has consented AND the app has not opted - * out. - * - *

    Safe Browsing

    - * - *

    - * If Safe Browsing is enabled, WebView will block malicious URLs and present a warning UI to the - * user to allow them to navigate back safely or proceed to the malicious page. - *

    - * The recommended way for apps to enable the feature is putting the following tag in the manifest's - * {@code } element: - *

    - *

    - * <manifest>
    - *     <application>
    - *         ...
    - *         <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
    - *             android:value="true" />
    - *     </application>
    - * </manifest>
    - * 
    + * Mock version of the WebView. + * Only non override public methods from the real WebView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. * */ -// Implementation notes. -// The WebView is a thin API class that delegates its public API to a backend WebViewProvider -// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons. -// Methods are delegated to the provider implementation: all public API methods introduced in this -// file are fully delegated, whereas public and protected methods from the View base classes are -// only delegated where a specific need exists for them to do so. -@Widget -public class WebView extends AbsoluteLayout - implements ViewTreeObserver.OnGlobalFocusChangeListener, - ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { - - private static final String LOGTAG = "WebView"; - - // Throwing an exception for incorrect thread usage if the - // build target is JB MR2 or newer. Defaults to false, and is - // set in the WebView constructor. - private static volatile boolean sEnforceThreadChecking = false; - - /** - * Transportation object for returning WebView across thread boundaries. - */ - public class WebViewTransport { - private WebView mWebview; - - /** - * Sets the WebView to the transportation object. - * - * @param webview the WebView to transport - */ - public synchronized void setWebView(WebView webview) { - mWebview = webview; - } - - /** - * Gets the WebView object. - * - * @return the transported WebView object - */ - public synchronized WebView getWebView() { - return mWebview; - } - } - - /** - * URI scheme for telephone number. - */ - public static final String SCHEME_TEL = "tel:"; - /** - * URI scheme for email address. - */ - public static final String SCHEME_MAILTO = "mailto:"; - /** - * URI scheme for map address. - */ - public static final String SCHEME_GEO = "geo:0,0?q="; - - /** - * Interface to listen for find results. - */ - public interface FindListener { - /** - * Notifies the listener about progress made by a find operation. - * - * @param activeMatchOrdinal the zero-based ordinal of the currently selected match - * @param numberOfMatches how many matches have been found - * @param isDoneCounting whether the find operation has actually completed. The listener - * may be notified multiple times while the - * operation is underway, and the numberOfMatches - * value should not be considered final unless - * isDoneCounting is {@code true}. - */ - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting); - } - - /** - * Callback interface supplied to {@link #postVisualStateCallback} for receiving - * notifications about the visual state. - */ - public static abstract class VisualStateCallback { - /** - * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}. - * - * @param requestId The identifier passed to {@link #postVisualStateCallback} when this - * callback was posted. - */ - public abstract void onComplete(long requestId); - } - - /** - * Interface to listen for new pictures as they change. - * - * @deprecated This interface is now obsolete. - */ - @Deprecated - public interface PictureListener { - /** - * Used to provide notification that the WebView's picture has changed. - * See {@link WebView#capturePicture} for details of the picture. - * - * @param view the WebView that owns the picture - * @param picture the new picture. Applications targeting - * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above - * will always receive a {@code null} Picture. - * @deprecated Deprecated due to internal changes. - */ - @Deprecated - void onNewPicture(WebView view, @Nullable Picture picture); - } - - public static class HitTestResult { - /** - * Default HitTestResult, where the target is unknown. - */ - public static final int UNKNOWN_TYPE = 0; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int ANCHOR_TYPE = 1; - /** - * HitTestResult for hitting a phone number. - */ - public static final int PHONE_TYPE = 2; - /** - * HitTestResult for hitting a map address. - */ - public static final int GEO_TYPE = 3; - /** - * HitTestResult for hitting an email address. - */ - public static final int EMAIL_TYPE = 4; - /** - * HitTestResult for hitting an HTML::img tag. - */ - public static final int IMAGE_TYPE = 5; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int IMAGE_ANCHOR_TYPE = 6; - /** - * HitTestResult for hitting a HTML::a tag with src=http. - */ - public static final int SRC_ANCHOR_TYPE = 7; - /** - * HitTestResult for hitting a HTML::a tag with src=http + HTML::img. - */ - public static final int SRC_IMAGE_ANCHOR_TYPE = 8; - /** - * HitTestResult for hitting an edit text area. - */ - public static final int EDIT_TEXT_TYPE = 9; - - private int mType; - private String mExtra; - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public HitTestResult() { - mType = UNKNOWN_TYPE; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setType(int type) { - mType = type; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setExtra(String extra) { - mExtra = extra; - } - - /** - * Gets the type of the hit test result. See the XXX_TYPE constants - * defined in this class. - * - * @return the type of the hit test result - */ - public int getType() { - return mType; - } - - /** - * Gets additional type-dependant information about the result. See - * {@link WebView#getHitTestResult()} for details. May either be {@code null} - * or contain extra information about this result. - * - * @return additional type-dependant information about the result - */ - @Nullable - public String getExtra() { - return mExtra; - } - } +public class WebView extends MockView { /** - * Constructs a new WebView with an Activity Context object. - * - *

    Note: WebView should always be instantiated with an Activity Context. - * If instantiated with an Application Context, WebView will be unable to provide several - * features, such as JavaScript dialogs and autofill. - * - * @param context an Activity Context to access application assets + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. */ public WebView(Context context) { this(context, null); } /** - * Constructs a new WebView with layout parameters. - * - * @param context an Activity Context to access application assets - * @param attrs an AttributeSet passed to our parent + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context an Activity Context to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context an Activity Context to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param defStyleRes a resource identifier of a style resource that - * supplies default values for the view, used only if - * defStyleAttr is 0 or can not be found in the theme. Can be 0 - * to not look for defaults. - */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - this(context, attrs, defStyleAttr, defStyleRes, null, false); - } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context an Activity Context to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * - * @deprecated Private browsing is no longer supported directly via - * WebView and will be removed in a future release. Prefer using - * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager} - * and {@link WebStorage} for fine-grained control of privacy data. - */ - @Deprecated - public WebView(Context context, AttributeSet attrs, int defStyleAttr, - boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, null, privateBrowsing); - } - - /** - * Constructs a new WebView with layout parameters, a default style and a set - * of custom JavaScript interfaces to be added to this WebView at initialization - * time. This guarantees that these interfaces will be available when the JS - * context is initialized. - * - * @param context an Activity Context to access application assets - * @param attrs an AttributeSet passed to our parent - * @param defStyleAttr an attribute in the current theme that contains a - * reference to a style resource that supplies default values for - * the view. Can be 0 to not look for defaults. - * @param javaScriptInterfaces a Map of interface names, as keys, and - * object implementing those interfaces, as - * values - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to - * be added synchronously, before a subsequent loadUrl call takes effect. - */ - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, - Map javaScriptInterfaces, boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); - } - - /** - * @hide + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. */ - @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, - Map javaScriptInterfaces, boolean privateBrowsing) { - super(context, attrs, defStyleAttr, defStyleRes); - - // WebView is important by default, unless app developer overrode attribute. - if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) { - setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); - } - - if (context == null) { - throw new IllegalArgumentException("Invalid context argument"); - } - sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= - Build.VERSION_CODES.JELLY_BEAN_MR2; - checkThread(); - - ensureProviderCreated(); - mProvider.init(javaScriptInterfaces, privateBrowsing); - // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed. - CookieSyncManager.setGetInstanceIsAllowed(); + public WebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } - - /** - * Specifies whether the horizontal scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if horizontal scrollbar should have overlay style - */ - @Deprecated + + // START FAKE PUBLIC METHODS + public void setHorizontalScrollbarOverlay(boolean overlay) { } - /** - * Specifies whether the vertical scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if vertical scrollbar should have overlay style - */ - @Deprecated public void setVerticalScrollbarOverlay(boolean overlay) { } - /** - * Gets whether horizontal scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code true} - */ - @Deprecated public boolean overlayHorizontalScrollbar() { - // The old implementation defaulted to true, so return true for consistency - return true; + return false; } - /** - * Gets whether vertical scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code false} - */ - @Deprecated public boolean overlayVerticalScrollbar() { - // The old implementation defaulted to false, so return false for consistency return false; } - /** - * Gets the visible height (in pixels) of the embedded title bar (if any). - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public int getVisibleTitleHeight() { - checkThread(); - return mProvider.getVisibleTitleHeight(); - } - - /** - * Gets the SSL certificate for the main top-level page or {@code null} if there is - * no certificate (the site is not secure). - * - * @return the SSL certificate for the main top-level page - */ - @Nullable - public SslCertificate getCertificate() { - checkThread(); - return mProvider.getCertificate(); - } - - /** - * Sets the SSL certificate for the main top-level page. - * - * @deprecated Calling this function has no useful effect, and will be - * ignored in future releases. - */ - @Deprecated - public void setCertificate(SslCertificate certificate) { - checkThread(); - mProvider.setCertificate(certificate); - } - - //------------------------------------------------------------------------- - // Methods called by activity - //------------------------------------------------------------------------- - - /** - * Sets a username and password pair for the specified host. This data is - * used by the WebView to autocomplete username and password fields in web - * forms. Note that this is unrelated to the credentials used for HTTP - * authentication. - * - * @param host the host that required the credentials - * @param username the username for the given host - * @param password the password for the given host - * @see WebViewDatabase#clearUsernamePassword - * @see WebViewDatabase#hasUsernamePassword - * @deprecated Saving passwords in WebView will not be supported in future versions. - */ - @Deprecated public void savePassword(String host, String username, String password) { - checkThread(); - mProvider.savePassword(host, username, password); } - /** - * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase} - * instance. - * - * @param host the host to which the credentials apply - * @param realm the realm to which the credentials apply - * @param username the username - * @param password the password - * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead - */ - @Deprecated public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { - checkThread(); - mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } - /** - * Retrieves HTTP authentication credentials for a given host and realm from the {@link - * WebViewDatabase} instance. - * @param host the host to which the credentials apply - * @param realm the realm to which the credentials apply - * @return the credentials as a String array, if found. The first element - * is the username and the second element is the password. {@code null} if - * no credentials are found. - * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead - */ - @Deprecated - @Nullable public String[] getHttpAuthUsernamePassword(String host, String realm) { - checkThread(); - return mProvider.getHttpAuthUsernamePassword(host, realm); + return null; } - /** - * Destroys the internal state of this WebView. This method should be called - * after this WebView has been removed from the view system. No other - * methods may be called on this WebView after destroy. - */ public void destroy() { - checkThread(); - mProvider.destroy(); } - /** - * Enables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void enablePlatformNotifications() { - // noop } - /** - * Disables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void disablePlatformNotifications() { - // noop } - /** - * Used only by internal tests to free up memory. - * - * @hide - */ - public static void freeMemoryForTests() { - getFactory().getStatics().freeMemoryForTests(); + public void loadUrl(String url) { } - /** - * Informs WebView of the network state. This is used to set - * the JavaScript property window.navigator.isOnline and - * generates the online/offline event as specified in HTML5, sec. 5.7.7 - * - * @param networkUp a boolean indicating if network is available - */ - public void setNetworkAvailable(boolean networkUp) { - checkThread(); - mProvider.setNetworkAvailable(networkUp); + public void loadData(String data, String mimeType, String encoding) { } - /** - * Saves the state of this WebView used in - * {@link android.app.Activity#onSaveInstanceState}. Please note that this - * method no longer stores the display data for this WebView. The previous - * behavior could potentially leak files if {@link #restoreState} was never - * called. - * - * @param outState the Bundle to store this WebView's state - * @return the same copy of the back/forward list used to save the state, {@code null} if the - * method fails. - */ - @Nullable - public WebBackForwardList saveState(Bundle outState) { - checkThread(); - return mProvider.saveState(outState); + public void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String failUrl) { } - /** - * Saves the current display data to the Bundle given. Used in conjunction - * with {@link #saveState}. - * @param b a Bundle to store the display data - * @param dest the file to store the serialized picture data. Will be - * overwritten with this WebView's picture data. - * @return {@code true} if the picture was successfully saved - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public boolean savePicture(Bundle b, final File dest) { - checkThread(); - return mProvider.savePicture(b, dest); + public void stopLoading() { } - /** - * Restores the display data that was saved in {@link #savePicture}. Used in - * conjunction with {@link #restoreState}. Note that this will not work if - * this WebView is hardware accelerated. - * - * @param b a Bundle containing the saved display data - * @param src the file where the picture data was stored - * @return {@code true} if the picture was successfully restored - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public boolean restorePicture(Bundle b, File src) { - checkThread(); - return mProvider.restorePicture(b, src); + public void reload() { } - /** - * Restores the state of this WebView from the given Bundle. This method is - * intended for use in {@link android.app.Activity#onRestoreInstanceState} - * and should be called to restore the state of this WebView. If - * it is called after this WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable - * side-effects. Please note that this method no longer restores the - * display data for this WebView. - * - * @param inState the incoming Bundle of state - * @return the restored back/forward list or {@code null} if restoreState failed - */ - @Nullable - public WebBackForwardList restoreState(Bundle inState) { - checkThread(); - return mProvider.restoreState(inState); + public boolean canGoBack() { + return false; } - /** - * Loads the given URL with the specified additional HTTP headers. - *

    - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - * @param additionalHttpHeaders the additional headers to be used in the - * HTTP request for this URL, specified as a map from name to - * value. Note that if this map contains any of the headers - * that are set by default by this WebView, such as those - * controlling caching, accept types or the User-Agent, their - * values may be overridden by this WebView's defaults. - */ - public void loadUrl(String url, Map additionalHttpHeaders) { - checkThread(); - mProvider.loadUrl(url, additionalHttpHeaders); + public void goBack() { } - /** - * Loads the given URL. - *

    - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - */ - public void loadUrl(String url) { - checkThread(); - mProvider.loadUrl(url); + public boolean canGoForward() { + return false; } - /** - * Loads the URL with postData using "POST" method into this WebView. If url - * is not a network URL, it will be loaded with {@link #loadUrl(String)} - * instead, ignoring the postData param. - * - * @param url the URL of the resource to load - * @param postData the data will be passed to "POST" request, which must be - * be "application/x-www-form-urlencoded" encoded. - */ - public void postUrl(String url, byte[] postData) { - checkThread(); - if (URLUtil.isNetworkUrl(url)) { - mProvider.postUrl(url, postData); - } else { - mProvider.loadUrl(url); - } + public void goForward() { } - /** - * Loads the given data into this WebView using a 'data' scheme URL. - *

    - * Note that JavaScript's same origin policy means that script running in a - * page loaded using this method will be unable to access content loaded - * using any scheme other than 'data', including 'http(s)'. To avoid this - * restriction, use {@link - * #loadDataWithBaseURL(String,String,String,String,String) - * loadDataWithBaseURL()} with an appropriate base URL. - *

    - * The encoding parameter specifies whether the data is base64 or URL - * encoded. If the data is base64 encoded, the value of the encoding - * parameter must be 'base64'. For all other values of the parameter, - * including {@code null}, it is assumed that the data uses ASCII encoding for - * octets inside the range of safe URL characters and use the standard %xx - * hex encoding of URLs for octets outside that range. For example, '#', - * '%', '\', '?' should be replaced by %23, %25, %27, %3f respectively. - *

    - * The 'data' scheme URL formed by this method uses the default US-ASCII - * charset. If you need need to set a different charset, you should form a - * 'data' scheme URL which explicitly specifies a charset parameter in the - * mediatype portion of the URL and call {@link #loadUrl(String)} instead. - * Note that the charset obtained from the mediatype portion of a data URL - * always overrides that specified in the HTML or XML document itself. - * - * @param data a String of data in the given encoding - * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null}, - * defaults to 'text/html'. - * @param encoding the encoding of the data - */ - public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) { - checkThread(); - mProvider.loadData(data, mimeType, encoding); + public boolean canGoBackOrForward(int steps) { + return false; } - /** - * Loads the given data into this WebView, using baseUrl as the base URL for - * the content. The base URL is used both to resolve relative URLs and when - * applying JavaScript's same origin policy. The historyUrl is used for the - * history entry. - *

    - * Note that content specified in this way can access local device files - * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than - * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'. - *

    - * If the base URL uses the data scheme, this method is equivalent to - * calling {@link #loadData(String,String,String) loadData()} and the - * historyUrl is ignored, and the data will be treated as part of a data: URL. - * If the base URL uses any other scheme, then the data will be loaded into - * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded - * entities in the string will not be decoded. - *

    - * Note that the baseUrl is sent in the 'Referer' HTTP header when - * requesting subresources (images, etc.) of the page loaded using this method. - * - * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to - * 'about:blank'. - * @param data a String of data in the given encoding - * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null}, - * defaults to 'text/html'. - * @param encoding the encoding of the data - * @param historyUrl the URL to use as the history entry. If {@code null} defaults - * to 'about:blank'. If non-null, this must be a valid URL. - */ - public void loadDataWithBaseURL(@Nullable String baseUrl, String data, - @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) { - checkThread(); - mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + public void goBackOrForward(int steps) { } - /** - * Asynchronously evaluates JavaScript in the context of the currently displayed page. - * If non-null, |resultCallback| will be invoked with any result returned from that - * execution. This method must be called on the UI thread and the callback will - * be made on the UI thread. - *

    - * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or - * later, JavaScript state from an empty WebView is no longer persisted across navigations like - * {@link #loadUrl(String)}. For example, global variables and functions defined before calling - * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use - * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations. - * - * @param script the JavaScript to execute. - * @param resultCallback A callback to be invoked when the script execution - * completes with the result of the execution (if any). - * May be {@code null} if no notification of the result is required. - */ - public void evaluateJavascript(String script, @Nullable ValueCallback resultCallback) { - checkThread(); - mProvider.evaluateJavaScript(script, resultCallback); + public boolean pageUp(boolean top) { + return false; + } + + public boolean pageDown(boolean bottom) { + return false; } - /** - * Saves the current view as a web archive. - * - * @param filename the filename where the archive should be placed - */ - public void saveWebArchive(String filename) { - checkThread(); - mProvider.saveWebArchive(filename); + public void clearView() { + } + + public Picture capturePicture() { + return null; } - /** - * Saves the current view as a web archive. - * - * @param basename the filename where the archive should be placed - * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename - * is assumed to be a directory in which a filename will be - * chosen according to the URL of the current page. - * @param callback called after the web archive has been saved. The - * parameter for onReceiveValue will either be the filename - * under which the file was saved, or {@code null} if saving the - * file failed. - */ - public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback - callback) { - checkThread(); - mProvider.saveWebArchive(basename, autoname, callback); + public float getScale() { + return 0; } - /** - * Stops the current load. - */ - public void stopLoading() { - checkThread(); - mProvider.stopLoading(); + public void setInitialScale(int scaleInPercent) { } - /** - * Reloads the current URL. - */ - public void reload() { - checkThread(); - mProvider.reload(); + public void invokeZoomPicker() { } - /** - * Gets whether this WebView has a back history item. - * - * @return {@code true} if this WebView has a back history item - */ - public boolean canGoBack() { - checkThread(); - return mProvider.canGoBack(); + public void requestFocusNodeHref(Message hrefMsg) { } - /** - * Goes back in the history of this WebView. - */ - public void goBack() { - checkThread(); - mProvider.goBack(); + public void requestImageRef(Message msg) { } - /** - * Gets whether this WebView has a forward history item. - * - * @return {@code true} if this WebView has a forward history item - */ - public boolean canGoForward() { - checkThread(); - return mProvider.canGoForward(); + public String getUrl() { + return null; } - /** - * Goes forward in the history of this WebView. - */ - public void goForward() { - checkThread(); - mProvider.goForward(); + public String getTitle() { + return null; } - /** - * Gets whether the page can go back or forward the given - * number of steps. - * - * @param steps the negative or positive number of steps to move the - * history - */ - public boolean canGoBackOrForward(int steps) { - checkThread(); - return mProvider.canGoBackOrForward(steps); + public Bitmap getFavicon() { + return null; } - /** - * Goes to the history item that is the number of steps away from - * the current item. Steps is negative if backward and positive - * if forward. - * - * @param steps the number of steps to take back or forward in the back - * forward list - */ - public void goBackOrForward(int steps) { - checkThread(); - mProvider.goBackOrForward(steps); + public int getProgress() { + return 0; + } + + public int getContentHeight() { + return 0; } - /** - * Gets whether private browsing is enabled in this WebView. - */ - public boolean isPrivateBrowsingEnabled() { - checkThread(); - return mProvider.isPrivateBrowsingEnabled(); + public void pauseTimers() { } - /** - * Scrolls the contents of this WebView up by half the view size. - * - * @param top {@code true} to jump to the top of the page - * @return {@code true} if the page was scrolled - */ - public boolean pageUp(boolean top) { - checkThread(); - return mProvider.pageUp(top); + public void resumeTimers() { } - /** - * Scrolls the contents of this WebView down by half the page size. - * - * @param bottom {@code true} to jump to bottom of page - * @return {@code true} if the page was scrolled - */ - public boolean pageDown(boolean bottom) { - checkThread(); - return mProvider.pageDown(bottom); + public void clearCache() { } - /** - * Posts a {@link VisualStateCallback}, which will be called when - * the current state of the WebView is ready to be drawn. - * - *

    Because updates to the DOM are processed asynchronously, updates to the DOM may not - * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The - * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of - * the DOM at the current time are ready to be drawn the next time the {@link WebView} - * draws. - * - *

    The next draw after the callback completes is guaranteed to reflect all the updates to the - * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also - * contain updates applied after the callback was posted. - * - *

    The state of the DOM covered by this API includes the following: - *

      - *
    • primitive HTML elements (div, img, span, etc..)
    • - *
    • images
    • - *
    • CSS animations
    • - *
    • WebGL
    • - *
    • canvas
    • - *
    - * It does not include the state of: - *
      - *
    • the video tag
    • - *
    - * - *

    To guarantee that the {@link WebView} will successfully render the first frame - * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions - * must be met: - *

      - *
    • If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then - * the {@link WebView} must be attached to the view hierarchy.
    • - *
    • If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE} - * then the {@link WebView} must be attached to the view hierarchy and must be made - * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.
    • - *
    • If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the - * {@link WebView} must be attached to the view hierarchy and its - * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed - * values and must be made {@link View#VISIBLE VISIBLE} from the - * {@link VisualStateCallback#onComplete} method.
    • - *
    - * - *

    When using this API it is also recommended to enable pre-rasterization if the {@link - * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for - * more details and do consider its caveats. - * - * @param requestId An id that will be returned in the callback to allow callers to match - * requests with callbacks. - * @param callback The callback to be invoked. - */ - public void postVisualStateCallback(long requestId, VisualStateCallback callback) { - checkThread(); - mProvider.insertVisualStateCallback(requestId, callback); + public void clearFormData() { } - /** - * Clears this WebView so that onDraw() will draw nothing but white background, - * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY. - * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state - * and release page resources (including any running JavaScript). - */ - @Deprecated - public void clearView() { - checkThread(); - mProvider.clearView(); + public void clearHistory() { } - /** - * Gets a new picture that captures the current contents of this WebView. - * The picture is of the entire document being displayed, and is not - * limited to the area currently displayed by this WebView. Also, the - * picture is a static copy and is unaffected by later changes to the - * content being displayed. - *

    - * Note that due to internal changes, for API levels between - * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and - * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the - * picture does not include fixed position elements or scrollable divs. - *

    - * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture - * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve - * additional conversion at a cost in memory and performance. Also the - * {@link android.graphics.Picture#createFromStream} and - * {@link android.graphics.Picture#writeToStream} methods are not supported on the - * returned object. - * - * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or - * {@link #saveWebArchive} to save the content to a file. - * - * @return a picture that captures the current contents of this WebView - */ - @Deprecated - public Picture capturePicture() { - checkThread(); - return mProvider.capturePicture(); + public void clearSslPreferences() { } - /** - * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user - * to provide a print document name. - */ - @Deprecated - public PrintDocumentAdapter createPrintDocumentAdapter() { - checkThread(); - return mProvider.createPrintDocumentAdapter("default"); + public static String findAddress(String addr) { + return null; } - /** - * Creates a PrintDocumentAdapter that provides the content of this WebView for printing. - * - * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot - * be drawn during the conversion process - any such draws are undefined. It is recommended - * to use a dedicated off screen WebView for the printing. If necessary, an application may - * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance - * wrapped around the object returned and observing the onStart and onFinish methods. See - * {@link android.print.PrintDocumentAdapter} for more information. - * - * @param documentName The user-facing name of the printed document. See - * {@link android.print.PrintDocumentInfo} - */ - public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) { - checkThread(); - return mProvider.createPrintDocumentAdapter(documentName); + public void documentHasImages(Message response) { } - /** - * Gets the current scale of this WebView. - * - * @return the current scale - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - @ViewDebug.ExportedProperty(category = "webview") - public float getScale() { - checkThread(); - return mProvider.getScale(); + public void setWebViewClient(WebViewClient client) { } - /** - * Sets the initial scale for this WebView. 0 means default. - * The behavior for the default scale depends on the state of - * {@link WebSettings#getUseWideViewPort()} and - * {@link WebSettings#getLoadWithOverviewMode()}. - * If the content fits into the WebView control by width, then - * the zoom is set to 100%. For wide content, the behavior - * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}. - * If its value is {@code true}, the content will be zoomed out to be fit - * by width into the WebView control, otherwise not. - * - * If initial scale is greater than 0, WebView starts with this value - * as initial scale. - * Please note that unlike the scale properties in the viewport meta tag, - * this method doesn't take the screen density into account. - * - * @param scaleInPercent the initial scale in percent - */ - public void setInitialScale(int scaleInPercent) { - checkThread(); - mProvider.setInitialScale(scaleInPercent); + public void setDownloadListener(DownloadListener listener) { } - /** - * Invokes the graphical zoom picker widget for this WebView. This will - * result in the zoom widget appearing on the screen to control the zoom - * level of this WebView. - */ - public void invokeZoomPicker() { - checkThread(); - mProvider.invokeZoomPicker(); + public void setWebChromeClient(WebChromeClient client) { } - /** - * Gets a HitTestResult based on the current cursor node. If a HTML::a - * tag is found and the anchor has a non-JavaScript URL, the HitTestResult - * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field. - * If the anchor does not have a URL or if it is a JavaScript URL, the type - * will be UNKNOWN_TYPE and the URL has to be retrieved through - * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is - * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in - * the "extra" field. A type of - * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as - * a child node. If a phone number is found, the HitTestResult type is set - * to PHONE_TYPE and the phone number is set in the "extra" field of - * HitTestResult. If a map address is found, the HitTestResult type is set - * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. - * If an email address is found, the HitTestResult type is set to EMAIL_TYPE - * and the email is set in the "extra" field of HitTestResult. Otherwise, - * HitTestResult type is set to UNKNOWN_TYPE. - */ - public HitTestResult getHitTestResult() { - checkThread(); - return mProvider.getHitTestResult(); + public void addJavascriptInterface(Object obj, String interfaceName) { } - /** - * Requests the anchor or image element URL at the last tapped point. - * If hrefMsg is {@code null}, this method returns immediately and does not - * dispatch hrefMsg to its target. If the tapped point hits an image, - * an anchor, or an image in an anchor, the message associates - * strings in named keys in its data. The value paired with the key - * may be an empty string. - * - * @param hrefMsg the message to be dispatched with the result of the - * request. The message data contains three keys. "url" - * returns the anchor's href attribute. "title" returns the - * anchor's text. "src" returns the image's src attribute. - */ - public void requestFocusNodeHref(@Nullable Message hrefMsg) { - checkThread(); - mProvider.requestFocusNodeHref(hrefMsg); - } - - /** - * Requests the URL of the image last touched by the user. msg will be sent - * to its target with a String representing the URL as its object. - * - * @param msg the message to be dispatched with the result of the request - * as the data member with "url" as key. The result can be {@code null}. - */ - public void requestImageRef(Message msg) { - checkThread(); - mProvider.requestImageRef(msg); - } - - /** - * Gets the URL for the current page. This is not always the same as the URL - * passed to WebViewClient.onPageStarted because although the load for - * that URL has begun, the current page may not have changed. - * - * @return the URL for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") - public String getUrl() { - checkThread(); - return mProvider.getUrl(); - } - - /** - * Gets the original URL for the current page. This is not always the same - * as the URL passed to WebViewClient.onPageStarted because although the - * load for that URL has begun, the current page may not have changed. - * Also, there may have been redirects resulting in a different URL to that - * originally requested. - * - * @return the URL that was originally requested for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") - public String getOriginalUrl() { - checkThread(); - return mProvider.getOriginalUrl(); - } - - /** - * Gets the title for the current page. This is the title of the current page - * until WebViewClient.onReceivedTitle is called. - * - * @return the title for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") - public String getTitle() { - checkThread(); - return mProvider.getTitle(); - } - - /** - * Gets the favicon for the current page. This is the favicon of the current - * page until WebViewClient.onReceivedIcon is called. - * - * @return the favicon for the current page - */ - public Bitmap getFavicon() { - checkThread(); - return mProvider.getFavicon(); - } - - /** - * Gets the touch icon URL for the apple-touch-icon element, or - * a URL on this site's server pointing to the standard location of a - * touch icon. - * - * @hide - */ - public String getTouchIconUrl() { - return mProvider.getTouchIconUrl(); - } - - /** - * Gets the progress for the current page. - * - * @return the progress for the current page between 0 and 100 - */ - public int getProgress() { - checkThread(); - return mProvider.getProgress(); - } - - /** - * Gets the height of the HTML content. - * - * @return the height of the HTML content - */ - @ViewDebug.ExportedProperty(category = "webview") - public int getContentHeight() { - checkThread(); - return mProvider.getContentHeight(); - } - - /** - * Gets the width of the HTML content. - * - * @return the width of the HTML content - * @hide - */ - @ViewDebug.ExportedProperty(category = "webview") - public int getContentWidth() { - return mProvider.getContentWidth(); - } - - /** - * Pauses all layout, parsing, and JavaScript timers for all WebViews. This - * is a global requests, not restricted to just this WebView. This can be - * useful if the application has been paused. - */ - public void pauseTimers() { - checkThread(); - mProvider.pauseTimers(); - } - - /** - * Resumes all layout, parsing, and JavaScript timers for all WebViews. - * This will resume dispatching all timers. - */ - public void resumeTimers() { - checkThread(); - mProvider.resumeTimers(); - } - - /** - * Does a best-effort attempt to pause any processing that can be paused - * safely, such as animations and geolocation. Note that this call - * does not pause JavaScript. To pause JavaScript globally, use - * {@link #pauseTimers}. - * - * To resume WebView, call {@link #onResume}. - */ - public void onPause() { - checkThread(); - mProvider.onPause(); - } - - /** - * Resumes a WebView after a previous call to {@link #onPause}. - */ - public void onResume() { - checkThread(); - mProvider.onResume(); - } - - /** - * Gets whether this WebView is paused, meaning onPause() was called. - * Calling onResume() sets the paused state back to {@code false}. - * - * @hide - */ - public boolean isPaused() { - return mProvider.isPaused(); - } - - /** - * Informs this WebView that memory is low so that it can free any available - * memory. - * @deprecated Memory caches are automatically dropped when no longer needed, and in response - * to system memory pressure. - */ - @Deprecated - public void freeMemory() { - checkThread(); - mProvider.freeMemory(); - } - - /** - * Clears the resource cache. Note that the cache is per-application, so - * this will clear the cache for all WebViews used. - * - * @param includeDiskFiles if {@code false}, only the RAM cache is cleared - */ - public void clearCache(boolean includeDiskFiles) { - checkThread(); - mProvider.clearCache(includeDiskFiles); - } - - /** - * Removes the autocomplete popup from the currently focused form field, if - * present. Note this only affects the display of the autocomplete popup, - * it does not remove any saved form data from this WebView's store. To do - * that, use {@link WebViewDatabase#clearFormData}. - */ - public void clearFormData() { - checkThread(); - mProvider.clearFormData(); - } - - /** - * Tells this WebView to clear its internal back/forward list. - */ - public void clearHistory() { - checkThread(); - mProvider.clearHistory(); - } - - /** - * Clears the SSL preferences table stored in response to proceeding with - * SSL certificate errors. - */ - public void clearSslPreferences() { - checkThread(); - mProvider.clearSslPreferences(); - } - - /** - * Clears the client certificate preferences stored in response - * to proceeding/cancelling client cert requests. Note that WebView - * automatically clears these preferences when it receives a - * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are - * shared by all the WebViews that are created by the embedder application. - * - * @param onCleared A runnable to be invoked when client certs are cleared. - * The runnable will be called in UI thread. - */ - public static void clearClientCertPreferences(@Nullable Runnable onCleared) { - getFactory().getStatics().clearClientCertPreferences(onCleared); - } - - /** - * Starts Safe Browsing initialization. - *

    - * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is - * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those - * devices {@code callback} will receive {@code false}. - *

    - * This does not enable the Safe Browsing feature itself, and should only be called if Safe - * Browsing is enabled by the manifest tag or {@link WebSettings#setSafeBrowsingEnabled}. This - * prepares resources used for Safe Browsing. - *

    - * This should be called with the Application Context (and will always use the Application - * context to do its work regardless). - * - * @param context Application Context. - * @param callback will be called on the UI thread with {@code true} if initialization is - * successful, {@code false} otherwise. - */ - public static void startSafeBrowsing(Context context, - @Nullable ValueCallback callback) { - getFactory().getStatics().initSafeBrowsing(context, callback); - } - - /** - * Sets the list of domains that are exempt from SafeBrowsing checks. The list is - * global for all the WebViews. - *

    - * Each rule should take one of these: - * - * - * - * - * - * - *
    Rule Example Matches Subdomain
    HOSTNAME example.com Yes
    .HOSTNAME .example.com No
    IPV4_LITERAL 192.168.1.1 No
    IPV6_LITERAL_WITH_BRACKETS [10:20:30:40:50:60:70:80]No
    - *

    - * All other rules, including wildcards, are invalid. - * - * @param urls the list of URLs - * @param callback will be called with {@code true} if URLs are successfully added to the - * whitelist. It will be called with {@code false} if any URLs are malformed. The callback will - * be run on the UI thread - */ - public static void setSafeBrowsingWhitelist(@NonNull List urls, - @Nullable ValueCallback callback) { - getFactory().getStatics().setSafeBrowsingWhitelist(urls, callback); - } - - /** - * Returns a URL pointing to the privacy policy for Safe Browsing reporting. - * - * @return the url pointing to a privacy policy document which can be displayed to users. - */ - @NonNull - public static Uri getSafeBrowsingPrivacyPolicyUrl() { - return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl(); - } - - /** - * Gets the WebBackForwardList for this WebView. This contains the - * back/forward list for use in querying each item in the history stack. - * This is a copy of the private WebBackForwardList so it contains only a - * snapshot of the current state. Multiple calls to this method may return - * different objects. The object returned from this method will not be - * updated to reflect any new state. - */ - public WebBackForwardList copyBackForwardList() { - checkThread(); - return mProvider.copyBackForwardList(); - - } - - /** - * Registers the listener to be notified as find-on-page operations - * progress. This will replace the current listener. - * - * @param listener an implementation of {@link FindListener} - */ - public void setFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mUserFindListener = listener; - } - - /** - * Highlights and scrolls to the next match found by - * {@link #findAllAsync}, wrapping around page boundaries as necessary. - * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)} - * has not been called yet, or if {@link #clearMatches} has been called since the - * last find operation, this function does nothing. - * - * @param forward the direction to search - * @see #setFindListener - */ - public void findNext(boolean forward) { - checkThread(); - mProvider.findNext(forward); - } - - /** - * Finds all instances of find on the page and highlights them. - * Notifies any registered {@link FindListener}. - * - * @param find the string to find - * @return the number of occurrences of the String "find" that were found - * @deprecated {@link #findAllAsync} is preferred. - * @see #setFindListener - */ - @Deprecated - public int findAll(String find) { - checkThread(); - StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); - return mProvider.findAll(find); - } - - /** - * Finds all instances of find on the page and highlights them, - * asynchronously. Notifies any registered {@link FindListener}. - * Successive calls to this will cancel any pending searches. - * - * @param find the string to find. - * @see #setFindListener - */ - public void findAllAsync(String find) { - checkThread(); - mProvider.findAllAsync(find); - } - - /** - * Starts an ActionMode for finding text in this WebView. Only works if this - * WebView is attached to the view system. - * - * @param text if non-null, will be the initial text to search for. - * Otherwise, the last String searched for in this WebView will - * be used to start. - * @param showIme if {@code true}, show the IME, assuming the user will begin typing. - * If {@code false} and text is non-null, perform a find all. - * @return {@code true} if the find dialog is shown, {@code false} otherwise - * @deprecated This method does not work reliably on all Android versions; - * implementing a custom find dialog using WebView.findAllAsync() - * provides a more robust solution. - */ - @Deprecated - public boolean showFindDialog(@Nullable String text, boolean showIme) { - checkThread(); - return mProvider.showFindDialog(text, showIme); - } - - /** - * Gets the first substring consisting of the address of a physical - * location. Currently, only addresses in the United States are detected, - * and consist of: - *

      - *
    • a house number
    • - *
    • a street name
    • - *
    • a street type (Road, Circle, etc), either spelled out or - * abbreviated
    • - *
    • a city name
    • - *
    • a state or territory, either spelled out or two-letter abbr
    • - *
    • an optional 5 digit or 9 digit zip code
    • - *
    - * All names must be correctly capitalized, and the zip code, if present, - * must be valid for the state. The street type must be a standard USPS - * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed - * five digits. - * - * @param addr the string to search for addresses - * @return the address, or if no address is found, {@code null} - */ - @Nullable - public static String findAddress(String addr) { - // TODO: Rewrite this in Java so it is not needed to start up chromium - // Could also be deprecated - return getFactory().getStatics().findAddress(addr); - } - - /** - * For apps targeting the L release, WebView has a new default behavior that reduces - * memory footprint and increases performance by intelligently choosing - * the portion of the HTML document that needs to be drawn. These - * optimizations are transparent to the developers. However, under certain - * circumstances, an App developer may want to disable them: - *
      - *
    1. When an app uses {@link #onDraw} to do own drawing and accesses portions - * of the page that is way outside the visible portion of the page.
    2. - *
    3. When an app uses {@link #capturePicture} to capture a very large HTML document. - * Note that capturePicture is a deprecated API.
    4. - *
    - * Enabling drawing the entire HTML document has a significant performance - * cost. This method should be called before any WebViews are created. - */ - public static void enableSlowWholeDocumentDraw() { - getFactory().getStatics().enableSlowWholeDocumentDraw(); - } - - /** - * Clears the highlighting surrounding text matches created by - * {@link #findAllAsync}. - */ - public void clearMatches() { - checkThread(); - mProvider.clearMatches(); - } - - /** - * Queries the document to see if it contains any image references. The - * message object will be dispatched with arg1 being set to 1 if images - * were found and 0 if the document does not reference any images. - * - * @param response the message that will be dispatched with the result - */ - public void documentHasImages(Message response) { - checkThread(); - mProvider.documentHasImages(response); - } - - /** - * Sets the WebViewClient that will receive various notifications and - * requests. This will replace the current handler. - * - * @param client an implementation of WebViewClient - * @see #getWebViewClient - */ - public void setWebViewClient(WebViewClient client) { - checkThread(); - mProvider.setWebViewClient(client); - } - - /** - * Gets the WebViewClient. - * - * @return the WebViewClient, or a default client if not yet set - * @see #setWebViewClient - */ - public WebViewClient getWebViewClient() { - checkThread(); - return mProvider.getWebViewClient(); - } - - /** - * Registers the interface to be used when content can not be handled by - * the rendering engine, and should be downloaded instead. This will replace - * the current handler. - * - * @param listener an implementation of DownloadListener - */ - public void setDownloadListener(DownloadListener listener) { - checkThread(); - mProvider.setDownloadListener(listener); - } - - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for - * use in handling JavaScript dialogs, favicons, titles, and the progress. - * This will replace the current handler. - * - * @param client an implementation of WebChromeClient - * @see #getWebChromeClient - */ - public void setWebChromeClient(WebChromeClient client) { - checkThread(); - mProvider.setWebChromeClient(client); - } - - /** - * Gets the chrome handler. - * - * @return the WebChromeClient, or {@code null} if not yet set - * @see #setWebChromeClient - */ - @Nullable - public WebChromeClient getWebChromeClient() { - checkThread(); - return mProvider.getWebChromeClient(); - } - - /** - * Sets the Picture listener. This is an interface used to receive - * notifications of a new Picture. - * - * @param listener an implementation of WebView.PictureListener - * @deprecated This method is now obsolete. - */ - @Deprecated - public void setPictureListener(PictureListener listener) { - checkThread(); - mProvider.setPictureListener(listener); - } - - /** - * Injects the supplied Java object into this WebView. The object is - * injected into the JavaScript context of the main frame, using the - * supplied name. This allows the Java object's methods to be - * accessed from JavaScript. For applications targeted to API - * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and above, only public methods that are annotated with - * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript. - * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below, - * all public methods (including the inherited ones) can be accessed, see the - * important security note below for implications. - *

    Note that injected objects will not appear in JavaScript until the page is next - * (re)loaded. JavaScript should be enabled before injecting the object. For example: - *

    -     * class JsObject {
    -     *    {@literal @}JavascriptInterface
    -     *    public String toString() { return "injectedObject"; }
    -     * }
    -     * webview.getSettings().setJavaScriptEnabled(true);
    -     * webView.addJavascriptInterface(new JsObject(), "injectedObject");
    -     * webView.loadData("", "text/html", null);
    -     * webView.loadUrl("javascript:alert(injectedObject.toString())");
    - *

    - * IMPORTANT: - *

      - *
    • This method can be used to allow JavaScript to control the host - * application. This is a powerful feature, but also presents a security - * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier. - * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - * are still vulnerable if the app runs on a device running Android earlier than 4.2. - * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and to ensure the method is called only when running on Android 4.2 or later. - * With these older versions, JavaScript could use reflection to access an - * injected object's public fields. Use of this method in a WebView - * containing untrusted content could allow an attacker to manipulate the - * host application in unintended ways, executing Java code with the - * permissions of the host application. Use extreme care when using this - * method in a WebView which could contain untrusted content.
    • - *
    • JavaScript interacts with Java object on a private, background - * thread of this WebView. Care is therefore required to maintain thread - * safety. - *
    • - *
    • The Java object's fields are not accessible.
    • - *
    • For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP} - * and above, methods of injected Java objects are enumerable from - * JavaScript.
    • - *
    - * - * @param object the Java object to inject into this WebView's JavaScript - * context. {@code null} values are ignored. - * @param name the name used to expose the object in JavaScript - */ - public void addJavascriptInterface(Object object, String name) { - checkThread(); - mProvider.addJavascriptInterface(object, name); - } - - /** - * Removes a previously injected Java object from this WebView. Note that - * the removal will not be reflected in JavaScript until the page is next - * (re)loaded. See {@link #addJavascriptInterface}. - * - * @param name the name used to expose the object in JavaScript - */ - public void removeJavascriptInterface(@NonNull String name) { - checkThread(); - mProvider.removeJavascriptInterface(name); - } - - /** - * Creates a message channel to communicate with JS and returns the message - * ports that represent the endpoints of this message channel. The HTML5 message - * channel functionality is described - * here - * - * - *

    The returned message channels are entangled and already in started state. - * - * @return the two message ports that form the message channel. - */ - public WebMessagePort[] createWebMessageChannel() { - checkThread(); - return mProvider.createWebMessageChannel(); - } - - /** - * Post a message to main frame. The embedded application can restrict the - * messages to a certain target origin. See - * - * HTML5 spec for how target origin can be used. - *

    - * A target origin can be set as a wildcard ("*"). However this is not recommended. - * See the page above for security issues. - * - * @param message the WebMessage - * @param targetOrigin the target origin. - */ - public void postWebMessage(WebMessage message, Uri targetOrigin) { - checkThread(); - mProvider.postMessageToMainFrame(message, targetOrigin); - } - - /** - * Gets the WebSettings object used to control the settings for this - * WebView. - * - * @return a WebSettings object that can be used to control this WebView's - * settings - */ - public WebSettings getSettings() { - checkThread(); - return mProvider.getSettings(); - } - - /** - * Enables debugging of web contents (HTML / CSS / JavaScript) - * loaded into any WebViews of this application. This flag can be enabled - * in order to facilitate debugging of web layouts and JavaScript - * code running inside WebViews. Please refer to WebView documentation - * for the debugging guide. - * - * The default is {@code false}. - * - * @param enabled whether to enable web contents debugging - */ - public static void setWebContentsDebuggingEnabled(boolean enabled) { - getFactory().getStatics().setWebContentsDebuggingEnabled(enabled); - } - - /** - * Gets the list of currently loaded plugins. - * - * @return the list of currently loaded plugins - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public static synchronized PluginList getPluginList() { - return new PluginList(); - } - - /** - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public void refreshPlugins(boolean reloadOpenPages) { - checkThread(); - } - - /** - * Puts this WebView into text selection mode. Do not rely on this - * functionality; it will be deprecated in the future. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void emulateShiftHeld() { - checkThread(); - } - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewAdded(View parent, View child) {} - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewRemoved(View p, View child) {} - - /** - * @deprecated WebView should not have implemented - * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - } - - /** - * @deprecated Only the default case, {@code true}, will be supported in a future version. - */ - @Deprecated - public void setMapTrackballToArrowKeys(boolean setMap) { - checkThread(); - mProvider.setMapTrackballToArrowKeys(setMap); - } - - - public void flingScroll(int vx, int vy) { - checkThread(); - mProvider.flingScroll(vx, vy); - } - - /** - * Gets the zoom controls for this WebView, as a separate View. The caller - * is responsible for inserting this View into the layout hierarchy. - *

    - * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced - * built-in zoom mechanisms for the WebView, as opposed to these separate - * zoom controls. The built-in mechanisms are preferred and can be enabled - * using {@link WebSettings#setBuiltInZoomControls}. - * - * @deprecated the built-in zoom mechanisms are preferred - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - */ - @Deprecated public View getZoomControls() { - checkThread(); - return mProvider.getZoomControls(); - } - - /** - * Gets whether this WebView can be zoomed in. - * - * @return {@code true} if this WebView can be zoomed in - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomIn() { - checkThread(); - return mProvider.canZoomIn(); - } - - /** - * Gets whether this WebView can be zoomed out. - * - * @return {@code true} if this WebView can be zoomed out - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomOut() { - checkThread(); - return mProvider.canZoomOut(); - } - - /** - * Performs a zoom operation in this WebView. - * - * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's - * zoom limits. This value must be in the range 0.01 to 100.0 inclusive. - */ - public void zoomBy(float zoomFactor) { - checkThread(); - if (zoomFactor < 0.01) - throw new IllegalArgumentException("zoomFactor must be greater than 0.01."); - if (zoomFactor > 100.0) - throw new IllegalArgumentException("zoomFactor must be less than 100."); - mProvider.zoomBy(zoomFactor); + return null; } - /** - * Performs zoom in in this WebView. - * - * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes - */ public boolean zoomIn() { - checkThread(); - return mProvider.zoomIn(); + return false; } - /** - * Performs zoom out in this WebView. - * - * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes - */ public boolean zoomOut() { - checkThread(); - return mProvider.zoomOut(); - } - - /** - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void debugDump() { - checkThread(); - } - - /** - * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)} - * @hide - */ - @Override - public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) { - mProvider.dumpViewHierarchyWithProperties(out, level); - } - - /** - * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)} - * @hide - */ - @Override - public View findHierarchyView(String className, int hashCode) { - return mProvider.findHierarchyView(className, hashCode); - } - - /** @hide */ - @IntDef({ - RENDERER_PRIORITY_WAIVED, - RENDERER_PRIORITY_BOUND, - RENDERER_PRIORITY_IMPORTANT - }) - @Retention(RetentionPolicy.SOURCE) - public @interface RendererPriority {} - - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level - * {@link WebView} renderers will be strong targets for out of memory - * killing. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_WAIVED = 0; - /** - * The renderer associated with this WebView is bound with - * the default priority for services. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_BOUND = 1; - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_IMPORTANT}. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_IMPORTANT = 2; - - /** - * Set the renderer priority policy for this {@link WebView}. The - * priority policy will be used to determine whether an out of - * process renderer should be considered to be a target for OOM - * killing. - * - * Because a renderer can be associated with more than one - * WebView, the final priority it is computed as the maximum of - * any attached WebViews. When a WebView is destroyed it will - * cease to be considerered when calculating the renderer - * priority. Once no WebViews remain associated with the renderer, - * the priority of the renderer will be reduced to - * {@link #RENDERER_PRIORITY_WAIVED}. - * - * The default policy is to set the priority to - * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility, - * and this should not be changed unless the caller also handles - * renderer crashes with - * {@link WebViewClient#onRenderProcessGone}. Any other setting - * will result in WebView renderers being killed by the system - * more aggressively than the application. - * - * @param rendererRequestedPriority the minimum priority at which - * this WebView desires the renderer process to be bound. - * @param waivedWhenNotVisible if {@code true}, this flag specifies that - * when this WebView is not visible, it will be treated as - * if it had requested a priority of - * {@link #RENDERER_PRIORITY_WAIVED}. - */ - public void setRendererPriorityPolicy( - @RendererPriority int rendererRequestedPriority, - boolean waivedWhenNotVisible) { - mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible); - } - - /** - * Get the requested renderer priority for this WebView. - * - * @return the requested renderer priority policy. - */ - @RendererPriority - public int getRendererRequestedPriority() { - return mProvider.getRendererRequestedPriority(); - } - - /** - * Return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - * - * @return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - */ - public boolean getRendererPriorityWaivedWhenNotVisible() { - return mProvider.getRendererPriorityWaivedWhenNotVisible(); - } - - /** - * Sets the {@link TextClassifier} for this WebView. - */ - public void setTextClassifier(@Nullable TextClassifier textClassifier) { - mProvider.setTextClassifier(textClassifier); - } - - /** - * Returns the {@link TextClassifier} used by this WebView. - * If no TextClassifier has been set, this WebView uses the default set by the system. - */ - @NonNull - public TextClassifier getTextClassifier() { - return mProvider.getTextClassifier(); - } - - //------------------------------------------------------------------------- - // Interface for WebView providers - //------------------------------------------------------------------------- - - /** - * Gets the WebViewProvider. Used by providers to obtain the underlying - * implementation, e.g. when the application responds to - * WebViewClient.onCreateWindow() request. - * - * @hide WebViewProvider is not public API. - */ - @SystemApi - public WebViewProvider getWebViewProvider() { - return mProvider; - } - - /** - * Callback interface, allows the provider implementation to access non-public methods - * and fields, and make super-class calls in this WebView instance. - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public class PrivateAccess { - // ---- Access to super-class methods ---- - public int super_getScrollBarStyle() { - return WebView.super.getScrollBarStyle(); - } - - public void super_scrollTo(int scrollX, int scrollY) { - WebView.super.scrollTo(scrollX, scrollY); - } - - public void super_computeScroll() { - WebView.super.computeScroll(); - } - - public boolean super_onHoverEvent(MotionEvent event) { - return WebView.super.onHoverEvent(event); - } - - public boolean super_performAccessibilityAction(int action, Bundle arguments) { - return WebView.super.performAccessibilityActionInternal(action, arguments); - } - - public boolean super_performLongClick() { - return WebView.super.performLongClick(); - } - - public boolean super_setFrame(int left, int top, int right, int bottom) { - return WebView.super.setFrame(left, top, right, bottom); - } - - public boolean super_dispatchKeyEvent(KeyEvent event) { - return WebView.super.dispatchKeyEvent(event); - } - - public boolean super_onGenericMotionEvent(MotionEvent event) { - return WebView.super.onGenericMotionEvent(event); - } - - public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) { - return WebView.super.requestFocus(direction, previouslyFocusedRect); - } - - public void super_setLayoutParams(ViewGroup.LayoutParams params) { - WebView.super.setLayoutParams(params); - } - - public void super_startActivityForResult(Intent intent, int requestCode) { - WebView.super.startActivityForResult(intent, requestCode); - } - - // ---- Access to non-public methods ---- - public void overScrollBy(int deltaX, int deltaY, - int scrollX, int scrollY, - int scrollRangeX, int scrollRangeY, - int maxOverScrollX, int maxOverScrollY, - boolean isTouchEvent) { - WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, - maxOverScrollX, maxOverScrollY, isTouchEvent); - } - - public void awakenScrollBars(int duration) { - WebView.this.awakenScrollBars(duration); - } - - public void awakenScrollBars(int duration, boolean invalidate) { - WebView.this.awakenScrollBars(duration, invalidate); - } - - public float getVerticalScrollFactor() { - return WebView.this.getVerticalScrollFactor(); - } - - public float getHorizontalScrollFactor() { - return WebView.this.getHorizontalScrollFactor(); - } - - public void setMeasuredDimension(int measuredWidth, int measuredHeight) { - WebView.this.setMeasuredDimension(measuredWidth, measuredHeight); - } - - public void onScrollChanged(int l, int t, int oldl, int oldt) { - WebView.this.onScrollChanged(l, t, oldl, oldt); - } - - public int getHorizontalScrollbarHeight() { - return WebView.this.getHorizontalScrollbarHeight(); - } - - public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - // ---- Access to (non-public) fields ---- - /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */ - public void setScrollXRaw(int scrollX) { - WebView.this.mScrollX = scrollX; - } - - /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */ - public void setScrollYRaw(int scrollY) { - WebView.this.mScrollY = scrollY; - } - - } - - //------------------------------------------------------------------------- - // Package-private internal stuff - //------------------------------------------------------------------------- - - // Only used by android.webkit.FindActionModeCallback. - void setFindDialogFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mFindDialogFindListener = listener; - } - - // Only used by android.webkit.FindActionModeCallback. - void notifyFindDialogDismissed() { - checkThread(); - mProvider.notifyFindDialogDismissed(); - } - - //------------------------------------------------------------------------- - // Private internal stuff - //------------------------------------------------------------------------- - - private WebViewProvider mProvider; - - /** - * In addition to the FindListener that the user may set via the WebView.setFindListener - * API, FindActionModeCallback will register it's own FindListener. We keep them separate - * via this class so that the two FindListeners can potentially exist at once. - */ - private class FindListenerDistributor implements FindListener { - private FindListener mFindDialogFindListener; - private FindListener mUserFindListener; - - @Override - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting) { - if (mFindDialogFindListener != null) { - mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - - if (mUserFindListener != null) { - mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - } - } - private FindListenerDistributor mFindListener; - - private void setupFindListenerIfNeeded() { - if (mFindListener == null) { - mFindListener = new FindListenerDistributor(); - mProvider.setFindListener(mFindListener); - } - } - - private void ensureProviderCreated() { - checkThread(); - if (mProvider == null) { - // As this can get called during the base class constructor chain, pass the minimum - // number of dependencies here; the rest are deferred to init(). - mProvider = getFactory().createWebView(this, new PrivateAccess()); - } - } - - private static WebViewFactoryProvider getFactory() { - return WebViewFactory.getProvider(); - } - - private final Looper mWebViewThread = Looper.myLooper(); - - private void checkThread() { - // Ignore mWebViewThread == null because this can be called during in the super class - // constructor, before this class's own constructor has even started. - if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) { - Throwable throwable = new Throwable( - "A WebView method was called on thread '" + - Thread.currentThread().getName() + "'. " + - "All WebView methods must be called on the same thread. " + - "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() + - ", FYI main Looper is " + Looper.getMainLooper() + ")"); - Log.w(LOGTAG, Log.getStackTraceString(throwable)); - StrictMode.onWebViewMethodCalledOnWrongThread(throwable); - - if (sEnforceThreadChecking) { - throw new RuntimeException(throwable); - } - } - } - - //------------------------------------------------------------------------- - // Override View methods - //------------------------------------------------------------------------- - - // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures - // there's a corresponding override (or better, caller) for each of them in here. - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mProvider.getViewDelegate().onAttachedToWindow(); - } - - /** @hide */ - @Override - protected void onDetachedFromWindowInternal() { - mProvider.getViewDelegate().onDetachedFromWindow(); - super.onDetachedFromWindowInternal(); - } - - /** @hide */ - @Override - public void onMovedToDisplay(int displayId, Configuration config) { - mProvider.getViewDelegate().onMovedToDisplay(displayId, config); - } - - @Override - public void setLayoutParams(ViewGroup.LayoutParams params) { - mProvider.getViewDelegate().setLayoutParams(params); - } - - @Override - public void setOverScrollMode(int mode) { - super.setOverScrollMode(mode); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().setOverScrollMode(mode); - } - - @Override - public void setScrollBarStyle(int style) { - mProvider.getViewDelegate().setScrollBarStyle(style); - super.setScrollBarStyle(style); - } - - @Override - protected int computeHorizontalScrollRange() { - return mProvider.getScrollDelegate().computeHorizontalScrollRange(); - } - - @Override - protected int computeHorizontalScrollOffset() { - return mProvider.getScrollDelegate().computeHorizontalScrollOffset(); - } - - @Override - protected int computeVerticalScrollRange() { - return mProvider.getScrollDelegate().computeVerticalScrollRange(); - } - - @Override - protected int computeVerticalScrollOffset() { - return mProvider.getScrollDelegate().computeVerticalScrollOffset(); - } - - @Override - protected int computeVerticalScrollExtent() { - return mProvider.getScrollDelegate().computeVerticalScrollExtent(); - } - - @Override - public void computeScroll() { - mProvider.getScrollDelegate().computeScroll(); - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - return mProvider.getViewDelegate().onHoverEvent(event); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTouchEvent(event); - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - return mProvider.getViewDelegate().onGenericMotionEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTrackballEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyUp(keyCode, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { - return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event); - } - - /* - TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not - to be delegating them too. - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyPreIme(keyCode, event); - } - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyLongPress(keyCode, event); - } - @Override - public boolean onKeyShortcut(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyShortcut(keyCode, event); - } - */ - - @Override - public AccessibilityNodeProvider getAccessibilityNodeProvider() { - AccessibilityNodeProvider provider = - mProvider.getViewDelegate().getAccessibilityNodeProvider(); - return provider == null ? super.getAccessibilityNodeProvider() : provider; - } - - @Deprecated - @Override - public boolean shouldDelayChildPressedState() { - return mProvider.getViewDelegate().shouldDelayChildPressedState(); - } - - @Override - public CharSequence getAccessibilityClassName() { - return WebView.class.getName(); - } - - @Override - public void onProvideVirtualStructure(ViewStructure structure) { - mProvider.getViewDelegate().onProvideVirtualStructure(structure); - } - - /** - * {@inheritDoc} - * - *

    The {@link ViewStructure} traditionally represents a {@link View}, while for web pages - * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is - * understood by the {@link android.service.autofill.AutofillService} implementations: - * - *

      - *
    1. Only the HTML nodes inside a {@code FORM} are generated. - *
    2. The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the - * node representing the WebView. - *
    3. If a web page has multiple {@code FORM}s, only the data for the current form is - * represented—if the user taps a field from another form, then the current autofill - * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and - * a new context is created for that {@code FORM}. - *
    4. Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in - * the view structure until the user taps a field from a {@code FORM} inside the - * {@code IFRAME}, in which case it would be treated the same way as multiple forms described - * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the - * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node. - *
    5. The W3C autofill field ({@code autocomplete} tag attribute) maps to - * {@link ViewStructure#setAutofillHints(String[])}. - *
    6. If the view is editable, the {@link ViewStructure#setAutofillType(int)} and - * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set. - *
    7. The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}. - *
    8. Other HTML attributes can be represented through - * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}. - *
    - * - *

    If the WebView implementation can determine that the value of a field was set statically - * (for example, not through Javascript), it should also call - * {@code structure.setDataIsSensitive(false)}. - * - *

    For example, an HTML form with 2 fields for username and password: - * - *

    -     *    <label>Username:</label>
    -     *    <input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username">
    -     *    <label>Password:</label>
    -     *    <input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password">
    -     * 
    - * - *

    Would map to: - * - *

    -     *     int index = structure.addChildCount(2);
    -     *     ViewStructure username = structure.newChild(index);
    -     *     username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child
    -     *     username.setAutofillHints("username");
    -     *     username.setHtmlInfo(username.newHtmlInfoBuilder("input")
    -     *         .addAttribute("type", "text")
    -     *         .addAttribute("name", "username")
    -     *         .addAttribute("label", "Username:")
    -     *         .build());
    -     *     username.setHint("Email or username");
    -     *     username.setAutofillType(View.AUTOFILL_TYPE_TEXT);
    -     *     username.setAutofillValue(AutofillValue.forText("Type your username"));
    -     *     // Value of the field is not sensitive because it was created statically and not changed.
    -     *     username.setDataIsSensitive(false);
    -     *
    -     *     ViewStructure password = structure.newChild(index + 1);
    -     *     username.setAutofillId(structure, 2); // id 2 - second child
    -     *     password.setAutofillHints("current-password");
    -     *     password.setHtmlInfo(password.newHtmlInfoBuilder("input")
    -     *         .addAttribute("type", "password")
    -     *         .addAttribute("name", "password")
    -     *         .addAttribute("label", "Password:")
    -     *         .build());
    -     *     password.setHint("Password");
    -     *     password.setAutofillType(View.AUTOFILL_TYPE_TEXT);
    -     * 
    - */ - @Override - public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { - mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags); - } - - @Override - public void autofill(SparseArrayvalues) { - mProvider.getViewDelegate().autofill(values); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfoInternal(info); - mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { - super.onInitializeAccessibilityEventInternal(event); - mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); - } - - /** @hide */ - @Override - public boolean performAccessibilityActionInternal(int action, Bundle arguments) { - return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); - } - - /** @hide */ - @Override - protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - @Override - protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { - mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY); - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mProvider.getViewDelegate().onWindowVisibilityChanged(visibility); - } - - @Override - protected void onDraw(Canvas canvas) { - mProvider.getViewDelegate().onDraw(canvas); - } - - @Override - public boolean performLongClick() { - return mProvider.getViewDelegate().performLongClick(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - mProvider.getViewDelegate().onConfigurationChanged(newConfig); - } - - /** - * Creates a new InputConnection for an InputMethod to interact with the WebView. - * This is similar to {@link View#onCreateInputConnection} but note that WebView - * calls InputConnection methods on a thread other than the UI thread. - * If these methods are overridden, then the overriding methods should respect - * thread restrictions when calling View methods or accessing data. - */ - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - return mProvider.getViewDelegate().onCreateInputConnection(outAttrs); - } - - @Override - public boolean onDragEvent(DragEvent event) { - return mProvider.getViewDelegate().onDragEvent(event); - } - - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility); - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus); - super.onWindowFocusChanged(hasWindowFocus); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect); - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - return mProvider.getViewDelegate().setFrame(left, top, right, bottom); - } - - @Override - protected void onSizeChanged(int w, int h, int ow, int oh) { - super.onSizeChanged(w, h, ow, oh); - mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh); - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return mProvider.getViewDelegate().dispatchKeyEvent(event); - } - - @Override - public boolean requestFocus(int direction, Rect previouslyFocusedRect) { - return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { - return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate); - } - - @Override - public void setBackgroundColor(int color) { - mProvider.getViewDelegate().setBackgroundColor(color); - } - - @Override - public void setLayerType(int layerType, Paint paint) { - super.setLayerType(layerType, paint); - mProvider.getViewDelegate().setLayerType(layerType, paint); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - mProvider.getViewDelegate().preDispatchDraw(canvas); - super.dispatchDraw(canvas); - } - - @Override - public void onStartTemporaryDetach() { - super.onStartTemporaryDetach(); - mProvider.getViewDelegate().onStartTemporaryDetach(); - } - - @Override - public void onFinishTemporaryDetach() { - super.onFinishTemporaryDetach(); - mProvider.getViewDelegate().onFinishTemporaryDetach(); - } - - @Override - public Handler getHandler() { - return mProvider.getViewDelegate().getHandler(super.getHandler()); - } - - @Override - public View findFocus() { - return mProvider.getViewDelegate().findFocus(super.findFocus()); - } - - /** - * If WebView has already been loaded into the current process this method will return the - * package that was used to load it. Otherwise, the package that would be used if the WebView - * was loaded right now will be returned; this does not cause WebView to be loaded, so this - * information may become outdated at any time. - * The WebView package changes either when the current WebView package is updated, disabled, or - * uninstalled. It can also be changed through a Developer Setting. - * If the WebView package changes, any app process that has loaded WebView will be killed. The - * next time the app starts and loads WebView it will use the new WebView package instead. - * @return the current WebView package, or {@code null} if there is none. - */ - @Nullable - public static PackageInfo getCurrentWebViewPackage() { - PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo(); - if (webviewPackage != null) { - return webviewPackage; - } - - IWebViewUpdateService service = WebViewFactory.getUpdateService(); - if (service == null) { - return null; - } - try { - return service.getCurrentWebViewPackage(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}. - * - * @param requestCode The integer request code originally supplied to - * startActivityForResult(), allowing you to identify who this - * result came from. - * @param resultCode The integer result code returned by the child activity - * through its setResult(). - * @param data An Intent, which can return result data to the caller - * (various data can be attached to Intent "extras"). - * @hide - */ - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data); - } - - /** @hide */ - @Override - protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { - super.encodeProperties(encoder); - - checkThread(); - encoder.addProperty("webview:contentHeight", mProvider.getContentHeight()); - encoder.addProperty("webview:contentWidth", mProvider.getContentWidth()); - encoder.addProperty("webview:scale", mProvider.getScale()); - encoder.addProperty("webview:title", mProvider.getTitle()); - encoder.addProperty("webview:url", mProvider.getUrl()); - encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl()); + return false; } } diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java index 46c39834..f5d220c0 100644 --- a/android/webkit/WebViewClient.java +++ b/android/webkit/WebViewClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,545 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.webkit; -import android.annotation.IntDef; -import android.annotation.Nullable; -import android.graphics.Bitmap; -import android.net.http.SslError; -import android.os.Message; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.ViewRootImpl; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - public class WebViewClient { - - /** - * Give the host application a chance to take over the control when a new - * url is about to be loaded in the current WebView. If WebViewClient is not - * provided, by default WebView will ask Activity Manager to choose the - * proper handler for the url. If WebViewClient is provided, return {@code true} - * means the host application handles the url, while return {@code false} means the - * current WebView handles the url. - * This method is not called for requests using the POST "method". - * - * @param view The WebView that is initiating the callback. - * @param url The url to be loaded. - * @return {@code true} if the host application wants to leave the current WebView - * and handle the url itself, otherwise return {@code false}. - * @deprecated Use {@link #shouldOverrideUrlLoading(WebView, WebResourceRequest) - * shouldOverrideUrlLoading(WebView, WebResourceRequest)} instead. - */ - @Deprecated - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return false; - } - - /** - * Give the host application a chance to take over the control when a new - * url is about to be loaded in the current WebView. If WebViewClient is not - * provided, by default WebView will ask Activity Manager to choose the - * proper handler for the url. If WebViewClient is provided, return {@code true} - * means the host application handles the url, while return {@code false} means the - * current WebView handles the url. - * - *

    Notes: - *

      - *
    • This method is not called for requests using the POST "method".
    • - *
    • 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.
    • - *
    - * - * @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. - * - *

    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. - * - *

    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. - * - *

    For more fine-grained notification of visual state updates, see {@link - * WebView#postVisualStateCallback}. - * - *

    Please note that all the conditions and recommendations applicable to - * {@link WebView#postVisualStateCallback} also apply to this API. - * - *

    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. - * - *

    Note: 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. - * - *

    Note: 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. - * - *

    Note: 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. - * - *

    Note: When Safe Browsing is enabled, these URLs still undergo Safe - * Browsing checks. If this is undesired, whitelist the URL with {@link - * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}. - * - * @param view The {@link android.webkit.WebView} that is requesting the - * resource. - * @param request Object containing the details of the request. - * @return A {@link android.webkit.WebResourceResponse} containing the - * response information or {@code null} if the WebView should load the - * resource itself. - */ - @Nullable - public WebResourceResponse shouldInterceptRequest(WebView view, - WebResourceRequest request) { - return shouldInterceptRequest(view, request.getUrl().toString()); - } - - /** - * Notify the host application that there have been an excessive number of - * HTTP redirects. As the host application if it would like to continue - * trying to load the resource. The default behavior is to send the cancel - * message. - * - * @param view The WebView that is initiating the callback. - * @param cancelMsg The message to send if the host wants to cancel - * @param continueMsg The message to send if the host wants to continue - * @deprecated This method is no longer called. When the WebView encounters - * a redirect loop, it will cancel the load. - */ - @Deprecated - public void onTooManyRedirects(WebView view, Message cancelMsg, - Message continueMsg) { - cancelMsg.sendToTarget(); - } - - // These ints must match up to the hidden values in EventHandler. - /** Generic error */ - public static final int ERROR_UNKNOWN = -1; - /** Server or proxy hostname lookup failed */ - public static final int ERROR_HOST_LOOKUP = -2; - /** Unsupported authentication scheme (not basic or digest) */ - public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3; - /** User authentication failed on server */ - public static final int ERROR_AUTHENTICATION = -4; - /** User authentication failed on proxy */ - public static final int ERROR_PROXY_AUTHENTICATION = -5; - /** Failed to connect to the server */ - public static final int ERROR_CONNECT = -6; - /** Failed to read or write to the server */ - public static final int ERROR_IO = -7; - /** Connection timed out */ - public static final int ERROR_TIMEOUT = -8; - /** Too many redirects */ - public static final int ERROR_REDIRECT_LOOP = -9; - /** Unsupported URI scheme */ - public static final int ERROR_UNSUPPORTED_SCHEME = -10; - /** Failed to perform SSL handshake */ - public static final int ERROR_FAILED_SSL_HANDSHAKE = -11; - /** Malformed URL */ - public static final int ERROR_BAD_URL = -12; - /** Generic file error */ - public static final int ERROR_FILE = -13; - /** File not found */ - public static final int ERROR_FILE_NOT_FOUND = -14; - /** Too many requests during this load */ - public static final int ERROR_TOO_MANY_REQUESTS = -15; - /** Resource load was canceled by Safe Browsing */ - public static final int ERROR_UNSAFE_RESOURCE = -16; - - /** @hide */ - @IntDef({ - SAFE_BROWSING_THREAT_UNKNOWN, - SAFE_BROWSING_THREAT_MALWARE, - SAFE_BROWSING_THREAT_PHISHING, - SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE - }) - @Retention(RetentionPolicy.SOURCE) - public @interface SafeBrowsingThreat {} - - /** The resource was blocked for an unknown reason */ - public static final int SAFE_BROWSING_THREAT_UNKNOWN = 0; - /** The resource was blocked because it contains malware */ - public static final int SAFE_BROWSING_THREAT_MALWARE = 1; - /** The resource was blocked because it contains deceptive content */ - public static final int SAFE_BROWSING_THREAT_PHISHING = 2; - /** The resource was blocked because it contains unwanted software */ - public static final int SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE = 3; - - /** - * Report an error to the host application. These errors are unrecoverable - * (i.e. the main resource is unavailable). The {@code errorCode} parameter - * corresponds to one of the {@code ERROR_*} constants. - * @param view The WebView that is initiating the callback. - * @param errorCode The error code corresponding to an ERROR_* value. - * @param description A String describing the error. - * @param failingUrl The url that failed to load. - * @deprecated Use {@link #onReceivedError(WebView, WebResourceRequest, WebResourceError) - * onReceivedError(WebView, WebResourceRequest, WebResourceError)} instead. - */ - @Deprecated - public void onReceivedError(WebView view, int errorCode, - String description, String failingUrl) { - } - - /** - * Report web resource loading error to the host application. These errors usually indicate - * inability to connect to the server. Note that unlike the deprecated version of the callback, - * the new version will be called for any resource (iframe, image, etc.), not just for the main - * page. Thus, it is recommended to perform minimum required work in this callback. - * @param view The WebView that is initiating the callback. - * @param request The originating request. - * @param error Information about the error occurred. - */ - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - onReceivedError(view, - error.getErrorCode(), error.getDescription().toString(), - request.getUrl().toString()); - } - } - - /** - * Notify the host application that an HTTP error has been received from the server while - * loading a resource. HTTP errors have status codes >= 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 - * - * AOSP Browser - * - * 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 - * must create a copy of the event. - * - * It is the responsibility of overriders of this method to call - * {@link #onUnhandledKeyEvent(WebView, KeyEvent)} - * when appropriate if they wish to continue receiving events through it. - * - * @param view The WebView that is initiating the callback. - * @param event The input event. - * @removed - */ - public void onUnhandledInputEvent(WebView view, InputEvent event) { - if (event instanceof KeyEvent) { - onUnhandledKeyEvent(view, (KeyEvent) event); - return; - } - onUnhandledInputEventInternal(view, event); - } - - private void onUnhandledInputEventInternal(WebView view, InputEvent event) { - ViewRootImpl root = view.getViewRootImpl(); - if (root != null) { - root.dispatchUnhandledInputEvent(event); - } - } - - /** - * Notify the host application that the scale applied to the WebView has - * changed. - * - * @param view The WebView that is initiating the callback. - * @param oldScale The old scale factor - * @param newScale The new scale factor - */ - public void onScaleChanged(WebView view, float oldScale, float newScale) { - } - - /** - * Notify the host application that a request to automatically log in the - * user has been processed. - * @param view The WebView requesting the login. - * @param realm The account realm used to look up accounts. - * @param account An optional account. If not {@code null}, the account should be - * checked against accounts on the device. If it is a valid - * account, it should be used to log in the user. - * @param args Authenticator specific arguments used to log in the user. - */ - public void onReceivedLoginRequest(WebView view, String realm, - @Nullable String account, String args) { - } - - /** - * Notify host application that the given WebView's render process has exited. - * - * Multiple WebView instances may be associated with a single render process; - * onRenderProcessGone will be called for each WebView that was affected. - * The application's implementation of this callback should only attempt to - * clean up the specific WebView given as a parameter, and should not assume - * that other WebView instances are affected. - * - * The given WebView can't be used, and should be removed from the view hierarchy, - * all references to it should be cleaned up, e.g any references in the Activity - * or other classes saved using {@link android.view.View#findViewById} and similar calls, etc. - * - * To cause an render process crash for test purpose, the application can - * call {@code loadUrl("chrome://crash")} on the WebView. Note that multiple WebView - * instances may be affected if they share a render process, not just the - * specific WebView which loaded chrome://crash. - * - * @param view The WebView which needs to be cleaned up. - * @param detail the reason why it exited. - * @return {@code true} if the host application handled the situation that process has - * exited, otherwise, application will crash if render process crashed, - * or be killed if render process was killed by the system. - */ - public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { - return false; - } - - /** - * Notify the host application that a loading URL has been flagged by Safe Browsing. - * - * The application must invoke the callback to indicate the preferred response. The default - * behavior is to show an interstitial to the user, with the reporting checkbox visible. - * - * If the application needs to show its own custom interstitial UI, the callback can be invoked - * asynchronously with {@link SafeBrowsingResponse#backToSafety} or {@link - * SafeBrowsingResponse#proceed}, depending on user response. - * - * @param view The WebView that hit the malicious resource. - * @param request Object containing the details of the request. - * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a - * {@code SAFE_BROWSING_THREAT_*} value. - * @param callback Applications must invoke one of the callback methods. - */ - public void onSafeBrowsingHit(WebView view, WebResourceRequest request, - @SafeBrowsingThreat int threatType, SafeBrowsingResponse callback) { - callback.showInterstitial(/* allowReporting */ true); - } } diff --git a/android/webkit/WebViewDelegate.java b/android/webkit/WebViewDelegate.java index 73399313..f0670914 100644 --- a/android/webkit/WebViewDelegate.java +++ b/android/webkit/WebViewDelegate.java @@ -218,4 +218,11 @@ public final class WebViewDelegate { throw e.rethrowFromSystemServer(); } } + + /** + * Returns the data directory suffix to use, or null for none. + */ + public String getDataDirectorySuffix() { + return WebViewFactory.getDataDirectorySuffix(); + } } diff --git a/android/webkit/WebViewFactory.java b/android/webkit/WebViewFactory.java index 797bdfb7..b3522ec9 100644 --- a/android/webkit/WebViewFactory.java +++ b/android/webkit/WebViewFactory.java @@ -33,6 +33,7 @@ import android.util.AndroidRuntimeException; import android.util.ArraySet; import android.util.Log; +import java.io.File; import java.lang.reflect.Method; /** @@ -46,7 +47,7 @@ public final class WebViewFactory { // visible for WebViewZygoteInit to look up the class by reflection and call preloadInZygote. /** @hide */ private static final String CHROMIUM_WEBVIEW_FACTORY = - "com.android.webview.chromium.WebViewChromiumFactoryProviderForOMR1"; + "com.android.webview.chromium.WebViewChromiumFactoryProviderForP"; private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"; @@ -63,6 +64,8 @@ public final class WebViewFactory { private static final Object sProviderLock = new Object(); private static PackageInfo sPackageInfo; private static Boolean sWebViewSupported; + private static boolean sWebViewDisabled; + private static String sDataDirectorySuffix; // stored here so it can be set without loading WV // Error codes for loadWebViewNativeLibraryFromPackage public static final int LIBLOAD_SUCCESS = 0; @@ -112,6 +115,45 @@ public final class WebViewFactory { return sWebViewSupported; } + /** + * @hide + */ + static void disableWebView() { + synchronized (sProviderLock) { + if (sProviderInstance != null) { + throw new IllegalStateException( + "Can't disable WebView: WebView already initialized"); + } + sWebViewDisabled = true; + } + } + + /** + * @hide + */ + static void setDataDirectorySuffix(String suffix) { + synchronized (sProviderLock) { + if (sProviderInstance != null) { + throw new IllegalStateException( + "Can't set data directory suffix: WebView already initialized"); + } + if (suffix.indexOf(File.separatorChar) >= 0) { + throw new IllegalArgumentException("Suffix " + suffix + + " contains a path separator"); + } + sDataDirectorySuffix = suffix; + } + } + + /** + * @hide + */ + static String getDataDirectorySuffix() { + synchronized (sProviderLock) { + return sDataDirectorySuffix; + } + } + /** * @hide */ @@ -204,6 +246,11 @@ public final class WebViewFactory { throw new UnsupportedOperationException(); } + if (sWebViewDisabled) { + throw new IllegalStateException( + "WebView.disableWebView() was called: WebView is disabled"); + } + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()"); try { @@ -265,10 +312,10 @@ public final class WebViewFactory { + "packageName mismatch, expected: " + chosen.packageName + " actual: " + toUse.packageName); } - if (chosen.versionCode > toUse.versionCode) { + if (chosen.getLongVersionCode() > toUse.getLongVersionCode()) { throw new MissingWebViewPackageException("Failed to verify WebView provider, " - + "version code is lower than expected: " + chosen.versionCode - + " actual: " + toUse.versionCode); + + "version code is lower than expected: " + chosen.getLongVersionCode() + + " actual: " + toUse.getLongVersionCode()); } if (getWebViewLibrary(toUse.applicationInfo) == null) { throw new MissingWebViewPackageException("Tried to load an invalid WebView provider: " @@ -401,7 +448,7 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " + - sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")"); + sPackageInfo.versionName + " (code " + sPackageInfo.getLongVersionCode() + ")"); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()"); try { diff --git a/android/webkit/WebViewFactoryProvider.java b/android/webkit/WebViewFactoryProvider.java index 4c47abc6..3ced6a5f 100644 --- a/android/webkit/WebViewFactoryProvider.java +++ b/android/webkit/WebViewFactoryProvider.java @@ -133,6 +133,14 @@ public interface WebViewFactoryProvider { */ TokenBindingService getTokenBindingService(); + /** + * Gets the TracingController instance for this WebView implementation. The + * implementation must return the same instance on subsequent calls. + * + * @return the TracingController instance + */ + TracingController getTracingController(); + /** * Gets the ServiceWorkerController instance for this WebView implementation. The * implementation must return the same instance on subsequent calls. diff --git a/android/webkit/WebViewLibraryLoader.java b/android/webkit/WebViewLibraryLoader.java index de0b97d1..eb2b6bcc 100644 --- a/android/webkit/WebViewLibraryLoader.java +++ b/android/webkit/WebViewLibraryLoader.java @@ -123,10 +123,11 @@ public class WebViewLibraryLoader { throw new IllegalArgumentException( "Native library paths to the WebView RelRo process must not be null!"); } - int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess( - RelroFileCreator.class.getName(), new String[] { nativeLib.path }, - "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler); - if (pid <= 0) throw new Exception("Failed to start the relro file creator process"); + boolean success = LocalServices.getService(ActivityManagerInternal.class) + .startIsolatedProcess( + RelroFileCreator.class.getName(), new String[] { nativeLib.path }, + "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler); + if (!success) throw new Exception("Failed to start the relro file creator process"); } catch (Throwable t) { // Log and discard errors as we must not crash the system server. Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t); diff --git a/android/widget/DatePicker.java b/android/widget/DatePicker.java index dfb36423..b2b93faf 100644 --- a/android/widget/DatePicker.java +++ b/android/widget/DatePicker.java @@ -107,7 +107,10 @@ public class DatePicker extends FrameLayout { public static final int MODE_CALENDAR = 2; /** @hide */ - @IntDef({MODE_SPINNER, MODE_CALENDAR}) + @IntDef(prefix = { "MODE_" }, value = { + MODE_SPINNER, + MODE_CALENDAR + }) @Retention(RetentionPolicy.SOURCE) public @interface DatePickerMode {} diff --git a/android/widget/EditText.java b/android/widget/EditText.java index 56c3e4a5..336c20cd 100644 --- a/android/widget/EditText.java +++ b/android/widget/EditText.java @@ -105,6 +105,11 @@ public class EditText extends TextView { @Override public Editable getText() { + CharSequence text = super.getText(); + if (text instanceof Editable) { + return (Editable) super.getText(); + } + super.setText(text, BufferType.EDITABLE); return (Editable) super.getText(); } diff --git a/android/widget/Editor.java b/android/widget/Editor.java index 05cba1e5..05d18d18 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -41,7 +41,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.metrics.LogMaker; import android.os.Bundle; import android.os.LocaleList; import android.os.Parcel; @@ -108,6 +107,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextLinks; import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView.Drawables; import android.widget.TextView.OnEditorActionListener; @@ -175,6 +175,13 @@ public class Editor { int SELECTION_END = 2; } + @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK}) + @interface TextActionMode { + int SELECTION = 0; + int INSERTION = 1; + int TEXT_LINK = 2; + } + // Each Editor manages its own undo stack. private final UndoManager mUndoManager = new UndoManager(); private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); @@ -545,7 +552,8 @@ public class Editor { chooseSize(mErrorPopup, mError, tv); tv.setText(mError); - mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY()); + mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(), + Gravity.TOP | Gravity.LEFT); mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor()); } @@ -2053,7 +2061,7 @@ public class Editor { stopTextActionMode(); ActionMode.Callback actionModeCallback = - new TextActionModeCallback(false /* hasSelection */); + new TextActionModeCallback(TextActionMode.INSERTION); mTextActionMode = mTextView.startActionMode( actionModeCallback, ActionMode.TYPE_FLOATING); if (mTextActionMode != null && getInsertionController() != null) { @@ -2079,7 +2087,23 @@ public class Editor { * Asynchronously starts a selection action mode using the TextClassifier. */ void startSelectionActionModeAsync(boolean adjustSelection) { - getSelectionActionModeHelper().startActionModeAsync(adjustSelection); + getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection); + } + + void startLinkActionModeAsync(TextLinks.TextLink link) { + Preconditions.checkNotNull(link); + if (!(mTextView.getText() instanceof Spannable)) { + return; + } + Spannable text = (Spannable) mTextView.getText(); + stopTextActionMode(); + if (mTextView.isTextSelectable()) { + Selection.setSelection((Spannable) text, link.getStart(), link.getEnd()); + } else { + //TODO: Nonselectable text + } + + getSelectionActionModeHelper().startLinkActionModeAsync(link); } /** @@ -2145,7 +2169,7 @@ public class Editor { return true; } - boolean startSelectionActionModeInternal() { + boolean startActionModeInternal(@TextActionMode int actionMode) { if (extractedTextModeWillBeStarted()) { return false; } @@ -2159,8 +2183,7 @@ public class Editor { return false; } - ActionMode.Callback actionModeCallback = - new TextActionModeCallback(true /* hasSelection */); + ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); final boolean selectionStarted = mTextActionMode != null; @@ -3828,8 +3851,9 @@ public class Editor { private final int mHandleHeight; private final Map mAssistClickHandlers = new HashMap<>(); - public TextActionModeCallback(boolean hasSelection) { - mHasSelection = hasSelection; + TextActionModeCallback(@TextActionMode int mode) { + mHasSelection = mode == TextActionMode.SELECTION + || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK); if (mHasSelection) { SelectionModifierCursorController selectionController = getSelectionController(); if (selectionController.mStartHandle == null) { @@ -3982,31 +4006,39 @@ public class Editor { } final TextClassification textClassification = getSelectionActionModeHelper().getTextClassification(); - final int count = textClassification != null ? textClassification.getActionCount() : 0; + if (textClassification == null) { + return; + } + if (isValidAssistMenuItem( + textClassification.getIcon(), + textClassification.getLabel(), + textClassification.getOnClickListener(), + textClassification.getIntent())) { + final MenuItem item = menu.add( + TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, + textClassification.getLabel()) + .setIcon(textClassification.getIcon()) + .setIntent(textClassification.getIntent()); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + mAssistClickHandlers.put(item, textClassification.getOnClickListener()); + } + final int count = textClassification.getSecondaryActionsCount(); for (int i = 0; i < count; i++) { - if (!isValidAssistMenuItem(i)) { + if (!isValidAssistMenuItem( + textClassification.getSecondaryIcon(i), + textClassification.getSecondaryLabel(i), + textClassification.getSecondaryOnClickListener(i), + textClassification.getSecondaryIntent(i))) { continue; } - final int groupId = TextView.ID_ASSIST; - final int order = (i == 0) - ? MENU_ITEM_ORDER_ASSIST - : MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i; - final int id = (i == 0) ? TextView.ID_ASSIST : Menu.NONE; - final int showAsFlag = (i == 0) - ? MenuItem.SHOW_AS_ACTION_ALWAYS - : MenuItem.SHOW_AS_ACTION_NEVER; + final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i; final MenuItem item = menu.add( - groupId, id, order, textClassification.getLabel(i)) - .setIcon(textClassification.getIcon(i)) - .setIntent(textClassification.getIntent(i)); - item.setShowAsAction(showAsFlag); - mAssistClickHandlers.put(item, textClassification.getOnClickListener(i)); - if (id == TextView.ID_ASSIST) { - mMetricsLogger.write( - new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST) - .setType(MetricsEvent.TYPE_OPEN) - .setSubtype(textClassification.getLogType())); - } + TextView.ID_ASSIST, Menu.NONE, order, + textClassification.getSecondaryLabel(i)) + .setIcon(textClassification.getSecondaryIcon(i)) + .setIntent(textClassification.getSecondaryIntent(i)); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i)); } } @@ -4022,18 +4054,9 @@ public class Editor { } } - private boolean isValidAssistMenuItem(int index) { - final TextClassification textClassification = - getSelectionActionModeHelper().getTextClassification(); - if (!mTextView.isDeviceProvisioned() || textClassification == null - || index < 0 || index >= textClassification.getActionCount()) { - return false; - } - final Drawable icon = textClassification.getIcon(index); - final CharSequence label = textClassification.getLabel(index); + private boolean isValidAssistMenuItem( + Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) { final boolean hasUi = icon != null || !TextUtils.isEmpty(label); - final OnClickListener onClick = textClassification.getOnClickListener(index); - final Intent intent = textClassification.getIntent(index); final boolean hasAction = onClick != null || isSupportedIntent(intent); return hasUi && hasAction; } @@ -4079,11 +4102,6 @@ public class Editor { if (onClickListener != null) { onClickListener.onClick(mTextView); stopTextActionMode(); - if (assistMenuItem.getItemId() == TextView.ID_ASSIST) { - mMetricsLogger.action( - MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST, - textClassification.getLogType()); - } } // We tried our best. return true; @@ -4930,7 +4948,10 @@ public class Editor { } @Retention(RetentionPolicy.SOURCE) - @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END}) + @IntDef(prefix = { "HANDLE_TYPE_" }, value = { + HANDLE_TYPE_SELECTION_START, + HANDLE_TYPE_SELECTION_END + }) public @interface HandleType {} public static final int HANDLE_TYPE_SELECTION_START = 0; public static final int HANDLE_TYPE_SELECTION_END = 1; @@ -6135,7 +6156,11 @@ public class Editor { } @Retention(RetentionPolicy.SOURCE) - @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL}) + @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = { + MERGE_EDIT_MODE_FORCE_MERGE, + MERGE_EDIT_MODE_NEVER_MERGE, + MERGE_EDIT_MODE_NORMAL + }) private @interface MergeMode {} private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0; private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1; @@ -6594,7 +6619,7 @@ public class Editor { Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i, getLabel(resolveInfo)) .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } } diff --git a/android/widget/GridLayout.java b/android/widget/GridLayout.java index cbd1e0ad..012b918f 100644 --- a/android/widget/GridLayout.java +++ b/android/widget/GridLayout.java @@ -172,7 +172,10 @@ public class GridLayout extends ViewGroup { // Public constants /** @hide */ - @IntDef({HORIZONTAL, VERTICAL}) + @IntDef(prefix = { "HORIZONTAL", "VERTICAL" }, value = { + HORIZONTAL, + VERTICAL + }) @Retention(RetentionPolicy.SOURCE) public @interface Orientation {} @@ -198,7 +201,10 @@ public class GridLayout extends ViewGroup { public static final int UNDEFINED = Integer.MIN_VALUE; /** @hide */ - @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS}) + @IntDef(prefix = { "ALIGN_" }, value = { + ALIGN_BOUNDS, + ALIGN_MARGINS + }) @Retention(RetentionPolicy.SOURCE) public @interface AlignmentMode {} diff --git a/android/widget/GridView.java b/android/widget/GridView.java index fcb44af6..1ec9b2f0 100644 --- a/android/widget/GridView.java +++ b/android/widget/GridView.java @@ -65,7 +65,12 @@ import java.lang.annotation.RetentionPolicy; @RemoteView public class GridView extends AbsListView { /** @hide */ - @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) + @IntDef(prefix = { "NO_STRETCH", "STRETCH_" }, value = { + NO_STRETCH, + STRETCH_SPACING, + STRETCH_COLUMN_WIDTH, + STRETCH_SPACING_UNIFORM + }) @Retention(RetentionPolicy.SOURCE) public @interface StretchMode {} diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java index 380bf7ad..7ea1f1ed 100644 --- a/android/widget/LinearLayout.java +++ b/android/widget/LinearLayout.java @@ -95,13 +95,12 @@ public class LinearLayout extends ViewGroup { public static final int VERTICAL = 1; /** @hide */ - @IntDef(flag = true, - value = { - SHOW_DIVIDER_NONE, - SHOW_DIVIDER_BEGINNING, - SHOW_DIVIDER_MIDDLE, - SHOW_DIVIDER_END - }) + @IntDef(flag = true, prefix = { "SHOW_DIVIDER_" }, value = { + SHOW_DIVIDER_NONE, + SHOW_DIVIDER_BEGINNING, + SHOW_DIVIDER_MIDDLE, + SHOW_DIVIDER_END + }) @Retention(RetentionPolicy.SOURCE) public @interface DividerMode {} diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java index bd48f455..26dfcc2d 100644 --- a/android/widget/Magnifier.java +++ b/android/widget/Magnifier.java @@ -125,7 +125,7 @@ public final class Magnifier { mView.getWidth() - mBitmap.getWidth())); final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2; - if (startX != mPrevStartCoordsInSurface.x || startY != mPrevStartCoordsInSurface.y) { + if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) { performPixelCopy(startX, startY); mPrevPosInView.x = xPosInView; diff --git a/android/widget/NumberPicker.java b/android/widget/NumberPicker.java index b3792806..d98b865d 100644 --- a/android/widget/NumberPicker.java +++ b/android/widget/NumberPicker.java @@ -510,7 +510,11 @@ public class NumberPicker extends LinearLayout { */ public interface OnScrollListener { /** @hide */ - @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING}) + @IntDef(prefix = { "SCROLL_STATE_" }, value = { + SCROLL_STATE_IDLE, + SCROLL_STATE_TOUCH_SCROLL, + SCROLL_STATE_FLING + }) @Retention(RetentionPolicy.SOURCE) public @interface ScrollState {} @@ -1952,8 +1956,7 @@ public class NumberPicker extends LinearLayout { CharSequence beforeText = mInputText.getText(); if (!text.equals(beforeText.toString())) { mInputText.setText(text); - if (AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); mInputText.onInitializeAccessibilityEvent(event); @@ -2613,7 +2616,7 @@ public class NumberPicker extends LinearLayout { } private void sendAccessibilityEventForVirtualText(int eventType) { - if (AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); mInputText.onInitializeAccessibilityEvent(event); mInputText.onPopulateAccessibilityEvent(event); @@ -2624,7 +2627,7 @@ public class NumberPicker extends LinearLayout { private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, String text) { - if (AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); event.setClassName(Button.class.getName()); event.setPackageName(mContext.getPackageName()); diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index d0ad27af..2c6466cd 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -35,6 +35,7 @@ import android.util.Log; import android.view.ActionMode; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; import android.view.textclassifier.logging.SmartSelectionEventTracker; import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent; @@ -97,7 +98,10 @@ public final class SelectionActionModeHelper { } } - public void startActionModeAsync(boolean adjustSelection) { + /** + * Starts Selection ActionMode. + */ + public void startSelectionActionModeAsync(boolean adjustSelection) { // Check if the smart selection should run for editable text. adjustSelection &= !mTextView.isTextEditable() || mTextView.getTextClassifier().getSettings() @@ -109,7 +113,7 @@ public final class SelectionActionModeHelper { mTextView.getSelectionEnd()); cancelAsyncTask(); if (skipTextClassification()) { - startActionMode(null); + startSelectionActionMode(null); } else { resetTextClassificationHelper(); mTextClassificationAsyncTask = new TextClassificationAsyncTask( @@ -119,8 +123,27 @@ public final class SelectionActionModeHelper { ? mTextClassificationHelper::suggestSelection : mTextClassificationHelper::classifyText, mSmartSelectSprite != null - ? this::startActionModeWithSmartSelectAnimation - : this::startActionMode) + ? this::startSelectionActionModeWithSmartSelectAnimation + : this::startSelectionActionMode) + .execute(); + } + } + + /** + * Starts Link ActionMode. + */ + public void startLinkActionModeAsync(TextLinks.TextLink textLink) { + //TODO: tracking/logging + cancelAsyncTask(); + if (skipTextClassification()) { + startLinkActionMode(null); + } else { + resetTextClassificationHelper(textLink.getStart(), textLink.getEnd()); + mTextClassificationAsyncTask = new TextClassificationAsyncTask( + mTextView, + mTextClassificationHelper.getTimeoutDuration(), + mTextClassificationHelper::classifyText, + this::startLinkActionMode) .execute(); } } @@ -200,9 +223,19 @@ public final class SelectionActionModeHelper { return noOpTextClassifier || noSelection || password; } - private void startActionMode(@Nullable SelectionResult result) { + private void startLinkActionMode(@Nullable SelectionResult result) { + startActionMode(Editor.TextActionMode.TEXT_LINK, result); + } + + private void startSelectionActionMode(@Nullable SelectionResult result) { + startActionMode(Editor.TextActionMode.SELECTION, result); + } + + private void startActionMode( + @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) { final CharSequence text = getText(mTextView); - if (result != null && text instanceof Spannable) { + if (result != null && text instanceof Spannable + && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { // Do not change the selection if TextClassifier should be dark launched. if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); @@ -211,12 +244,13 @@ public final class SelectionActionModeHelper { } else { mTextClassification = null; } - if (mEditor.startSelectionActionModeInternal()) { + if (mEditor.startActionModeInternal(actionMode)) { final SelectionModifierCursorController controller = mEditor.getSelectionController(); - if (controller != null) { + if (controller != null + && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { controller.show(); } - if (result != null) { + if (result != null && actionMode == Editor.TextActionMode.SELECTION) { mSelectionTracker.onSmartSelection(result); } } @@ -224,10 +258,11 @@ public final class SelectionActionModeHelper { mTextClassificationAsyncTask = null; } - private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) { + private void startSelectionActionModeWithSmartSelectAnimation( + @Nullable SelectionResult result) { final Layout layout = mTextView.getLayout(); - final Runnable onAnimationEndCallback = () -> startActionMode(result); + final Runnable onAnimationEndCallback = () -> startSelectionActionMode(result); // TODO do not trigger the animation if the change included only non-printable characters final boolean didSelectionChange = result != null && (mTextView.getSelectionStart() != result.mStart @@ -386,15 +421,24 @@ public final class SelectionActionModeHelper { mTextClassificationAsyncTask = null; } - private void resetTextClassificationHelper() { + private void resetTextClassificationHelper(int selectionStart, int selectionEnd) { + if (selectionStart < 0 || selectionEnd < 0) { + // Use selection indices + selectionStart = mTextView.getSelectionStart(); + selectionEnd = mTextView.getSelectionEnd(); + } mTextClassificationHelper.init( mTextView.getContext(), mTextView.getTextClassifier(), getText(mTextView), - mTextView.getSelectionStart(), mTextView.getSelectionEnd(), + selectionStart, selectionEnd, mTextView.getTextLocales()); } + private void resetTextClassificationHelper() { + resetTextClassificationHelper(-1, -1); + } + private void cancelSmartSelectAnimation() { if (mSmartSelectSprite != null) { mSmartSelectSprite.cancelAnimation(); diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java index 12790403..53318c99 100644 --- a/android/widget/TextClock.java +++ b/android/widget/TextClock.java @@ -20,6 +20,7 @@ import static android.view.ViewDebug.ExportedProperty; import static android.widget.RemoteViews.RemoteView; import android.annotation.NonNull; +import android.annotation.TestApi; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -141,6 +142,9 @@ public class TextClock extends TextView { private boolean mShowCurrentUserTime; private ContentObserver mFormatChangeObserver; + // Used by tests to stop time change events from triggering the text update + private boolean mStopTicking; + private class FormatChangeObserver extends ContentObserver { public FormatChangeObserver(Handler handler) { @@ -163,6 +167,9 @@ public class TextClock extends TextView { private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + if (mStopTicking) { + return; // Test disabled the clock ticks + } if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { final String timeZone = intent.getStringExtra("time-zone"); createTime(timeZone); @@ -173,6 +180,9 @@ public class TextClock extends TextView { private final Runnable mTicker = new Runnable() { public void run() { + if (mStopTicking) { + return; // Test disabled the clock ticks + } onTimeChanged(); long now = SystemClock.uptimeMillis(); @@ -546,6 +556,15 @@ public class TextClock extends TextView { } } + /** + * Used by tests to stop the clock tick from updating the text. + * @hide + */ + @TestApi + public void disableClockTick() { + mStopTicking = true; + } + private void registerReceiver() { final IntentFilter filter = new IntentFilter(); @@ -570,11 +589,12 @@ public class TextClock extends TextView { mFormatChangeObserver = new FormatChangeObserver(getHandler()); } final ContentResolver resolver = getContext().getContentResolver(); + Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24); if (mShowCurrentUserTime) { - resolver.registerContentObserver(Settings.System.CONTENT_URI, true, + resolver.registerContentObserver(uri, true, mFormatChangeObserver, UserHandle.USER_ALL); } else { - resolver.registerContentObserver(Settings.System.CONTENT_URI, true, + resolver.registerContentObserver(uri, true, mFormatChangeObserver); } } diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 71532a72..1e17f34a 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -77,6 +77,7 @@ import android.text.InputFilter; import android.text.InputType; import android.text.Layout; import android.text.ParcelableSpan; +import android.text.PremeasuredText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -159,6 +160,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; import android.widget.RemoteViews.RemoteView; @@ -167,6 +169,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FastMath; +import com.android.internal.util.Preconditions; import com.android.internal.widget.EditableInputConnection; import libcore.util.EmptyArray; @@ -750,7 +753,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; /** @hide */ - @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM}) + @IntDef(prefix = { "AUTO_SIZE_TEXT_TYPE_" }, value = { + AUTO_SIZE_TEXT_TYPE_NONE, + AUTO_SIZE_TEXT_TYPE_UNIFORM + }) @Retention(RetentionPolicy.SOURCE) public @interface AutoSizeTextType {} // Default minimum size for auto-sizing text in scaled pixels. @@ -4861,6 +4867,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Sets line spacing for this TextView. Each line other than the last line will have its height * multiplied by {@code mult} and have {@code add} added to it. * + * @param add The value in pixels that should be added to each line other than the last line. + * This will be applied after the multiplier + * @param mult The value by which each line height other than the last line will be multiplied + * by * * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier @@ -5326,7 +5336,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof CharWrapper)) { + } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -5610,10 +5620,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener spannable = (Spannable) text; } else { spannable = mSpannableFactory.newSpannable(text); - text = spannable; } SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); + if (spans.length == 0) { + return text; + } else { + text = spannable; + } + for (int i = 0; i < spans.length; i++) { spannable.removeSpan(spans[i]); } @@ -10836,10 +10851,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount) { - if (!AccessibilityManager.getInstance(mContext).isObservedEventType( - AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)) { - return; - } AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); event.setFromIndex(fromIndex); @@ -11145,6 +11156,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mTextClassifier; } + /** + * Starts an ActionMode for the specified TextLink. + * + * @return Whether or not we're attempting to start the action mode. + * @hide + */ + public boolean requestActionMode(@NonNull TextLinks.TextLink link) { + Preconditions.checkNotNull(link); + if (mEditor != null) { + mEditor.startLinkActionModeAsync(link); + return true; + } + return false; + } /** * @hide */ diff --git a/android/widget/TextViewMetrics.java b/android/widget/TextViewMetrics.java index 96d17943..738a5742 100644 --- a/android/widget/TextViewMetrics.java +++ b/android/widget/TextViewMetrics.java @@ -37,29 +37,4 @@ public final class TextViewMetrics { * Long press on TextView - drag and drop started. */ public static final int SUBTYPE_LONG_PRESS_DRAG_AND_DROP = 2; - - /** - * Assist menu item (shown or clicked) - classification: other. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_OTHER = 0; - - /** - * Assist menu item (shown or clicked) - classification: email. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_EMAIL = 1; - - /** - * Assist menu item (shown or clicked) - classification: phone. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_PHONE = 2; - - /** - * Assist menu item (shown or clicked) - classification: address. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_ADDRESS = 3; - - /** - * Assist menu item (shown or clicked) - classification: url. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_URL = 4; } diff --git a/android/widget/TimePicker.java b/android/widget/TimePicker.java index ae6881e4..cfec3f2f 100644 --- a/android/widget/TimePicker.java +++ b/android/widget/TimePicker.java @@ -77,7 +77,10 @@ public class TimePicker extends FrameLayout { public static final int MODE_CLOCK = 2; /** @hide */ - @IntDef({MODE_SPINNER, MODE_CLOCK}) + @IntDef(prefix = { "MODE_" }, value = { + MODE_SPINNER, + MODE_CLOCK + }) @Retention(RetentionPolicy.SOURCE) public @interface TimePickerMode {} diff --git a/android/widget/Toast.java b/android/widget/Toast.java index bfde6ac3..edcf209b 100644 --- a/android/widget/Toast.java +++ b/android/widget/Toast.java @@ -71,7 +71,10 @@ public class Toast { static final boolean localLOGV = false; /** @hide */ - @IntDef({LENGTH_SHORT, LENGTH_LONG}) + @IntDef(prefix = { "LENGTH_" }, value = { + LENGTH_SHORT, + LENGTH_LONG + }) @Retention(RetentionPolicy.SOURCE) public @interface Duration {} @@ -504,8 +507,7 @@ public class Toast { private void trySendAccessibilityEvent() { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mView.getContext()); - if (!accessibilityManager.isObservedEventType( - AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)) { + if (!accessibilityManager.isEnabled()) { return; } // treat toasts as notifications since they are used to -- cgit v1.2.3