From 9158825f9c41869689d6b1786d7c7aa8bdd524ce Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Fri, 22 Nov 2013 08:25:26 -0800 Subject: Move some system services to separate directories Refactored the directory structure so that services can be optionally excluded. This is step 1. Will be followed by another change that makes it possible to remove services from the build. Change-Id: Ideacedfd34b5e213217ad3ff4ebb21c4a8e73f85 --- services/Android.mk | 45 + .../accessibility/AccessibilityInputFilter.java | 404 + .../accessibility/AccessibilityManagerService.java | 3131 ++++ .../accessibility/EventStreamTransformation.java | 93 + .../android/server/accessibility/GestureUtils.java | 102 + .../server/accessibility/ScreenMagnifier.java | 1177 ++ .../server/accessibility/TouchExplorer.java | 1937 +++ services/accessibility/java/service.mk | 11 + .../android/server/appwidget/AppWidgetService.java | 363 + .../server/appwidget/AppWidgetServiceImpl.java | 2126 +++ services/appwidget/java/service.mk | 11 + .../server/backup/BackupManagerService.java | 6196 +++++++ .../server/backup/PackageManagerBackupAgent.java | 425 + .../android/server/backup/SystemBackupAgent.java | 185 + services/backup/java/service.mk | 11 + services/common_time/Android.mk | 36 - services/common_time/clock_recovery.cpp | 423 - services/common_time/clock_recovery.h | 159 - services/common_time/common_clock.cpp | 150 - services/common_time/common_clock.h | 57 - services/common_time/common_clock_service.cpp | 157 - services/common_time/common_clock_service.h | 91 - .../common_time/common_time_config_service.cpp | 112 - services/common_time/common_time_config_service.h | 60 - services/common_time/common_time_server.cpp | 1506 -- services/common_time/common_time_server.h | 324 - services/common_time/common_time_server_api.cpp | 438 - .../common_time/common_time_server_packets.cpp | 293 - services/common_time/common_time_server_packets.h | 189 - services/common_time/diag_thread.cpp | 323 - services/common_time/diag_thread.h | 76 - services/common_time/main.cpp | 43 - services/common_time/utils.cpp | 164 - services/common_time/utils.h | 83 - .../com/android/server/AlarmManagerService.java | 1515 ++ .../java/com/android/server/AppOpsService.java | 1083 ++ .../java/com/android/server/AssetAtlasService.java | 735 + .../java/com/android/server/AttributeCache.java | 143 + .../java/com/android/server/BatteryService.java | 753 + .../android/server/BluetoothManagerService.java | 1262 ++ .../core/java/com/android/server/BootReceiver.java | 241 + .../java/com/android/server/BrickReceiver.java | 31 + .../java/com/android/server/CertBlacklister.java | 142 + .../server/CommonTimeManagementService.java | 376 + .../com/android/server/ConnectivityService.java | 4930 ++++++ .../java/com/android/server/ConsumerIrService.java | 135 + .../com/android/server/CountryDetectorService.java | 227 + .../java/com/android/server/DiskStatsService.java | 113 + .../core/java/com/android/server/DockObserver.java | 206 + .../com/android/server/DropBoxManagerService.java | 781 + .../core/java/com/android/server/EntropyMixer.java | 208 + .../java/com/android/server/EventLogTags.logtags | 188 + .../core/java/com/android/server/FgThread.java | 65 + .../server/INativeDaemonConnectorCallbacks.java | 24 + .../com/android/server/IdleMaintenanceService.java | 327 + .../android/server/InputMethodManagerService.java | 3574 ++++ .../java/com/android/server/IntentResolver.java | 660 + .../core/java/com/android/server/IoThread.java | 62 + .../java/com/android/server/LocalServices.java | 58 + .../com/android/server/LocationManagerService.java | 2411 +++ .../com/android/server/LockSettingsService.java | 530 + .../com/android/server/MasterClearReceiver.java | 55 + .../core/java/com/android/server/MountService.java | 2833 ++++ .../com/android/server/NativeDaemonConnector.java | 598 + .../server/NativeDaemonConnectorException.java | 58 + .../java/com/android/server/NativeDaemonEvent.java | 254 + .../android/server/NetworkManagementService.java | 1810 ++ .../android/server/NetworkTimeUpdateService.java | 309 + .../core/java/com/android/server/NsdService.java | 849 + .../core/java/com/android/server/RandomBlock.java | 101 + .../android/server/RecognitionManagerService.java | 167 + .../android/server/SamplingProfilerService.java | 121 + .../java/com/android/server/SerialService.java | 63 + .../java/com/android/server/ServiceWatcher.java | 382 + .../java/com/android/server/ShutdownActivity.java | 69 + .../core/java/com/android/server/SystemServer.java | 1159 ++ .../java/com/android/server/SystemService.java | 156 + .../com/android/server/SystemServiceManager.java | 163 + .../java/com/android/server/TelephonyRegistry.java | 797 + .../android/server/TextServicesManagerService.java | 983 ++ .../com/android/server/TwilightCalculator.java | 123 + .../com/android/server/UiModeManagerService.java | 618 + .../core/java/com/android/server/UiThread.java | 71 + .../java/com/android/server/UpdateLockService.java | 126 + .../java/com/android/server/VibratorService.java | 613 + .../core/java/com/android/server/Watchdog.java | 483 + .../com/android/server/WiredAccessoryManager.java | 433 + .../server/accounts/AccountAuthenticatorCache.java | 94 + .../server/accounts/AccountManagerService.java | 3089 ++++ .../accounts/IAccountAuthenticatorCache.java | 67 + .../java/com/android/server/am/ActiveServices.java | 2663 +++ .../android/server/am/ActivityManagerService.java | 16548 +++++++++++++++++++ .../java/com/android/server/am/ActivityRecord.java | 1068 ++ .../java/com/android/server/am/ActivityResult.java | 34 + .../java/com/android/server/am/ActivityStack.java | 3615 ++++ .../android/server/am/ActivityStackSupervisor.java | 2974 ++++ .../java/com/android/server/am/AppBindRecord.java | 63 + .../java/com/android/server/am/AppErrorDialog.java | 102 + .../java/com/android/server/am/AppErrorResult.java | 42 + .../android/server/am/AppNotRespondingDialog.java | 143 + .../server/am/AppWaitingForDebuggerDialog.java | 74 + .../java/com/android/server/am/BackupRecord.java | 58 + .../com/android/server/am/BaseErrorDialog.java | 82 + .../com/android/server/am/BatteryStatsService.java | 579 + .../com/android/server/am/BroadcastFilter.java | 74 + .../java/com/android/server/am/BroadcastQueue.java | 1219 ++ .../com/android/server/am/BroadcastRecord.java | 210 + .../com/android/server/am/CompatModeDialog.java | 90 + .../com/android/server/am/CompatModePackages.java | 385 + .../com/android/server/am/ConnectionRecord.java | 110 + .../server/am/ContentProviderConnection.java | 94 + .../android/server/am/ContentProviderRecord.java | 252 + .../android/server/am/CoreSettingsObserver.java | 110 + .../com/android/server/am/EventLogTags.logtags | 91 + .../com/android/server/am/FactoryErrorDialog.java | 47 + .../com/android/server/am/IntentBindRecord.java | 112 + .../com/android/server/am/LaunchWarningWindow.java | 56 + .../com/android/server/am/NativeCrashListener.java | 285 + .../com/android/server/am/PendingIntentRecord.java | 364 + .../android/server/am/PendingThumbnailsRecord.java | 39 + .../java/com/android/server/am/ProcessList.java | 525 + .../java/com/android/server/am/ProcessMemInfo.java | 37 + .../java/com/android/server/am/ProcessRecord.java | 644 + .../com/android/server/am/ProcessStatsService.java | 909 + .../java/com/android/server/am/ProviderMap.java | 415 + .../java/com/android/server/am/ReceiverList.java | 116 + .../java/com/android/server/am/ServiceRecord.java | 544 + .../server/am/StrictModeViolationDialog.java | 100 + .../java/com/android/server/am/TaskAccessInfo.java | 34 + .../java/com/android/server/am/TaskRecord.java | 527 + .../com/android/server/am/ThumbnailHolder.java | 29 + .../java/com/android/server/am/UriPermission.java | 357 + .../com/android/server/am/UriPermissionOwner.java | 150 + .../com/android/server/am/UsageStatsService.java | 1173 ++ .../com/android/server/am/UserStartedState.java | 60 + .../core/java/com/android/server/am/package.html | 5 + .../android/server/clipboard/ClipboardService.java | 366 + .../server/connectivity/DataConnectionStats.java | 154 + .../android/server/connectivity/Nat464Xlat.java | 200 + .../android/server/connectivity/PacManager.java | 381 + .../com/android/server/connectivity/Tethering.java | 1590 ++ .../java/com/android/server/connectivity/Vpn.java | 1205 ++ .../com/android/server/content/ContentService.java | 975 ++ .../com/android/server/content/SyncManager.java | 2940 ++++ .../com/android/server/content/SyncOperation.java | 294 + .../java/com/android/server/content/SyncQueue.java | 230 + .../android/server/content/SyncStorageEngine.java | 2638 +++ .../com/android/server/display/DisplayAdapter.java | 129 + .../com/android/server/display/DisplayDevice.java | 213 + .../android/server/display/DisplayDeviceInfo.java | 302 + .../server/display/DisplayManagerService.java | 1312 ++ .../server/display/DisplayTransactionListener.java | 26 + .../android/server/display/DisplayViewport.java | 75 + .../server/display/LocalDisplayAdapter.java | 211 + .../com/android/server/display/LogicalDisplay.java | 337 + .../server/display/OverlayDisplayAdapter.java | 359 + .../server/display/OverlayDisplayWindow.java | 376 + .../server/display/PersistentDataStore.java | 287 + .../server/display/VirtualDisplayAdapter.java | 177 + .../android/server/display/WifiDisplayAdapter.java | 760 + .../server/display/WifiDisplayController.java | 1113 ++ .../com/android/server/dreams/DreamController.java | 273 + .../android/server/dreams/DreamManagerService.java | 425 + .../com/android/server/firewall/AndFilter.java | 46 + .../android/server/firewall/CategoryFilter.java | 58 + .../java/com/android/server/firewall/Filter.java | 36 + .../com/android/server/firewall/FilterFactory.java | 40 + .../com/android/server/firewall/FilterList.java | 41 + .../android/server/firewall/IntentFirewall.java | 601 + .../com/android/server/firewall/NotFilter.java | 59 + .../java/com/android/server/firewall/OrFilter.java | 46 + .../com/android/server/firewall/PortFilter.java | 110 + .../com/android/server/firewall/SenderFilter.java | 115 + .../server/firewall/SenderPermissionFilter.java | 57 + .../com/android/server/firewall/StringFilter.java | 338 + .../server/input/InputApplicationHandle.java | 54 + .../android/server/input/InputManagerService.java | 1642 ++ .../android/server/input/InputWindowHandle.java | 111 + .../android/server/input/PersistentDataStore.java | 416 + .../core/java/com/android/server/lights/Light.java | 41 + .../com/android/server/lights/LightsManager.java | 31 + .../com/android/server/lights/LightsService.java | 205 + .../location/ComprehensiveCountryDetector.java | 492 + .../server/location/CountryDetectorBase.java | 72 + .../server/location/FlpHardwareProvider.java | 427 + .../location/FusedLocationHardwareSecure.java | 119 + .../com/android/server/location/FusedProxy.java | 128 + .../com/android/server/location/GeocoderProxy.java | 105 + .../android/server/location/GeofenceManager.java | 440 + .../com/android/server/location/GeofenceProxy.java | 186 + .../com/android/server/location/GeofenceState.java | 114 + .../server/location/GpsLocationProvider.java | 1924 +++ .../android/server/location/GpsXtraDownloader.java | 171 + .../location/LocationBasedCountryDetector.java | 245 + .../android/server/location/LocationBlacklist.java | 151 + .../android/server/location/LocationFudger.java | 385 + .../server/location/LocationProviderInterface.java | 48 + .../server/location/LocationProviderProxy.java | 292 + .../com/android/server/location/MockProvider.java | 162 + .../android/server/location/PassiveProvider.java | 119 + .../android/server/media/MediaRouterService.java | 1423 ++ .../server/media/RemoteDisplayProviderProxy.java | 442 + .../server/media/RemoteDisplayProviderWatcher.java | 219 + .../com/android/server/net/LockdownVpnTracker.java | 324 + .../com/android/server/net/NetworkIdentitySet.java | 95 + .../server/net/NetworkPolicyManagerService.java | 2089 +++ .../android/server/net/NetworkStatsCollection.java | 534 + .../android/server/net/NetworkStatsRecorder.java | 407 + .../android/server/net/NetworkStatsService.java | 1340 ++ .../server/notification/NotificationDelegate.java | 27 + .../notification/NotificationManagerInternal.java | 8 + .../notification/NotificationManagerService.java | 2427 +++ .../android/server/os/SchedulingPolicyService.java | 64 + .../java/com/android/server/pm/BasePermission.java | 59 + .../com/android/server/pm/GrantedPermissions.java | 51 + .../core/java/com/android/server/pm/Installer.java | 375 + .../java/com/android/server/pm/KeySetManager.java | 586 + .../com/android/server/pm/PackageKeySetData.java | 125 + .../android/server/pm/PackageManagerService.java | 11592 +++++++++++++ .../java/com/android/server/pm/PackageSetting.java | 64 + .../com/android/server/pm/PackageSettingBase.java | 371 + .../com/android/server/pm/PackageSignatures.java | 204 + .../server/pm/PackageVerificationResponse.java | 28 + .../server/pm/PackageVerificationState.java | 170 + .../java/com/android/server/pm/PendingPackage.java | 30 + .../com/android/server/pm/PreferredActivity.java | 79 + .../com/android/server/pm/PreferredComponent.java | 234 + .../android/server/pm/PreferredIntentResolver.java | 40 + .../java/com/android/server/pm/SELinuxMMAC.java | 394 + .../core/java/com/android/server/pm/Settings.java | 3187 ++++ .../com/android/server/pm/SharedUserSetting.java | 66 + .../com/android/server/pm/UserManagerService.java | 1568 ++ .../com/android/server/power/DisplayBlanker.java | 25 + .../server/power/DisplayPowerController.java | 1379 ++ .../android/server/power/DisplayPowerRequest.java | 117 + .../android/server/power/DisplayPowerState.java | 414 + .../com/android/server/power/ElectronBeam.java | 733 + .../java/com/android/server/power/Notifier.java | 546 + .../android/server/power/PowerManagerService.java | 2739 +++ .../com/android/server/power/RampAnimator.java | 137 + .../com/android/server/power/ScreenOnBlocker.java | 41 + .../com/android/server/power/ShutdownThread.java | 518 + .../com/android/server/power/SuspendBlocker.java | 43 + .../server/power/WirelessChargerDetector.java | 357 + .../server/search/SearchManagerService.java | 279 + .../com/android/server/search/Searchables.java | 464 + .../server/statusbar/StatusBarManagerInternal.java | 29 + .../server/statusbar/StatusBarManagerService.java | 701 + .../storage/DeviceStorageMonitorInternal.java | 24 + .../storage/DeviceStorageMonitorService.java | 505 + .../android/server/twilight/TwilightListener.java | 21 + .../android/server/twilight/TwilightManager.java | 24 + .../android/server/twilight/TwilightService.java | 467 + .../com/android/server/twilight/TwilightState.java | 112 + .../CarrierProvisioningUrlsInstallReceiver.java | 24 + .../server/updates/CertPinInstallReceiver.java | 24 + .../updates/ConfigUpdateInstallReceiver.java | 264 + .../updates/IntentFirewallInstallReceiver.java | 28 + .../updates/SELinuxPolicyInstallReceiver.java | 141 + .../updates/SmsShortCodesInstallReceiver.java | 24 + .../server/updates/TZInfoInstallReceiver.java | 34 + .../android/server/usb/UsbDebuggingManager.java | 342 + .../com/android/server/usb/UsbDeviceManager.java | 885 + .../com/android/server/usb/UsbHostManager.java | 220 + .../java/com/android/server/usb/UsbService.java | 288 + .../com/android/server/usb/UsbSettingsManager.java | 1100 ++ .../server/wallpaper/WallpaperManagerService.java | 1353 ++ .../core/java/com/android/server/wifi/README.txt | 19 + .../com/android/server/wifi/WifiController.java | 751 + .../server/wifi/WifiNotificationController.java | 297 + .../java/com/android/server/wifi/WifiService.java | 1599 ++ .../com/android/server/wifi/WifiSettingsStore.java | 197 + .../com/android/server/wifi/WifiTrafficPoller.java | 191 + .../java/com/android/server/wm/AppTransition.java | 767 + .../com/android/server/wm/AppWindowAnimator.java | 334 + .../java/com/android/server/wm/AppWindowToken.java | 305 + .../java/com/android/server/wm/BlackFrame.java | 196 + .../core/java/com/android/server/wm/DimLayer.java | 290 + .../java/com/android/server/wm/DisplayContent.java | 433 + .../com/android/server/wm/DisplayMagnifier.java | 756 + .../com/android/server/wm/DisplaySettings.java | 224 + .../core/java/com/android/server/wm/DragState.java | 442 + .../java/com/android/server/wm/FakeWindowImpl.java | 112 + .../com/android/server/wm/FocusedStackFrame.java | 142 + .../java/com/android/server/wm/InputMonitor.java | 493 + .../android/server/wm/KeyguardDisableHandler.java | 115 + .../android/server/wm/PointerEventDispatcher.java | 90 + .../android/server/wm/ScreenRotationAnimation.java | 1004 ++ .../core/java/com/android/server/wm/Session.java | 503 + .../server/wm/StackTapPointerEventListener.java | 87 + .../java/com/android/server/wm/StartingData.java | 43 + .../com/android/server/wm/StrictModeFlash.java | 116 + services/core/java/com/android/server/wm/Task.java | 59 + .../core/java/com/android/server/wm/TaskGroup.java | 31 + .../core/java/com/android/server/wm/TaskStack.java | 354 + .../java/com/android/server/wm/ViewServer.java | 336 + .../core/java/com/android/server/wm/Watermark.java | 178 + .../java/com/android/server/wm/WindowAnimator.java | 689 + .../android/server/wm/WindowManagerService.java | 10827 ++++++++++++ .../java/com/android/server/wm/WindowState.java | 1482 ++ .../com/android/server/wm/WindowStateAnimator.java | 1660 ++ .../java/com/android/server/wm/WindowToken.java | 103 + services/core/java/service.mk | 6 + services/core/jni/Android.mk | 56 + .../jni/com_android_server_AlarmManagerService.cpp | 321 + .../jni/com_android_server_AssetAtlasService.cpp | 272 + .../jni/com_android_server_ConsumerIrService.cpp | 114 + .../core/jni/com_android_server_SerialService.cpp | 82 + .../core/jni/com_android_server_SystemServer.cpp | 51 + .../jni/com_android_server_UsbDeviceManager.cpp | 157 + .../core/jni/com_android_server_UsbHostManager.cpp | 204 + .../jni/com_android_server_VibratorService.cpp | 61 + .../jni/com_android_server_connectivity_Vpn.cpp | 473 + ...android_server_input_InputApplicationHandle.cpp | 158 + ...m_android_server_input_InputApplicationHandle.h | 46 + ...om_android_server_input_InputManagerService.cpp | 1464 ++ .../com_android_server_input_InputWindowHandle.cpp | 309 + .../com_android_server_input_InputWindowHandle.h | 47 + .../com_android_server_lights_LightsService.cpp | 141 + ...android_server_location_FlpHardwareProvider.cpp | 1002 ++ ...android_server_location_GpsLocationProvider.cpp | 777 + ...om_android_server_power_PowerManagerService.cpp | 252 + .../com_android_server_power_PowerManagerService.h | 35 + services/core/jni/onload.cpp | 73 + .../devicepolicy/DevicePolicyManagerService.java | 2969 ++++ services/devicepolicy/java/service.mk | 11 + services/input/Android.mk | 59 - services/input/EventHub.cpp | 1589 -- services/input/EventHub.h | 447 - services/input/InputApplication.cpp | 42 - services/input/InputApplication.h | 83 - services/input/InputDispatcher.cpp | 4470 ----- services/input/InputDispatcher.h | 1123 -- services/input/InputListener.cpp | 182 - services/input/InputListener.h | 196 - services/input/InputManager.cpp | 93 - services/input/InputManager.h | 109 - services/input/InputReader.cpp | 6530 -------- services/input/InputReader.h | 1816 -- services/input/InputWindow.cpp | 64 - services/input/InputWindow.h | 206 - services/input/PointerController.cpp | 604 - services/input/PointerController.h | 266 - services/input/SpriteController.cpp | 482 - services/input/SpriteController.h | 293 - services/input/tests/Android.mk | 48 - services/input/tests/InputDispatcher_test.cpp | 247 - services/input/tests/InputReader_test.cpp | 5099 ------ services/java/Android.mk | 18 - .../com/android/server/AlarmManagerService.java | 1515 -- .../java/com/android/server/AppOpsService.java | 1083 -- .../java/com/android/server/AssetAtlasService.java | 735 - .../java/com/android/server/AttributeCache.java | 143 - .../java/com/android/server/BatteryService.java | 753 - .../android/server/BluetoothManagerService.java | 1262 -- services/java/com/android/server/BootReceiver.java | 241 - .../java/com/android/server/BrickReceiver.java | 31 - .../java/com/android/server/CertBlacklister.java | 142 - .../server/CommonTimeManagementService.java | 376 - .../com/android/server/ConnectivityService.java | 4930 ------ .../java/com/android/server/ConsumerIrService.java | 135 - .../com/android/server/CountryDetectorService.java | 227 - .../java/com/android/server/DiskStatsService.java | 113 - services/java/com/android/server/DockObserver.java | 206 - .../com/android/server/DropBoxManagerService.java | 781 - services/java/com/android/server/EntropyMixer.java | 208 - .../java/com/android/server/EventLogTags.logtags | 188 - services/java/com/android/server/FgThread.java | 65 - .../server/INativeDaemonConnectorCallbacks.java | 24 - .../com/android/server/IdleMaintenanceService.java | 327 - .../android/server/InputMethodManagerService.java | 3574 ---- .../java/com/android/server/IntentResolver.java | 660 - services/java/com/android/server/IoThread.java | 62 - .../java/com/android/server/LocalServices.java | 58 - .../com/android/server/LocationManagerService.java | 2411 --- .../com/android/server/LockSettingsService.java | 530 - .../com/android/server/MasterClearReceiver.java | 55 - services/java/com/android/server/MountService.java | 2833 ---- .../com/android/server/NativeDaemonConnector.java | 598 - .../server/NativeDaemonConnectorException.java | 58 - .../java/com/android/server/NativeDaemonEvent.java | 254 - .../android/server/NetworkManagementService.java | 1810 -- .../android/server/NetworkTimeUpdateService.java | 309 - services/java/com/android/server/NsdService.java | 849 - services/java/com/android/server/RandomBlock.java | 101 - .../android/server/RecognitionManagerService.java | 167 - .../android/server/SamplingProfilerService.java | 121 - .../java/com/android/server/SerialService.java | 63 - .../java/com/android/server/ServiceWatcher.java | 382 - .../java/com/android/server/ShutdownActivity.java | 69 - services/java/com/android/server/SystemServer.java | 1159 -- .../java/com/android/server/SystemService.java | 151 - .../com/android/server/SystemServiceManager.java | 141 - .../java/com/android/server/TelephonyRegistry.java | 797 - .../android/server/TextServicesManagerService.java | 983 -- .../com/android/server/TwilightCalculator.java | 123 - .../com/android/server/UiModeManagerService.java | 618 - services/java/com/android/server/UiThread.java | 71 - .../java/com/android/server/UpdateLockService.java | 126 - .../java/com/android/server/VibratorService.java | 613 - services/java/com/android/server/Watchdog.java | 483 - .../com/android/server/WiredAccessoryManager.java | 433 - .../accessibility/AccessibilityInputFilter.java | 404 - .../accessibility/AccessibilityManagerService.java | 3131 ---- .../accessibility/EventStreamTransformation.java | 93 - .../android/server/accessibility/GestureUtils.java | 102 - .../server/accessibility/ScreenMagnifier.java | 1177 -- .../server/accessibility/TouchExplorer.java | 1937 --- .../server/accounts/AccountAuthenticatorCache.java | 94 - .../server/accounts/AccountManagerService.java | 3089 ---- .../accounts/IAccountAuthenticatorCache.java | 67 - .../java/com/android/server/am/ActiveServices.java | 2663 --- .../android/server/am/ActivityManagerService.java | 16548 ------------------- .../java/com/android/server/am/ActivityRecord.java | 1068 -- .../java/com/android/server/am/ActivityResult.java | 34 - .../java/com/android/server/am/ActivityStack.java | 3615 ---- .../android/server/am/ActivityStackSupervisor.java | 2974 ---- .../java/com/android/server/am/AppBindRecord.java | 63 - .../java/com/android/server/am/AppErrorDialog.java | 102 - .../java/com/android/server/am/AppErrorResult.java | 42 - .../android/server/am/AppNotRespondingDialog.java | 143 - .../server/am/AppWaitingForDebuggerDialog.java | 74 - .../java/com/android/server/am/BackupRecord.java | 58 - .../com/android/server/am/BaseErrorDialog.java | 82 - .../com/android/server/am/BatteryStatsService.java | 579 - .../com/android/server/am/BroadcastFilter.java | 74 - .../java/com/android/server/am/BroadcastQueue.java | 1219 -- .../com/android/server/am/BroadcastRecord.java | 210 - .../com/android/server/am/CompatModeDialog.java | 90 - .../com/android/server/am/CompatModePackages.java | 385 - .../com/android/server/am/ConnectionRecord.java | 110 - .../server/am/ContentProviderConnection.java | 94 - .../android/server/am/ContentProviderRecord.java | 252 - .../android/server/am/CoreSettingsObserver.java | 110 - .../com/android/server/am/EventLogTags.logtags | 91 - .../com/android/server/am/FactoryErrorDialog.java | 47 - .../com/android/server/am/IntentBindRecord.java | 112 - .../com/android/server/am/LaunchWarningWindow.java | 56 - .../com/android/server/am/NativeCrashListener.java | 285 - .../com/android/server/am/PendingIntentRecord.java | 364 - .../android/server/am/PendingThumbnailsRecord.java | 39 - .../java/com/android/server/am/ProcessList.java | 525 - .../java/com/android/server/am/ProcessMemInfo.java | 37 - .../java/com/android/server/am/ProcessRecord.java | 644 - .../com/android/server/am/ProcessStatsService.java | 909 - .../java/com/android/server/am/ProviderMap.java | 415 - .../java/com/android/server/am/ReceiverList.java | 116 - .../java/com/android/server/am/ServiceRecord.java | 544 - .../server/am/StrictModeViolationDialog.java | 100 - .../java/com/android/server/am/TaskAccessInfo.java | 34 - .../java/com/android/server/am/TaskRecord.java | 527 - .../com/android/server/am/ThumbnailHolder.java | 29 - .../java/com/android/server/am/UriPermission.java | 357 - .../com/android/server/am/UriPermissionOwner.java | 150 - .../com/android/server/am/UsageStatsService.java | 1173 -- .../com/android/server/am/UserStartedState.java | 60 - services/java/com/android/server/am/package.html | 5 - .../android/server/appwidget/AppWidgetService.java | 363 - .../server/appwidget/AppWidgetServiceImpl.java | 2126 --- .../server/backup/BackupManagerService.java | 6196 ------- .../server/backup/PackageManagerBackupAgent.java | 425 - .../android/server/backup/SystemBackupAgent.java | 185 - .../android/server/clipboard/ClipboardService.java | 366 - .../server/connectivity/DataConnectionStats.java | 154 - .../android/server/connectivity/Nat464Xlat.java | 200 - .../android/server/connectivity/PacManager.java | 381 - .../com/android/server/connectivity/Tethering.java | 1590 -- .../java/com/android/server/connectivity/Vpn.java | 1205 -- .../com/android/server/content/ContentService.java | 975 -- .../com/android/server/content/SyncManager.java | 2940 ---- .../com/android/server/content/SyncOperation.java | 294 - .../java/com/android/server/content/SyncQueue.java | 230 - .../android/server/content/SyncStorageEngine.java | 2638 --- .../devicepolicy/DevicePolicyManagerService.java | 2969 ---- .../com/android/server/display/DisplayAdapter.java | 129 - .../com/android/server/display/DisplayDevice.java | 213 - .../android/server/display/DisplayDeviceInfo.java | 302 - .../server/display/DisplayManagerService.java | 1312 -- .../server/display/DisplayTransactionListener.java | 26 - .../android/server/display/DisplayViewport.java | 75 - .../server/display/LocalDisplayAdapter.java | 211 - .../com/android/server/display/LogicalDisplay.java | 337 - .../server/display/OverlayDisplayAdapter.java | 359 - .../server/display/OverlayDisplayWindow.java | 376 - .../server/display/PersistentDataStore.java | 287 - .../server/display/VirtualDisplayAdapter.java | 177 - .../android/server/display/WifiDisplayAdapter.java | 760 - .../server/display/WifiDisplayController.java | 1113 -- .../com/android/server/dreams/DreamController.java | 273 - .../android/server/dreams/DreamManagerService.java | 425 - .../com/android/server/firewall/AndFilter.java | 46 - .../android/server/firewall/CategoryFilter.java | 58 - .../java/com/android/server/firewall/Filter.java | 36 - .../com/android/server/firewall/FilterFactory.java | 40 - .../com/android/server/firewall/FilterList.java | 41 - .../android/server/firewall/IntentFirewall.java | 601 - .../com/android/server/firewall/NotFilter.java | 59 - .../java/com/android/server/firewall/OrFilter.java | 46 - .../com/android/server/firewall/PortFilter.java | 110 - .../com/android/server/firewall/SenderFilter.java | 115 - .../server/firewall/SenderPermissionFilter.java | 57 - .../com/android/server/firewall/StringFilter.java | 338 - .../server/input/InputApplicationHandle.java | 54 - .../android/server/input/InputManagerService.java | 1642 -- .../android/server/input/InputWindowHandle.java | 111 - .../android/server/input/PersistentDataStore.java | 416 - services/java/com/android/server/lights/Light.java | 41 - .../com/android/server/lights/LightsManager.java | 31 - .../com/android/server/lights/LightsService.java | 205 - .../location/ComprehensiveCountryDetector.java | 492 - .../server/location/CountryDetectorBase.java | 72 - .../server/location/FlpHardwareProvider.java | 427 - .../location/FusedLocationHardwareSecure.java | 119 - .../com/android/server/location/FusedProxy.java | 128 - .../com/android/server/location/GeocoderProxy.java | 105 - .../android/server/location/GeofenceManager.java | 440 - .../com/android/server/location/GeofenceProxy.java | 186 - .../com/android/server/location/GeofenceState.java | 114 - .../server/location/GpsLocationProvider.java | 1924 --- .../android/server/location/GpsXtraDownloader.java | 171 - .../location/LocationBasedCountryDetector.java | 245 - .../android/server/location/LocationBlacklist.java | 151 - .../android/server/location/LocationFudger.java | 385 - .../server/location/LocationProviderInterface.java | 48 - .../server/location/LocationProviderProxy.java | 292 - .../com/android/server/location/MockProvider.java | 162 - .../android/server/location/PassiveProvider.java | 119 - .../android/server/media/MediaRouterService.java | 1423 -- .../server/media/RemoteDisplayProviderProxy.java | 442 - .../server/media/RemoteDisplayProviderWatcher.java | 219 - .../com/android/server/net/LockdownVpnTracker.java | 324 - .../com/android/server/net/NetworkIdentitySet.java | 95 - .../server/net/NetworkPolicyManagerService.java | 2089 --- .../android/server/net/NetworkStatsCollection.java | 534 - .../android/server/net/NetworkStatsRecorder.java | 407 - .../android/server/net/NetworkStatsService.java | 1340 -- .../server/notification/NotificationDelegate.java | 27 - .../notification/NotificationManagerInternal.java | 8 - .../notification/NotificationManagerService.java | 2427 --- .../android/server/os/SchedulingPolicyService.java | 64 - .../java/com/android/server/pm/BasePermission.java | 59 - .../com/android/server/pm/GrantedPermissions.java | 51 - services/java/com/android/server/pm/Installer.java | 375 - .../java/com/android/server/pm/KeySetManager.java | 586 - .../com/android/server/pm/PackageKeySetData.java | 125 - .../android/server/pm/PackageManagerService.java | 11592 ------------- .../java/com/android/server/pm/PackageSetting.java | 64 - .../com/android/server/pm/PackageSettingBase.java | 371 - .../com/android/server/pm/PackageSignatures.java | 204 - .../server/pm/PackageVerificationResponse.java | 28 - .../server/pm/PackageVerificationState.java | 170 - .../java/com/android/server/pm/PendingPackage.java | 30 - .../com/android/server/pm/PreferredActivity.java | 79 - .../com/android/server/pm/PreferredComponent.java | 234 - .../android/server/pm/PreferredIntentResolver.java | 40 - .../java/com/android/server/pm/SELinuxMMAC.java | 394 - services/java/com/android/server/pm/Settings.java | 3187 ---- .../com/android/server/pm/SharedUserSetting.java | 66 - .../com/android/server/pm/UserManagerService.java | 1568 -- .../com/android/server/power/DisplayBlanker.java | 25 - .../server/power/DisplayPowerController.java | 1379 -- .../android/server/power/DisplayPowerRequest.java | 117 - .../android/server/power/DisplayPowerState.java | 414 - .../com/android/server/power/ElectronBeam.java | 733 - .../java/com/android/server/power/Notifier.java | 546 - .../android/server/power/PowerManagerService.java | 2739 --- .../com/android/server/power/RampAnimator.java | 137 - .../com/android/server/power/ScreenOnBlocker.java | 41 - .../com/android/server/power/ShutdownThread.java | 518 - .../com/android/server/power/SuspendBlocker.java | 43 - .../server/power/WirelessChargerDetector.java | 357 - .../android/server/print/PrintManagerService.java | 656 - .../android/server/print/RemotePrintService.java | 806 - .../android/server/print/RemotePrintSpooler.java | 634 - .../java/com/android/server/print/UserState.java | 1628 -- .../server/search/SearchManagerService.java | 279 - .../com/android/server/search/Searchables.java | 464 - .../server/statusbar/StatusBarManagerInternal.java | 29 - .../server/statusbar/StatusBarManagerService.java | 701 - .../storage/DeviceStorageMonitorInternal.java | 24 - .../storage/DeviceStorageMonitorService.java | 505 - .../android/server/twilight/TwilightListener.java | 21 - .../android/server/twilight/TwilightManager.java | 24 - .../android/server/twilight/TwilightService.java | 467 - .../com/android/server/twilight/TwilightState.java | 112 - .../CarrierProvisioningUrlsInstallReceiver.java | 24 - .../server/updates/CertPinInstallReceiver.java | 24 - .../updates/ConfigUpdateInstallReceiver.java | 264 - .../updates/IntentFirewallInstallReceiver.java | 28 - .../updates/SELinuxPolicyInstallReceiver.java | 141 - .../updates/SmsShortCodesInstallReceiver.java | 24 - .../server/updates/TZInfoInstallReceiver.java | 34 - .../android/server/usb/UsbDebuggingManager.java | 342 - .../com/android/server/usb/UsbDeviceManager.java | 885 - .../com/android/server/usb/UsbHostManager.java | 220 - .../java/com/android/server/usb/UsbService.java | 288 - .../com/android/server/usb/UsbSettingsManager.java | 1100 -- .../server/wallpaper/WallpaperManagerService.java | 1353 -- services/java/com/android/server/wifi/README.txt | 19 - .../com/android/server/wifi/WifiController.java | 751 - .../server/wifi/WifiNotificationController.java | 297 - .../java/com/android/server/wifi/WifiService.java | 1599 -- .../com/android/server/wifi/WifiSettingsStore.java | 197 - .../com/android/server/wifi/WifiTrafficPoller.java | 191 - .../java/com/android/server/wm/AppTransition.java | 767 - .../com/android/server/wm/AppWindowAnimator.java | 334 - .../java/com/android/server/wm/AppWindowToken.java | 305 - .../java/com/android/server/wm/BlackFrame.java | 196 - services/java/com/android/server/wm/DimLayer.java | 290 - .../java/com/android/server/wm/DisplayContent.java | 433 - .../com/android/server/wm/DisplayMagnifier.java | 756 - .../com/android/server/wm/DisplaySettings.java | 224 - services/java/com/android/server/wm/DragState.java | 442 - .../java/com/android/server/wm/FakeWindowImpl.java | 112 - .../com/android/server/wm/FocusedStackFrame.java | 142 - .../java/com/android/server/wm/InputMonitor.java | 493 - .../android/server/wm/KeyguardDisableHandler.java | 115 - .../android/server/wm/PointerEventDispatcher.java | 90 - .../android/server/wm/ScreenRotationAnimation.java | 1004 -- services/java/com/android/server/wm/Session.java | 503 - .../server/wm/StackTapPointerEventListener.java | 87 - .../java/com/android/server/wm/StartingData.java | 43 - .../com/android/server/wm/StrictModeFlash.java | 116 - services/java/com/android/server/wm/Task.java | 59 - services/java/com/android/server/wm/TaskGroup.java | 31 - services/java/com/android/server/wm/TaskStack.java | 354 - .../java/com/android/server/wm/ViewServer.java | 336 - services/java/com/android/server/wm/Watermark.java | 178 - .../java/com/android/server/wm/WindowAnimator.java | 689 - .../android/server/wm/WindowManagerService.java | 10827 ------------ .../java/com/android/server/wm/WindowState.java | 1482 -- .../com/android/server/wm/WindowStateAnimator.java | 1660 -- .../java/com/android/server/wm/WindowToken.java | 103 - services/jni/Android.mk | 63 - .../jni/com_android_server_AlarmManagerService.cpp | 321 - .../jni/com_android_server_AssetAtlasService.cpp | 272 - .../jni/com_android_server_ConsumerIrService.cpp | 114 - services/jni/com_android_server_SerialService.cpp | 82 - services/jni/com_android_server_SystemServer.cpp | 51 - .../jni/com_android_server_UsbDeviceManager.cpp | 157 - services/jni/com_android_server_UsbHostManager.cpp | 204 - .../jni/com_android_server_VibratorService.cpp | 61 - .../jni/com_android_server_connectivity_Vpn.cpp | 473 - ...android_server_input_InputApplicationHandle.cpp | 158 - ...m_android_server_input_InputApplicationHandle.h | 46 - ...om_android_server_input_InputManagerService.cpp | 1464 -- .../com_android_server_input_InputWindowHandle.cpp | 309 - .../com_android_server_input_InputWindowHandle.h | 47 - .../com_android_server_lights_LightsService.cpp | 141 - ...android_server_location_FlpHardwareProvider.cpp | 1002 -- ...android_server_location_GpsLocationProvider.cpp | 777 - ...om_android_server_power_PowerManagerService.cpp | 252 - .../com_android_server_power_PowerManagerService.h | 35 - services/jni/onload.cpp | 73 - .../android/server/print/PrintManagerService.java | 656 + .../android/server/print/RemotePrintService.java | 806 + .../android/server/print/RemotePrintSpooler.java | 634 + .../java/com/android/server/print/UserState.java | 1628 ++ services/print/java/service.mk | 11 + 659 files changed, 198889 insertions(+), 227513 deletions(-) create mode 100644 services/Android.mk create mode 100644 services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java create mode 100644 services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java create mode 100644 services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java create mode 100644 services/accessibility/java/com/android/server/accessibility/GestureUtils.java create mode 100644 services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java create mode 100644 services/accessibility/java/com/android/server/accessibility/TouchExplorer.java create mode 100644 services/accessibility/java/service.mk create mode 100644 services/appwidget/java/com/android/server/appwidget/AppWidgetService.java create mode 100644 services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java create mode 100644 services/appwidget/java/service.mk create mode 100644 services/backup/java/com/android/server/backup/BackupManagerService.java create mode 100644 services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java create mode 100644 services/backup/java/com/android/server/backup/SystemBackupAgent.java create mode 100644 services/backup/java/service.mk delete mode 100644 services/common_time/Android.mk delete mode 100644 services/common_time/clock_recovery.cpp delete mode 100644 services/common_time/clock_recovery.h delete mode 100644 services/common_time/common_clock.cpp delete mode 100644 services/common_time/common_clock.h delete mode 100644 services/common_time/common_clock_service.cpp delete mode 100644 services/common_time/common_clock_service.h delete mode 100644 services/common_time/common_time_config_service.cpp delete mode 100644 services/common_time/common_time_config_service.h delete mode 100644 services/common_time/common_time_server.cpp delete mode 100644 services/common_time/common_time_server.h delete mode 100644 services/common_time/common_time_server_api.cpp delete mode 100644 services/common_time/common_time_server_packets.cpp delete mode 100644 services/common_time/common_time_server_packets.h delete mode 100644 services/common_time/diag_thread.cpp delete mode 100644 services/common_time/diag_thread.h delete mode 100644 services/common_time/main.cpp delete mode 100644 services/common_time/utils.cpp delete mode 100644 services/common_time/utils.h create mode 100644 services/core/java/com/android/server/AlarmManagerService.java create mode 100644 services/core/java/com/android/server/AppOpsService.java create mode 100644 services/core/java/com/android/server/AssetAtlasService.java create mode 100644 services/core/java/com/android/server/AttributeCache.java create mode 100644 services/core/java/com/android/server/BatteryService.java create mode 100644 services/core/java/com/android/server/BluetoothManagerService.java create mode 100644 services/core/java/com/android/server/BootReceiver.java create mode 100644 services/core/java/com/android/server/BrickReceiver.java create mode 100644 services/core/java/com/android/server/CertBlacklister.java create mode 100644 services/core/java/com/android/server/CommonTimeManagementService.java create mode 100644 services/core/java/com/android/server/ConnectivityService.java create mode 100644 services/core/java/com/android/server/ConsumerIrService.java create mode 100644 services/core/java/com/android/server/CountryDetectorService.java create mode 100644 services/core/java/com/android/server/DiskStatsService.java create mode 100644 services/core/java/com/android/server/DockObserver.java create mode 100644 services/core/java/com/android/server/DropBoxManagerService.java create mode 100644 services/core/java/com/android/server/EntropyMixer.java create mode 100644 services/core/java/com/android/server/EventLogTags.logtags create mode 100644 services/core/java/com/android/server/FgThread.java create mode 100644 services/core/java/com/android/server/INativeDaemonConnectorCallbacks.java create mode 100644 services/core/java/com/android/server/IdleMaintenanceService.java create mode 100644 services/core/java/com/android/server/InputMethodManagerService.java create mode 100644 services/core/java/com/android/server/IntentResolver.java create mode 100644 services/core/java/com/android/server/IoThread.java create mode 100644 services/core/java/com/android/server/LocalServices.java create mode 100644 services/core/java/com/android/server/LocationManagerService.java create mode 100644 services/core/java/com/android/server/LockSettingsService.java create mode 100644 services/core/java/com/android/server/MasterClearReceiver.java create mode 100644 services/core/java/com/android/server/MountService.java create mode 100644 services/core/java/com/android/server/NativeDaemonConnector.java create mode 100644 services/core/java/com/android/server/NativeDaemonConnectorException.java create mode 100644 services/core/java/com/android/server/NativeDaemonEvent.java create mode 100644 services/core/java/com/android/server/NetworkManagementService.java create mode 100644 services/core/java/com/android/server/NetworkTimeUpdateService.java create mode 100644 services/core/java/com/android/server/NsdService.java create mode 100644 services/core/java/com/android/server/RandomBlock.java create mode 100644 services/core/java/com/android/server/RecognitionManagerService.java create mode 100644 services/core/java/com/android/server/SamplingProfilerService.java create mode 100644 services/core/java/com/android/server/SerialService.java create mode 100644 services/core/java/com/android/server/ServiceWatcher.java create mode 100644 services/core/java/com/android/server/ShutdownActivity.java create mode 100644 services/core/java/com/android/server/SystemServer.java create mode 100644 services/core/java/com/android/server/SystemService.java create mode 100644 services/core/java/com/android/server/SystemServiceManager.java create mode 100644 services/core/java/com/android/server/TelephonyRegistry.java create mode 100644 services/core/java/com/android/server/TextServicesManagerService.java create mode 100644 services/core/java/com/android/server/TwilightCalculator.java create mode 100644 services/core/java/com/android/server/UiModeManagerService.java create mode 100644 services/core/java/com/android/server/UiThread.java create mode 100644 services/core/java/com/android/server/UpdateLockService.java create mode 100644 services/core/java/com/android/server/VibratorService.java create mode 100644 services/core/java/com/android/server/Watchdog.java create mode 100644 services/core/java/com/android/server/WiredAccessoryManager.java create mode 100644 services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java create mode 100644 services/core/java/com/android/server/accounts/AccountManagerService.java create mode 100644 services/core/java/com/android/server/accounts/IAccountAuthenticatorCache.java create mode 100644 services/core/java/com/android/server/am/ActiveServices.java create mode 100644 services/core/java/com/android/server/am/ActivityManagerService.java create mode 100644 services/core/java/com/android/server/am/ActivityRecord.java create mode 100644 services/core/java/com/android/server/am/ActivityResult.java create mode 100644 services/core/java/com/android/server/am/ActivityStack.java create mode 100644 services/core/java/com/android/server/am/ActivityStackSupervisor.java create mode 100644 services/core/java/com/android/server/am/AppBindRecord.java create mode 100644 services/core/java/com/android/server/am/AppErrorDialog.java create mode 100644 services/core/java/com/android/server/am/AppErrorResult.java create mode 100644 services/core/java/com/android/server/am/AppNotRespondingDialog.java create mode 100644 services/core/java/com/android/server/am/AppWaitingForDebuggerDialog.java create mode 100644 services/core/java/com/android/server/am/BackupRecord.java create mode 100644 services/core/java/com/android/server/am/BaseErrorDialog.java create mode 100644 services/core/java/com/android/server/am/BatteryStatsService.java create mode 100644 services/core/java/com/android/server/am/BroadcastFilter.java create mode 100644 services/core/java/com/android/server/am/BroadcastQueue.java create mode 100644 services/core/java/com/android/server/am/BroadcastRecord.java create mode 100644 services/core/java/com/android/server/am/CompatModeDialog.java create mode 100644 services/core/java/com/android/server/am/CompatModePackages.java create mode 100644 services/core/java/com/android/server/am/ConnectionRecord.java create mode 100644 services/core/java/com/android/server/am/ContentProviderConnection.java create mode 100644 services/core/java/com/android/server/am/ContentProviderRecord.java create mode 100644 services/core/java/com/android/server/am/CoreSettingsObserver.java create mode 100644 services/core/java/com/android/server/am/EventLogTags.logtags create mode 100644 services/core/java/com/android/server/am/FactoryErrorDialog.java create mode 100644 services/core/java/com/android/server/am/IntentBindRecord.java create mode 100644 services/core/java/com/android/server/am/LaunchWarningWindow.java create mode 100644 services/core/java/com/android/server/am/NativeCrashListener.java create mode 100644 services/core/java/com/android/server/am/PendingIntentRecord.java create mode 100644 services/core/java/com/android/server/am/PendingThumbnailsRecord.java create mode 100644 services/core/java/com/android/server/am/ProcessList.java create mode 100644 services/core/java/com/android/server/am/ProcessMemInfo.java create mode 100644 services/core/java/com/android/server/am/ProcessRecord.java create mode 100644 services/core/java/com/android/server/am/ProcessStatsService.java create mode 100644 services/core/java/com/android/server/am/ProviderMap.java create mode 100644 services/core/java/com/android/server/am/ReceiverList.java create mode 100644 services/core/java/com/android/server/am/ServiceRecord.java create mode 100644 services/core/java/com/android/server/am/StrictModeViolationDialog.java create mode 100644 services/core/java/com/android/server/am/TaskAccessInfo.java create mode 100644 services/core/java/com/android/server/am/TaskRecord.java create mode 100644 services/core/java/com/android/server/am/ThumbnailHolder.java create mode 100644 services/core/java/com/android/server/am/UriPermission.java create mode 100644 services/core/java/com/android/server/am/UriPermissionOwner.java create mode 100644 services/core/java/com/android/server/am/UsageStatsService.java create mode 100644 services/core/java/com/android/server/am/UserStartedState.java create mode 100644 services/core/java/com/android/server/am/package.html create mode 100644 services/core/java/com/android/server/clipboard/ClipboardService.java create mode 100644 services/core/java/com/android/server/connectivity/DataConnectionStats.java create mode 100644 services/core/java/com/android/server/connectivity/Nat464Xlat.java create mode 100644 services/core/java/com/android/server/connectivity/PacManager.java create mode 100644 services/core/java/com/android/server/connectivity/Tethering.java create mode 100644 services/core/java/com/android/server/connectivity/Vpn.java create mode 100644 services/core/java/com/android/server/content/ContentService.java create mode 100644 services/core/java/com/android/server/content/SyncManager.java create mode 100644 services/core/java/com/android/server/content/SyncOperation.java create mode 100644 services/core/java/com/android/server/content/SyncQueue.java create mode 100644 services/core/java/com/android/server/content/SyncStorageEngine.java create mode 100644 services/core/java/com/android/server/display/DisplayAdapter.java create mode 100644 services/core/java/com/android/server/display/DisplayDevice.java create mode 100644 services/core/java/com/android/server/display/DisplayDeviceInfo.java create mode 100644 services/core/java/com/android/server/display/DisplayManagerService.java create mode 100644 services/core/java/com/android/server/display/DisplayTransactionListener.java create mode 100644 services/core/java/com/android/server/display/DisplayViewport.java create mode 100644 services/core/java/com/android/server/display/LocalDisplayAdapter.java create mode 100644 services/core/java/com/android/server/display/LogicalDisplay.java create mode 100644 services/core/java/com/android/server/display/OverlayDisplayAdapter.java create mode 100644 services/core/java/com/android/server/display/OverlayDisplayWindow.java create mode 100644 services/core/java/com/android/server/display/PersistentDataStore.java create mode 100644 services/core/java/com/android/server/display/VirtualDisplayAdapter.java create mode 100644 services/core/java/com/android/server/display/WifiDisplayAdapter.java create mode 100644 services/core/java/com/android/server/display/WifiDisplayController.java create mode 100644 services/core/java/com/android/server/dreams/DreamController.java create mode 100644 services/core/java/com/android/server/dreams/DreamManagerService.java create mode 100644 services/core/java/com/android/server/firewall/AndFilter.java create mode 100644 services/core/java/com/android/server/firewall/CategoryFilter.java create mode 100644 services/core/java/com/android/server/firewall/Filter.java create mode 100644 services/core/java/com/android/server/firewall/FilterFactory.java create mode 100644 services/core/java/com/android/server/firewall/FilterList.java create mode 100644 services/core/java/com/android/server/firewall/IntentFirewall.java create mode 100644 services/core/java/com/android/server/firewall/NotFilter.java create mode 100644 services/core/java/com/android/server/firewall/OrFilter.java create mode 100644 services/core/java/com/android/server/firewall/PortFilter.java create mode 100644 services/core/java/com/android/server/firewall/SenderFilter.java create mode 100644 services/core/java/com/android/server/firewall/SenderPermissionFilter.java create mode 100644 services/core/java/com/android/server/firewall/StringFilter.java create mode 100644 services/core/java/com/android/server/input/InputApplicationHandle.java create mode 100644 services/core/java/com/android/server/input/InputManagerService.java create mode 100644 services/core/java/com/android/server/input/InputWindowHandle.java create mode 100644 services/core/java/com/android/server/input/PersistentDataStore.java create mode 100644 services/core/java/com/android/server/lights/Light.java create mode 100644 services/core/java/com/android/server/lights/LightsManager.java create mode 100644 services/core/java/com/android/server/lights/LightsService.java create mode 100644 services/core/java/com/android/server/location/ComprehensiveCountryDetector.java create mode 100644 services/core/java/com/android/server/location/CountryDetectorBase.java create mode 100644 services/core/java/com/android/server/location/FlpHardwareProvider.java create mode 100644 services/core/java/com/android/server/location/FusedLocationHardwareSecure.java create mode 100644 services/core/java/com/android/server/location/FusedProxy.java create mode 100644 services/core/java/com/android/server/location/GeocoderProxy.java create mode 100644 services/core/java/com/android/server/location/GeofenceManager.java create mode 100644 services/core/java/com/android/server/location/GeofenceProxy.java create mode 100644 services/core/java/com/android/server/location/GeofenceState.java create mode 100644 services/core/java/com/android/server/location/GpsLocationProvider.java create mode 100644 services/core/java/com/android/server/location/GpsXtraDownloader.java create mode 100644 services/core/java/com/android/server/location/LocationBasedCountryDetector.java create mode 100644 services/core/java/com/android/server/location/LocationBlacklist.java create mode 100644 services/core/java/com/android/server/location/LocationFudger.java create mode 100644 services/core/java/com/android/server/location/LocationProviderInterface.java create mode 100644 services/core/java/com/android/server/location/LocationProviderProxy.java create mode 100644 services/core/java/com/android/server/location/MockProvider.java create mode 100644 services/core/java/com/android/server/location/PassiveProvider.java create mode 100644 services/core/java/com/android/server/media/MediaRouterService.java create mode 100644 services/core/java/com/android/server/media/RemoteDisplayProviderProxy.java create mode 100644 services/core/java/com/android/server/media/RemoteDisplayProviderWatcher.java create mode 100644 services/core/java/com/android/server/net/LockdownVpnTracker.java create mode 100644 services/core/java/com/android/server/net/NetworkIdentitySet.java create mode 100644 services/core/java/com/android/server/net/NetworkPolicyManagerService.java create mode 100644 services/core/java/com/android/server/net/NetworkStatsCollection.java create mode 100644 services/core/java/com/android/server/net/NetworkStatsRecorder.java create mode 100644 services/core/java/com/android/server/net/NetworkStatsService.java create mode 100644 services/core/java/com/android/server/notification/NotificationDelegate.java create mode 100644 services/core/java/com/android/server/notification/NotificationManagerInternal.java create mode 100644 services/core/java/com/android/server/notification/NotificationManagerService.java create mode 100644 services/core/java/com/android/server/os/SchedulingPolicyService.java create mode 100644 services/core/java/com/android/server/pm/BasePermission.java create mode 100644 services/core/java/com/android/server/pm/GrantedPermissions.java create mode 100644 services/core/java/com/android/server/pm/Installer.java create mode 100644 services/core/java/com/android/server/pm/KeySetManager.java create mode 100644 services/core/java/com/android/server/pm/PackageKeySetData.java create mode 100755 services/core/java/com/android/server/pm/PackageManagerService.java create mode 100644 services/core/java/com/android/server/pm/PackageSetting.java create mode 100644 services/core/java/com/android/server/pm/PackageSettingBase.java create mode 100644 services/core/java/com/android/server/pm/PackageSignatures.java create mode 100644 services/core/java/com/android/server/pm/PackageVerificationResponse.java create mode 100644 services/core/java/com/android/server/pm/PackageVerificationState.java create mode 100644 services/core/java/com/android/server/pm/PendingPackage.java create mode 100644 services/core/java/com/android/server/pm/PreferredActivity.java create mode 100644 services/core/java/com/android/server/pm/PreferredComponent.java create mode 100644 services/core/java/com/android/server/pm/PreferredIntentResolver.java create mode 100644 services/core/java/com/android/server/pm/SELinuxMMAC.java create mode 100644 services/core/java/com/android/server/pm/Settings.java create mode 100644 services/core/java/com/android/server/pm/SharedUserSetting.java create mode 100644 services/core/java/com/android/server/pm/UserManagerService.java create mode 100644 services/core/java/com/android/server/power/DisplayBlanker.java create mode 100644 services/core/java/com/android/server/power/DisplayPowerController.java create mode 100644 services/core/java/com/android/server/power/DisplayPowerRequest.java create mode 100644 services/core/java/com/android/server/power/DisplayPowerState.java create mode 100644 services/core/java/com/android/server/power/ElectronBeam.java create mode 100644 services/core/java/com/android/server/power/Notifier.java create mode 100644 services/core/java/com/android/server/power/PowerManagerService.java create mode 100644 services/core/java/com/android/server/power/RampAnimator.java create mode 100644 services/core/java/com/android/server/power/ScreenOnBlocker.java create mode 100644 services/core/java/com/android/server/power/ShutdownThread.java create mode 100644 services/core/java/com/android/server/power/SuspendBlocker.java create mode 100644 services/core/java/com/android/server/power/WirelessChargerDetector.java create mode 100644 services/core/java/com/android/server/search/SearchManagerService.java create mode 100644 services/core/java/com/android/server/search/Searchables.java create mode 100644 services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java create mode 100644 services/core/java/com/android/server/statusbar/StatusBarManagerService.java create mode 100644 services/core/java/com/android/server/storage/DeviceStorageMonitorInternal.java create mode 100644 services/core/java/com/android/server/storage/DeviceStorageMonitorService.java create mode 100644 services/core/java/com/android/server/twilight/TwilightListener.java create mode 100644 services/core/java/com/android/server/twilight/TwilightManager.java create mode 100644 services/core/java/com/android/server/twilight/TwilightService.java create mode 100644 services/core/java/com/android/server/twilight/TwilightState.java create mode 100644 services/core/java/com/android/server/updates/CarrierProvisioningUrlsInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/CertPinInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/ConfigUpdateInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/IntentFirewallInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/SELinuxPolicyInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/SmsShortCodesInstallReceiver.java create mode 100644 services/core/java/com/android/server/updates/TZInfoInstallReceiver.java create mode 100644 services/core/java/com/android/server/usb/UsbDebuggingManager.java create mode 100644 services/core/java/com/android/server/usb/UsbDeviceManager.java create mode 100644 services/core/java/com/android/server/usb/UsbHostManager.java create mode 100644 services/core/java/com/android/server/usb/UsbService.java create mode 100644 services/core/java/com/android/server/usb/UsbSettingsManager.java create mode 100644 services/core/java/com/android/server/wallpaper/WallpaperManagerService.java create mode 100644 services/core/java/com/android/server/wifi/README.txt create mode 100644 services/core/java/com/android/server/wifi/WifiController.java create mode 100644 services/core/java/com/android/server/wifi/WifiNotificationController.java create mode 100644 services/core/java/com/android/server/wifi/WifiService.java create mode 100644 services/core/java/com/android/server/wifi/WifiSettingsStore.java create mode 100644 services/core/java/com/android/server/wifi/WifiTrafficPoller.java create mode 100644 services/core/java/com/android/server/wm/AppTransition.java create mode 100644 services/core/java/com/android/server/wm/AppWindowAnimator.java create mode 100644 services/core/java/com/android/server/wm/AppWindowToken.java create mode 100644 services/core/java/com/android/server/wm/BlackFrame.java create mode 100644 services/core/java/com/android/server/wm/DimLayer.java create mode 100644 services/core/java/com/android/server/wm/DisplayContent.java create mode 100644 services/core/java/com/android/server/wm/DisplayMagnifier.java create mode 100644 services/core/java/com/android/server/wm/DisplaySettings.java create mode 100644 services/core/java/com/android/server/wm/DragState.java create mode 100644 services/core/java/com/android/server/wm/FakeWindowImpl.java create mode 100644 services/core/java/com/android/server/wm/FocusedStackFrame.java create mode 100644 services/core/java/com/android/server/wm/InputMonitor.java create mode 100644 services/core/java/com/android/server/wm/KeyguardDisableHandler.java create mode 100644 services/core/java/com/android/server/wm/PointerEventDispatcher.java create mode 100644 services/core/java/com/android/server/wm/ScreenRotationAnimation.java create mode 100644 services/core/java/com/android/server/wm/Session.java create mode 100644 services/core/java/com/android/server/wm/StackTapPointerEventListener.java create mode 100644 services/core/java/com/android/server/wm/StartingData.java create mode 100644 services/core/java/com/android/server/wm/StrictModeFlash.java create mode 100644 services/core/java/com/android/server/wm/Task.java create mode 100644 services/core/java/com/android/server/wm/TaskGroup.java create mode 100644 services/core/java/com/android/server/wm/TaskStack.java create mode 100644 services/core/java/com/android/server/wm/ViewServer.java create mode 100644 services/core/java/com/android/server/wm/Watermark.java create mode 100644 services/core/java/com/android/server/wm/WindowAnimator.java create mode 100644 services/core/java/com/android/server/wm/WindowManagerService.java create mode 100644 services/core/java/com/android/server/wm/WindowState.java create mode 100644 services/core/java/com/android/server/wm/WindowStateAnimator.java create mode 100644 services/core/java/com/android/server/wm/WindowToken.java create mode 100644 services/core/java/service.mk create mode 100644 services/core/jni/Android.mk create mode 100644 services/core/jni/com_android_server_AlarmManagerService.cpp create mode 100644 services/core/jni/com_android_server_AssetAtlasService.cpp create mode 100644 services/core/jni/com_android_server_ConsumerIrService.cpp create mode 100644 services/core/jni/com_android_server_SerialService.cpp create mode 100644 services/core/jni/com_android_server_SystemServer.cpp create mode 100644 services/core/jni/com_android_server_UsbDeviceManager.cpp create mode 100644 services/core/jni/com_android_server_UsbHostManager.cpp create mode 100644 services/core/jni/com_android_server_VibratorService.cpp create mode 100644 services/core/jni/com_android_server_connectivity_Vpn.cpp create mode 100644 services/core/jni/com_android_server_input_InputApplicationHandle.cpp create mode 100644 services/core/jni/com_android_server_input_InputApplicationHandle.h create mode 100644 services/core/jni/com_android_server_input_InputManagerService.cpp create mode 100644 services/core/jni/com_android_server_input_InputWindowHandle.cpp create mode 100644 services/core/jni/com_android_server_input_InputWindowHandle.h create mode 100644 services/core/jni/com_android_server_lights_LightsService.cpp create mode 100644 services/core/jni/com_android_server_location_FlpHardwareProvider.cpp create mode 100644 services/core/jni/com_android_server_location_GpsLocationProvider.cpp create mode 100644 services/core/jni/com_android_server_power_PowerManagerService.cpp create mode 100644 services/core/jni/com_android_server_power_PowerManagerService.h create mode 100644 services/core/jni/onload.cpp create mode 100644 services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java create mode 100644 services/devicepolicy/java/service.mk delete mode 100644 services/input/Android.mk delete mode 100644 services/input/EventHub.cpp delete mode 100644 services/input/EventHub.h delete mode 100644 services/input/InputApplication.cpp delete mode 100644 services/input/InputApplication.h delete mode 100644 services/input/InputDispatcher.cpp delete mode 100644 services/input/InputDispatcher.h delete mode 100644 services/input/InputListener.cpp delete mode 100644 services/input/InputListener.h delete mode 100644 services/input/InputManager.cpp delete mode 100644 services/input/InputManager.h delete mode 100644 services/input/InputReader.cpp delete mode 100644 services/input/InputReader.h delete mode 100644 services/input/InputWindow.cpp delete mode 100644 services/input/InputWindow.h delete mode 100644 services/input/PointerController.cpp delete mode 100644 services/input/PointerController.h delete mode 100644 services/input/SpriteController.cpp delete mode 100644 services/input/SpriteController.h delete mode 100644 services/input/tests/Android.mk delete mode 100644 services/input/tests/InputDispatcher_test.cpp delete mode 100644 services/input/tests/InputReader_test.cpp delete mode 100644 services/java/Android.mk delete mode 100644 services/java/com/android/server/AlarmManagerService.java delete mode 100644 services/java/com/android/server/AppOpsService.java delete mode 100644 services/java/com/android/server/AssetAtlasService.java delete mode 100644 services/java/com/android/server/AttributeCache.java delete mode 100644 services/java/com/android/server/BatteryService.java delete mode 100644 services/java/com/android/server/BluetoothManagerService.java delete mode 100644 services/java/com/android/server/BootReceiver.java delete mode 100644 services/java/com/android/server/BrickReceiver.java delete mode 100644 services/java/com/android/server/CertBlacklister.java delete mode 100644 services/java/com/android/server/CommonTimeManagementService.java delete mode 100644 services/java/com/android/server/ConnectivityService.java delete mode 100644 services/java/com/android/server/ConsumerIrService.java delete mode 100644 services/java/com/android/server/CountryDetectorService.java delete mode 100644 services/java/com/android/server/DiskStatsService.java delete mode 100644 services/java/com/android/server/DockObserver.java delete mode 100644 services/java/com/android/server/DropBoxManagerService.java delete mode 100644 services/java/com/android/server/EntropyMixer.java delete mode 100644 services/java/com/android/server/EventLogTags.logtags delete mode 100644 services/java/com/android/server/FgThread.java delete mode 100644 services/java/com/android/server/INativeDaemonConnectorCallbacks.java delete mode 100644 services/java/com/android/server/IdleMaintenanceService.java delete mode 100644 services/java/com/android/server/InputMethodManagerService.java delete mode 100644 services/java/com/android/server/IntentResolver.java delete mode 100644 services/java/com/android/server/IoThread.java delete mode 100644 services/java/com/android/server/LocalServices.java delete mode 100644 services/java/com/android/server/LocationManagerService.java delete mode 100644 services/java/com/android/server/LockSettingsService.java delete mode 100644 services/java/com/android/server/MasterClearReceiver.java delete mode 100644 services/java/com/android/server/MountService.java delete mode 100644 services/java/com/android/server/NativeDaemonConnector.java delete mode 100644 services/java/com/android/server/NativeDaemonConnectorException.java delete mode 100644 services/java/com/android/server/NativeDaemonEvent.java delete mode 100644 services/java/com/android/server/NetworkManagementService.java delete mode 100644 services/java/com/android/server/NetworkTimeUpdateService.java delete mode 100644 services/java/com/android/server/NsdService.java delete mode 100644 services/java/com/android/server/RandomBlock.java delete mode 100644 services/java/com/android/server/RecognitionManagerService.java delete mode 100644 services/java/com/android/server/SamplingProfilerService.java delete mode 100644 services/java/com/android/server/SerialService.java delete mode 100644 services/java/com/android/server/ServiceWatcher.java delete mode 100644 services/java/com/android/server/ShutdownActivity.java delete mode 100644 services/java/com/android/server/SystemServer.java delete mode 100644 services/java/com/android/server/SystemService.java delete mode 100644 services/java/com/android/server/SystemServiceManager.java delete mode 100644 services/java/com/android/server/TelephonyRegistry.java delete mode 100644 services/java/com/android/server/TextServicesManagerService.java delete mode 100644 services/java/com/android/server/TwilightCalculator.java delete mode 100644 services/java/com/android/server/UiModeManagerService.java delete mode 100644 services/java/com/android/server/UiThread.java delete mode 100644 services/java/com/android/server/UpdateLockService.java delete mode 100644 services/java/com/android/server/VibratorService.java delete mode 100644 services/java/com/android/server/Watchdog.java delete mode 100644 services/java/com/android/server/WiredAccessoryManager.java delete mode 100644 services/java/com/android/server/accessibility/AccessibilityInputFilter.java delete mode 100644 services/java/com/android/server/accessibility/AccessibilityManagerService.java delete mode 100644 services/java/com/android/server/accessibility/EventStreamTransformation.java delete mode 100644 services/java/com/android/server/accessibility/GestureUtils.java delete mode 100644 services/java/com/android/server/accessibility/ScreenMagnifier.java delete mode 100644 services/java/com/android/server/accessibility/TouchExplorer.java delete mode 100644 services/java/com/android/server/accounts/AccountAuthenticatorCache.java delete mode 100644 services/java/com/android/server/accounts/AccountManagerService.java delete mode 100644 services/java/com/android/server/accounts/IAccountAuthenticatorCache.java delete mode 100644 services/java/com/android/server/am/ActiveServices.java delete mode 100644 services/java/com/android/server/am/ActivityManagerService.java delete mode 100644 services/java/com/android/server/am/ActivityRecord.java delete mode 100644 services/java/com/android/server/am/ActivityResult.java delete mode 100644 services/java/com/android/server/am/ActivityStack.java delete mode 100644 services/java/com/android/server/am/ActivityStackSupervisor.java delete mode 100644 services/java/com/android/server/am/AppBindRecord.java delete mode 100644 services/java/com/android/server/am/AppErrorDialog.java delete mode 100644 services/java/com/android/server/am/AppErrorResult.java delete mode 100644 services/java/com/android/server/am/AppNotRespondingDialog.java delete mode 100644 services/java/com/android/server/am/AppWaitingForDebuggerDialog.java delete mode 100644 services/java/com/android/server/am/BackupRecord.java delete mode 100644 services/java/com/android/server/am/BaseErrorDialog.java delete mode 100644 services/java/com/android/server/am/BatteryStatsService.java delete mode 100644 services/java/com/android/server/am/BroadcastFilter.java delete mode 100644 services/java/com/android/server/am/BroadcastQueue.java delete mode 100644 services/java/com/android/server/am/BroadcastRecord.java delete mode 100644 services/java/com/android/server/am/CompatModeDialog.java delete mode 100644 services/java/com/android/server/am/CompatModePackages.java delete mode 100644 services/java/com/android/server/am/ConnectionRecord.java delete mode 100644 services/java/com/android/server/am/ContentProviderConnection.java delete mode 100644 services/java/com/android/server/am/ContentProviderRecord.java delete mode 100644 services/java/com/android/server/am/CoreSettingsObserver.java delete mode 100644 services/java/com/android/server/am/EventLogTags.logtags delete mode 100644 services/java/com/android/server/am/FactoryErrorDialog.java delete mode 100644 services/java/com/android/server/am/IntentBindRecord.java delete mode 100644 services/java/com/android/server/am/LaunchWarningWindow.java delete mode 100644 services/java/com/android/server/am/NativeCrashListener.java delete mode 100644 services/java/com/android/server/am/PendingIntentRecord.java delete mode 100644 services/java/com/android/server/am/PendingThumbnailsRecord.java delete mode 100644 services/java/com/android/server/am/ProcessList.java delete mode 100644 services/java/com/android/server/am/ProcessMemInfo.java delete mode 100644 services/java/com/android/server/am/ProcessRecord.java delete mode 100644 services/java/com/android/server/am/ProcessStatsService.java delete mode 100644 services/java/com/android/server/am/ProviderMap.java delete mode 100644 services/java/com/android/server/am/ReceiverList.java delete mode 100644 services/java/com/android/server/am/ServiceRecord.java delete mode 100644 services/java/com/android/server/am/StrictModeViolationDialog.java delete mode 100644 services/java/com/android/server/am/TaskAccessInfo.java delete mode 100644 services/java/com/android/server/am/TaskRecord.java delete mode 100644 services/java/com/android/server/am/ThumbnailHolder.java delete mode 100644 services/java/com/android/server/am/UriPermission.java delete mode 100644 services/java/com/android/server/am/UriPermissionOwner.java delete mode 100644 services/java/com/android/server/am/UsageStatsService.java delete mode 100644 services/java/com/android/server/am/UserStartedState.java delete mode 100644 services/java/com/android/server/am/package.html delete mode 100644 services/java/com/android/server/appwidget/AppWidgetService.java delete mode 100644 services/java/com/android/server/appwidget/AppWidgetServiceImpl.java delete mode 100644 services/java/com/android/server/backup/BackupManagerService.java delete mode 100644 services/java/com/android/server/backup/PackageManagerBackupAgent.java delete mode 100644 services/java/com/android/server/backup/SystemBackupAgent.java delete mode 100644 services/java/com/android/server/clipboard/ClipboardService.java delete mode 100644 services/java/com/android/server/connectivity/DataConnectionStats.java delete mode 100644 services/java/com/android/server/connectivity/Nat464Xlat.java delete mode 100644 services/java/com/android/server/connectivity/PacManager.java delete mode 100644 services/java/com/android/server/connectivity/Tethering.java delete mode 100644 services/java/com/android/server/connectivity/Vpn.java delete mode 100644 services/java/com/android/server/content/ContentService.java delete mode 100644 services/java/com/android/server/content/SyncManager.java delete mode 100644 services/java/com/android/server/content/SyncOperation.java delete mode 100644 services/java/com/android/server/content/SyncQueue.java delete mode 100644 services/java/com/android/server/content/SyncStorageEngine.java delete mode 100644 services/java/com/android/server/devicepolicy/DevicePolicyManagerService.java delete mode 100644 services/java/com/android/server/display/DisplayAdapter.java delete mode 100644 services/java/com/android/server/display/DisplayDevice.java delete mode 100644 services/java/com/android/server/display/DisplayDeviceInfo.java delete mode 100644 services/java/com/android/server/display/DisplayManagerService.java delete mode 100644 services/java/com/android/server/display/DisplayTransactionListener.java delete mode 100644 services/java/com/android/server/display/DisplayViewport.java delete mode 100644 services/java/com/android/server/display/LocalDisplayAdapter.java delete mode 100644 services/java/com/android/server/display/LogicalDisplay.java delete mode 100644 services/java/com/android/server/display/OverlayDisplayAdapter.java delete mode 100644 services/java/com/android/server/display/OverlayDisplayWindow.java delete mode 100644 services/java/com/android/server/display/PersistentDataStore.java delete mode 100644 services/java/com/android/server/display/VirtualDisplayAdapter.java delete mode 100644 services/java/com/android/server/display/WifiDisplayAdapter.java delete mode 100644 services/java/com/android/server/display/WifiDisplayController.java delete mode 100644 services/java/com/android/server/dreams/DreamController.java delete mode 100644 services/java/com/android/server/dreams/DreamManagerService.java delete mode 100644 services/java/com/android/server/firewall/AndFilter.java delete mode 100644 services/java/com/android/server/firewall/CategoryFilter.java delete mode 100644 services/java/com/android/server/firewall/Filter.java delete mode 100644 services/java/com/android/server/firewall/FilterFactory.java delete mode 100644 services/java/com/android/server/firewall/FilterList.java delete mode 100644 services/java/com/android/server/firewall/IntentFirewall.java delete mode 100644 services/java/com/android/server/firewall/NotFilter.java delete mode 100644 services/java/com/android/server/firewall/OrFilter.java delete mode 100644 services/java/com/android/server/firewall/PortFilter.java delete mode 100644 services/java/com/android/server/firewall/SenderFilter.java delete mode 100644 services/java/com/android/server/firewall/SenderPermissionFilter.java delete mode 100644 services/java/com/android/server/firewall/StringFilter.java delete mode 100644 services/java/com/android/server/input/InputApplicationHandle.java delete mode 100644 services/java/com/android/server/input/InputManagerService.java delete mode 100644 services/java/com/android/server/input/InputWindowHandle.java delete mode 100644 services/java/com/android/server/input/PersistentDataStore.java delete mode 100644 services/java/com/android/server/lights/Light.java delete mode 100644 services/java/com/android/server/lights/LightsManager.java delete mode 100644 services/java/com/android/server/lights/LightsService.java delete mode 100644 services/java/com/android/server/location/ComprehensiveCountryDetector.java delete mode 100644 services/java/com/android/server/location/CountryDetectorBase.java delete mode 100644 services/java/com/android/server/location/FlpHardwareProvider.java delete mode 100644 services/java/com/android/server/location/FusedLocationHardwareSecure.java delete mode 100644 services/java/com/android/server/location/FusedProxy.java delete mode 100644 services/java/com/android/server/location/GeocoderProxy.java delete mode 100644 services/java/com/android/server/location/GeofenceManager.java delete mode 100644 services/java/com/android/server/location/GeofenceProxy.java delete mode 100644 services/java/com/android/server/location/GeofenceState.java delete mode 100644 services/java/com/android/server/location/GpsLocationProvider.java delete mode 100644 services/java/com/android/server/location/GpsXtraDownloader.java delete mode 100644 services/java/com/android/server/location/LocationBasedCountryDetector.java delete mode 100644 services/java/com/android/server/location/LocationBlacklist.java delete mode 100644 services/java/com/android/server/location/LocationFudger.java delete mode 100644 services/java/com/android/server/location/LocationProviderInterface.java delete mode 100644 services/java/com/android/server/location/LocationProviderProxy.java delete mode 100644 services/java/com/android/server/location/MockProvider.java delete mode 100644 services/java/com/android/server/location/PassiveProvider.java delete mode 100644 services/java/com/android/server/media/MediaRouterService.java delete mode 100644 services/java/com/android/server/media/RemoteDisplayProviderProxy.java delete mode 100644 services/java/com/android/server/media/RemoteDisplayProviderWatcher.java delete mode 100644 services/java/com/android/server/net/LockdownVpnTracker.java delete mode 100644 services/java/com/android/server/net/NetworkIdentitySet.java delete mode 100644 services/java/com/android/server/net/NetworkPolicyManagerService.java delete mode 100644 services/java/com/android/server/net/NetworkStatsCollection.java delete mode 100644 services/java/com/android/server/net/NetworkStatsRecorder.java delete mode 100644 services/java/com/android/server/net/NetworkStatsService.java delete mode 100644 services/java/com/android/server/notification/NotificationDelegate.java delete mode 100644 services/java/com/android/server/notification/NotificationManagerInternal.java delete mode 100644 services/java/com/android/server/notification/NotificationManagerService.java delete mode 100644 services/java/com/android/server/os/SchedulingPolicyService.java delete mode 100644 services/java/com/android/server/pm/BasePermission.java delete mode 100644 services/java/com/android/server/pm/GrantedPermissions.java delete mode 100644 services/java/com/android/server/pm/Installer.java delete mode 100644 services/java/com/android/server/pm/KeySetManager.java delete mode 100644 services/java/com/android/server/pm/PackageKeySetData.java delete mode 100755 services/java/com/android/server/pm/PackageManagerService.java delete mode 100644 services/java/com/android/server/pm/PackageSetting.java delete mode 100644 services/java/com/android/server/pm/PackageSettingBase.java delete mode 100644 services/java/com/android/server/pm/PackageSignatures.java delete mode 100644 services/java/com/android/server/pm/PackageVerificationResponse.java delete mode 100644 services/java/com/android/server/pm/PackageVerificationState.java delete mode 100644 services/java/com/android/server/pm/PendingPackage.java delete mode 100644 services/java/com/android/server/pm/PreferredActivity.java delete mode 100644 services/java/com/android/server/pm/PreferredComponent.java delete mode 100644 services/java/com/android/server/pm/PreferredIntentResolver.java delete mode 100644 services/java/com/android/server/pm/SELinuxMMAC.java delete mode 100644 services/java/com/android/server/pm/Settings.java delete mode 100644 services/java/com/android/server/pm/SharedUserSetting.java delete mode 100644 services/java/com/android/server/pm/UserManagerService.java delete mode 100644 services/java/com/android/server/power/DisplayBlanker.java delete mode 100644 services/java/com/android/server/power/DisplayPowerController.java delete mode 100644 services/java/com/android/server/power/DisplayPowerRequest.java delete mode 100644 services/java/com/android/server/power/DisplayPowerState.java delete mode 100644 services/java/com/android/server/power/ElectronBeam.java delete mode 100644 services/java/com/android/server/power/Notifier.java delete mode 100644 services/java/com/android/server/power/PowerManagerService.java delete mode 100644 services/java/com/android/server/power/RampAnimator.java delete mode 100644 services/java/com/android/server/power/ScreenOnBlocker.java delete mode 100644 services/java/com/android/server/power/ShutdownThread.java delete mode 100644 services/java/com/android/server/power/SuspendBlocker.java delete mode 100644 services/java/com/android/server/power/WirelessChargerDetector.java delete mode 100644 services/java/com/android/server/print/PrintManagerService.java delete mode 100644 services/java/com/android/server/print/RemotePrintService.java delete mode 100644 services/java/com/android/server/print/RemotePrintSpooler.java delete mode 100644 services/java/com/android/server/print/UserState.java delete mode 100644 services/java/com/android/server/search/SearchManagerService.java delete mode 100644 services/java/com/android/server/search/Searchables.java delete mode 100644 services/java/com/android/server/statusbar/StatusBarManagerInternal.java delete mode 100644 services/java/com/android/server/statusbar/StatusBarManagerService.java delete mode 100644 services/java/com/android/server/storage/DeviceStorageMonitorInternal.java delete mode 100644 services/java/com/android/server/storage/DeviceStorageMonitorService.java delete mode 100644 services/java/com/android/server/twilight/TwilightListener.java delete mode 100644 services/java/com/android/server/twilight/TwilightManager.java delete mode 100644 services/java/com/android/server/twilight/TwilightService.java delete mode 100644 services/java/com/android/server/twilight/TwilightState.java delete mode 100644 services/java/com/android/server/updates/CarrierProvisioningUrlsInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/CertPinInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/ConfigUpdateInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/IntentFirewallInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/SELinuxPolicyInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/SmsShortCodesInstallReceiver.java delete mode 100644 services/java/com/android/server/updates/TZInfoInstallReceiver.java delete mode 100644 services/java/com/android/server/usb/UsbDebuggingManager.java delete mode 100644 services/java/com/android/server/usb/UsbDeviceManager.java delete mode 100644 services/java/com/android/server/usb/UsbHostManager.java delete mode 100644 services/java/com/android/server/usb/UsbService.java delete mode 100644 services/java/com/android/server/usb/UsbSettingsManager.java delete mode 100644 services/java/com/android/server/wallpaper/WallpaperManagerService.java delete mode 100644 services/java/com/android/server/wifi/README.txt delete mode 100644 services/java/com/android/server/wifi/WifiController.java delete mode 100644 services/java/com/android/server/wifi/WifiNotificationController.java delete mode 100644 services/java/com/android/server/wifi/WifiService.java delete mode 100644 services/java/com/android/server/wifi/WifiSettingsStore.java delete mode 100644 services/java/com/android/server/wifi/WifiTrafficPoller.java delete mode 100644 services/java/com/android/server/wm/AppTransition.java delete mode 100644 services/java/com/android/server/wm/AppWindowAnimator.java delete mode 100644 services/java/com/android/server/wm/AppWindowToken.java delete mode 100644 services/java/com/android/server/wm/BlackFrame.java delete mode 100644 services/java/com/android/server/wm/DimLayer.java delete mode 100644 services/java/com/android/server/wm/DisplayContent.java delete mode 100644 services/java/com/android/server/wm/DisplayMagnifier.java delete mode 100644 services/java/com/android/server/wm/DisplaySettings.java delete mode 100644 services/java/com/android/server/wm/DragState.java delete mode 100644 services/java/com/android/server/wm/FakeWindowImpl.java delete mode 100644 services/java/com/android/server/wm/FocusedStackFrame.java delete mode 100644 services/java/com/android/server/wm/InputMonitor.java delete mode 100644 services/java/com/android/server/wm/KeyguardDisableHandler.java delete mode 100644 services/java/com/android/server/wm/PointerEventDispatcher.java delete mode 100644 services/java/com/android/server/wm/ScreenRotationAnimation.java delete mode 100644 services/java/com/android/server/wm/Session.java delete mode 100644 services/java/com/android/server/wm/StackTapPointerEventListener.java delete mode 100644 services/java/com/android/server/wm/StartingData.java delete mode 100644 services/java/com/android/server/wm/StrictModeFlash.java delete mode 100644 services/java/com/android/server/wm/Task.java delete mode 100644 services/java/com/android/server/wm/TaskGroup.java delete mode 100644 services/java/com/android/server/wm/TaskStack.java delete mode 100644 services/java/com/android/server/wm/ViewServer.java delete mode 100644 services/java/com/android/server/wm/Watermark.java delete mode 100644 services/java/com/android/server/wm/WindowAnimator.java delete mode 100644 services/java/com/android/server/wm/WindowManagerService.java delete mode 100644 services/java/com/android/server/wm/WindowState.java delete mode 100644 services/java/com/android/server/wm/WindowStateAnimator.java delete mode 100644 services/java/com/android/server/wm/WindowToken.java delete mode 100644 services/jni/Android.mk delete mode 100644 services/jni/com_android_server_AlarmManagerService.cpp delete mode 100644 services/jni/com_android_server_AssetAtlasService.cpp delete mode 100644 services/jni/com_android_server_ConsumerIrService.cpp delete mode 100644 services/jni/com_android_server_SerialService.cpp delete mode 100644 services/jni/com_android_server_SystemServer.cpp delete mode 100644 services/jni/com_android_server_UsbDeviceManager.cpp delete mode 100644 services/jni/com_android_server_UsbHostManager.cpp delete mode 100644 services/jni/com_android_server_VibratorService.cpp delete mode 100644 services/jni/com_android_server_connectivity_Vpn.cpp delete mode 100644 services/jni/com_android_server_input_InputApplicationHandle.cpp delete mode 100644 services/jni/com_android_server_input_InputApplicationHandle.h delete mode 100644 services/jni/com_android_server_input_InputManagerService.cpp delete mode 100644 services/jni/com_android_server_input_InputWindowHandle.cpp delete mode 100644 services/jni/com_android_server_input_InputWindowHandle.h delete mode 100644 services/jni/com_android_server_lights_LightsService.cpp delete mode 100644 services/jni/com_android_server_location_FlpHardwareProvider.cpp delete mode 100644 services/jni/com_android_server_location_GpsLocationProvider.cpp delete mode 100644 services/jni/com_android_server_power_PowerManagerService.cpp delete mode 100644 services/jni/com_android_server_power_PowerManagerService.h delete mode 100644 services/jni/onload.cpp create mode 100644 services/print/java/com/android/server/print/PrintManagerService.java create mode 100644 services/print/java/com/android/server/print/RemotePrintService.java create mode 100644 services/print/java/com/android/server/print/RemotePrintSpooler.java create mode 100644 services/print/java/com/android/server/print/UserState.java create mode 100644 services/print/java/service.mk (limited to 'services') diff --git a/services/Android.mk b/services/Android.mk new file mode 100644 index 0000000..6ee7fca --- /dev/null +++ b/services/Android.mk @@ -0,0 +1,45 @@ +LOCAL_PATH:= $(call my-dir) + +# the java library +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := + +# TODO: Move this to the product makefiles +REQUIRED_SERVICES := core appwidget backup devicepolicy accessibility print + +include $(patsubst %,$(LOCAL_PATH)/%/java/service.mk,$(REQUIRED_SERVICES)) + +LOCAL_MODULE:= services + +LOCAL_JAVA_LIBRARIES := android.policy conscrypt telephony-common + +#LOCAL_PROGUARD_ENABLED := full +#LOCAL_PROGUARD_FLAG_FILES := proguard.flags + +include $(BUILD_JAVA_LIBRARY) + +include $(BUILD_DROIDDOC) + +# native library +# ============================================================= + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := +LOCAL_SHARED_LIBRARIES := + +# include all the jni subdirs to collect their sources +include $(wildcard $(LOCAL_PATH)/*/jni/Android.mk) + +LOCAL_CFLAGS += -DEGL_EGLEXT_PROTOTYPES -DGL_GLEXT_PROTOTYPES + +ifeq ($(WITH_MALLOC_LEAK_CHECK),true) + LOCAL_CFLAGS += -DMALLOC_LEAK_CHECK +endif + +LOCAL_MODULE:= libandroid_servers + +include $(BUILD_SHARED_LIBRARY) + diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java new file mode 100644 index 0000000..9e893da --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Pools.SimplePool; +import android.util.Slog; +import android.view.Choreographer; +import android.view.Display; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputFilter; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; + +/** + * This class is an input filter for implementing accessibility features such + * as display magnification and explore by touch. + * + * NOTE: This class has to be created and poked only from the main thread. + */ +class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation { + + private static final String TAG = AccessibilityInputFilter.class.getSimpleName(); + + private static final boolean DEBUG = false; + + /** + * Flag for enabling the screen magnification feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001; + + /** + * Flag for enabling the touch exploration feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002; + + /** + * Flag for enabling the filtering key events feature. + * + * @see #setEnabledFeatures(int) + */ + static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004; + + private final Runnable mProcessBatchedEventsRunnable = new Runnable() { + @Override + public void run() { + final long frameTimeNanos = mChoreographer.getFrameTimeNanos(); + if (DEBUG) { + Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos); + } + processBatchedEvents(frameTimeNanos); + if (DEBUG) { + Slog.i(TAG, "End batch processing."); + } + if (mEventQueue != null) { + scheduleProcessBatchedEvents(); + } + } + }; + + private final Context mContext; + + private final PowerManager mPm; + + private final AccessibilityManagerService mAms; + + private final Choreographer mChoreographer; + + private int mCurrentTouchDeviceId; + + private boolean mInstalled; + + private int mEnabledFeatures; + + private TouchExplorer mTouchExplorer; + + private ScreenMagnifier mScreenMagnifier; + + private EventStreamTransformation mEventHandler; + + private MotionEventHolder mEventQueue; + + private boolean mMotionEventSequenceStarted; + + private boolean mHoverEventSequenceStarted; + + private boolean mKeyEventSequenceStarted; + + private boolean mFilterKeyEvents; + + AccessibilityInputFilter(Context context, AccessibilityManagerService service) { + super(context.getMainLooper()); + mContext = context; + mAms = service; + mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mChoreographer = Choreographer.getInstance(); + } + + @Override + public void onInstalled() { + if (DEBUG) { + Slog.d(TAG, "Accessibility input filter installed."); + } + mInstalled = true; + disableFeatures(); + enableFeatures(); + super.onInstalled(); + } + + @Override + public void onUninstalled() { + if (DEBUG) { + Slog.d(TAG, "Accessibility input filter uninstalled."); + } + mInstalled = false; + disableFeatures(); + super.onUninstalled(); + } + + @Override + public void onInputEvent(InputEvent event, int policyFlags) { + if (DEBUG) { + Slog.d(TAG, "Received event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + } + if (event instanceof MotionEvent + && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { + MotionEvent motionEvent = (MotionEvent) event; + onMotionEvent(motionEvent, policyFlags); + } else if (event instanceof KeyEvent + && event.isFromSource(InputDevice.SOURCE_KEYBOARD)) { + KeyEvent keyEvent = (KeyEvent) event; + onKeyEvent(keyEvent, policyFlags); + } else { + super.onInputEvent(event, policyFlags); + } + } + + private void onMotionEvent(MotionEvent event, int policyFlags) { + if (mEventHandler == null) { + super.onInputEvent(event, policyFlags); + return; + } + if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mEventHandler.clear(); + super.onInputEvent(event, policyFlags); + return; + } + final int deviceId = event.getDeviceId(); + if (mCurrentTouchDeviceId != deviceId) { + mCurrentTouchDeviceId = deviceId; + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mEventHandler.clear(); + } + if (mCurrentTouchDeviceId < 0) { + super.onInputEvent(event, policyFlags); + return; + } + // We do not handle scroll events. + if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { + super.onInputEvent(event, policyFlags); + return; + } + // Wait for a down touch event to start processing. + if (event.isTouchEvent()) { + if (!mMotionEventSequenceStarted) { + if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { + return; + } + mMotionEventSequenceStarted = true; + } + } else { + // Wait for an enter hover event to start processing. + if (!mHoverEventSequenceStarted) { + if (event.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) { + return; + } + mHoverEventSequenceStarted = true; + } + } + batchMotionEvent((MotionEvent) event, policyFlags); + } + + private void onKeyEvent(KeyEvent event, int policyFlags) { + if (!mFilterKeyEvents) { + super.onInputEvent(event, policyFlags); + return; + } + if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) { + mKeyEventSequenceStarted = false; + super.onInputEvent(event, policyFlags); + return; + } + // Wait for a down key event to start processing. + if (!mKeyEventSequenceStarted) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + mKeyEventSequenceStarted = true; + } + mAms.notifyKeyEvent(event, policyFlags); + } + + private void scheduleProcessBatchedEvents() { + mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, + mProcessBatchedEventsRunnable, null); + } + + private void batchMotionEvent(MotionEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags); + } + if (mEventQueue == null) { + mEventQueue = MotionEventHolder.obtain(event, policyFlags); + scheduleProcessBatchedEvents(); + return; + } + if (mEventQueue.event.addBatch(event)) { + return; + } + MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags); + holder.next = mEventQueue; + mEventQueue.previous = holder; + mEventQueue = holder; + } + + private void processBatchedEvents(long frameNanos) { + MotionEventHolder current = mEventQueue; + while (current.next != null) { + current = current.next; + } + while (true) { + if (current == null) { + mEventQueue = null; + break; + } + if (current.event.getEventTimeNano() >= frameNanos) { + // Finished with this choreographer frame. Do the rest on the next one. + current.next = null; + break; + } + handleMotionEvent(current.event, current.policyFlags); + MotionEventHolder prior = current; + current = current.previous; + prior.recycle(); + } + } + + private void handleMotionEvent(MotionEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags); + } + // Since we do batch processing it is possible that by the time the + // next batch is processed the event handle had been set to null. + if (mEventHandler != null) { + mPm.userActivity(event.getEventTime(), false); + MotionEvent transformedEvent = MotionEvent.obtain(event); + mEventHandler.onMotionEvent(transformedEvent, event, policyFlags); + transformedEvent.recycle(); + } + } + + @Override + public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent, + int policyFlags) { + sendInputEvent(transformedEvent, policyFlags); + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + // TODO Implement this to inject the accessibility event + // into the accessibility manager service similarly + // to how this is done for input events. + } + + @Override + public void setNext(EventStreamTransformation sink) { + /* do nothing */ + } + + @Override + public void clear() { + /* do nothing */ + } + + void setEnabledFeatures(int enabledFeatures) { + if (mEnabledFeatures == enabledFeatures) { + return; + } + if (mInstalled) { + disableFeatures(); + } + mEnabledFeatures = enabledFeatures; + if (mInstalled) { + enableFeatures(); + } + } + + void notifyAccessibilityEvent(AccessibilityEvent event) { + if (mEventHandler != null) { + mEventHandler.onAccessibilityEvent(event); + } + } + + private void enableFeatures() { + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + if ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) { + mEventHandler = mScreenMagnifier = new ScreenMagnifier(mContext, + Display.DEFAULT_DISPLAY, mAms); + mEventHandler.setNext(this); + } + if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) { + mTouchExplorer = new TouchExplorer(mContext, mAms); + mTouchExplorer.setNext(this); + if (mEventHandler != null) { + mEventHandler.setNext(mTouchExplorer); + } else { + mEventHandler = mTouchExplorer; + } + } + if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) { + mFilterKeyEvents = true; + } + } + + void disableFeatures() { + if (mTouchExplorer != null) { + mTouchExplorer.clear(); + mTouchExplorer.onDestroy(); + mTouchExplorer = null; + } + if (mScreenMagnifier != null) { + mScreenMagnifier.clear(); + mScreenMagnifier.onDestroy(); + mScreenMagnifier = null; + } + mEventHandler = null; + mKeyEventSequenceStarted = false; + mMotionEventSequenceStarted = false; + mHoverEventSequenceStarted = false; + mFilterKeyEvents = false; + } + + @Override + public void onDestroy() { + /* ignore */ + } + + private static class MotionEventHolder { + private static final int MAX_POOL_SIZE = 32; + private static final SimplePool sPool = + new SimplePool(MAX_POOL_SIZE); + + public int policyFlags; + public MotionEvent event; + public MotionEventHolder next; + public MotionEventHolder previous; + + public static MotionEventHolder obtain(MotionEvent event, int policyFlags) { + MotionEventHolder holder = sPool.acquire(); + if (holder == null) { + holder = new MotionEventHolder(); + } + holder.event = MotionEvent.obtain(event); + holder.policyFlags = policyFlags; + return holder; + } + + public void recycle() { + event.recycle(); + event = null; + policyFlags = 0; + next = null; + previous = null; + sPool.release(this); + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java new file mode 100644 index 0000000..ccac0d3 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -0,0 +1,3131 @@ +/* + ** Copyright 2009, The Android Open Source Project + ** + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** + ** http://www.apache.org/licenses/LICENSE-2.0 + ** + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + */ + +package com.android.server.accessibility; + +import static android.accessibilityservice.AccessibilityServiceInfo.DEFAULT; + +import android.Manifest; +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceClient; +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.database.ContentObserver; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.util.Pools.Pool; +import android.util.Pools.SimplePool; +import android.util.Slog; +import android.util.SparseArray; +import android.view.Display; +import android.view.IWindow; +import android.view.IWindowManager; +import android.view.InputDevice; +import android.view.InputEventConsistencyVerifier; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MagnificationSpec; +import android.view.WindowManager; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityInteractionClient; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.IAccessibilityManager; +import android.view.accessibility.IAccessibilityManagerClient; + +import com.android.internal.R; +import com.android.internal.content.PackageMonitor; +import com.android.internal.statusbar.IStatusBarService; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This class is instantiated by the system as a system level service and can be + * accessed only by the system. The task of this service is to be a centralized + * event dispatch for {@link AccessibilityEvent}s generated across all processes + * on the device. Events are dispatched to {@link AccessibilityService}s. + * + * @hide + */ +public class AccessibilityManagerService extends IAccessibilityManager.Stub { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "AccessibilityManagerService"; + + // TODO: This is arbitrary. When there is time implement this by watching + // when that accessibility services are bound. + private static final int WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS = 3000; + + private static final String FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE = + "registerUiTestAutomationService"; + + private static final String TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED = + "temporaryEnableAccessibilityStateUntilKeyguardRemoved"; + + private static final ComponentName sFakeAccessibilityServiceComponentName = + new ComponentName("foo.bar", "FakeService"); + + private static final String FUNCTION_DUMP = "dump"; + + private static final char COMPONENT_NAME_SEPARATOR = ':'; + + private static final int OWN_PROCESS_ID = android.os.Process.myPid(); + + private static final int MAX_POOL_SIZE = 10; + + private static int sIdCounter = 0; + + private static int sNextWindowId; + + private final Context mContext; + + private final Object mLock = new Object(); + + private final Pool mPendingEventPool = + new SimplePool(MAX_POOL_SIZE); + + private final SimpleStringSplitter mStringColonSplitter = + new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); + + private final List mEnabledServicesForFeedbackTempList = + new ArrayList(); + + private final Rect mTempRect = new Rect(); + + private final Point mTempPoint = new Point(); + + private final Display mDefaultDisplay; + + private final PackageManager mPackageManager; + + private final IWindowManager mWindowManagerService; + + private final SecurityPolicy mSecurityPolicy; + + private final MainHandler mMainHandler; + + private Service mQueryBridge; + + private AlertDialog mEnableTouchExplorationDialog; + + private AccessibilityInputFilter mInputFilter; + + private boolean mHasInputFilter; + + private final Set mTempComponentNameSet = new HashSet(); + + private final List mTempAccessibilityServiceInfoList = + new ArrayList(); + + private final RemoteCallbackList mGlobalClients = + new RemoteCallbackList(); + + private final SparseArray mGlobalInteractionConnections = + new SparseArray(); + + private final SparseArray mGlobalWindowTokens = new SparseArray(); + + private final SparseArray mUserStates = new SparseArray(); + + private int mCurrentUserId = UserHandle.USER_OWNER; + + //TODO: Remove this hack + private boolean mInitialized; + + private UserState getCurrentUserStateLocked() { + return getUserStateLocked(mCurrentUserId); + } + + private UserState getUserStateLocked(int userId) { + UserState state = mUserStates.get(userId); + if (state == null) { + state = new UserState(userId); + mUserStates.put(userId, state); + } + return state; + } + + /** + * Creates a new instance. + * + * @param context A {@link Context} instance. + */ + public AccessibilityManagerService(Context context) { + mContext = context; + mPackageManager = mContext.getPackageManager(); + mWindowManagerService = (IWindowManager) ServiceManager.getService(Context.WINDOW_SERVICE); + mSecurityPolicy = new SecurityPolicy(); + mMainHandler = new MainHandler(mContext.getMainLooper()); + //TODO: (multi-display) We need to support multiple displays. + DisplayManager displayManager = (DisplayManager) + mContext.getSystemService(Context.DISPLAY_SERVICE); + mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + registerBroadcastReceivers(); + new AccessibilityContentObserver(mMainHandler).register( + context.getContentResolver()); + } + + private void registerBroadcastReceivers() { + PackageMonitor monitor = new PackageMonitor() { + @Override + public void onSomePackagesChanged() { + synchronized (mLock) { + if (getChangingUserId() != mCurrentUserId) { + return; + } + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + // We have to reload the installed services since some services may + // have different attributes, resolve info (does not support equals), + // etc. Remove them then to force reload. Do it even if automation is + // running since when it goes away, we will have to reload as well. + userState.mInstalledServices.clear(); + if (userState.mUiAutomationService == null) { + if (readConfigurationForUserStateLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + synchronized (mLock) { + final int userId = getChangingUserId(); + if (userId != mCurrentUserId) { + return; + } + UserState userState = getUserStateLocked(userId); + Iterator it = userState.mEnabledServices.iterator(); + while (it.hasNext()) { + ComponentName comp = it.next(); + String compPkg = comp.getPackageName(); + if (compPkg.equals(packageName)) { + it.remove(); + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, userId); + // Update the touch exploration granted services setting. + userState.mTouchExplorationGrantedServices.remove(comp); + persistComponentNamesToSettingLocked( + Settings.Secure. + TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, userId); + // We will update when the automation service dies. + if (userState.mUiAutomationService == null) { + onUserStateChangedLocked(userState); + } + return; + } + } + } + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, + int uid, boolean doit) { + synchronized (mLock) { + final int userId = getChangingUserId(); + if (userId != mCurrentUserId) { + return false; + } + UserState userState = getUserStateLocked(userId); + Iterator it = userState.mEnabledServices.iterator(); + while (it.hasNext()) { + ComponentName comp = it.next(); + String compPkg = comp.getPackageName(); + for (String pkg : packages) { + if (compPkg.equals(pkg)) { + if (!doit) { + return true; + } + it.remove(); + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, userId); + // We will update when the automation service dies. + if (userState.mUiAutomationService == null) { + onUserStateChangedLocked(userState); + } + } + } + } + return false; + } + } + }; + + // package changes + monitor.register(mContext, null, UserHandle.ALL, true); + + // user change and unlock + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + intentFilter.addAction(Intent.ACTION_USER_PRESENT); + + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_SWITCHED.equals(action)) { + switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readConfigurationForUserStateLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } + }, UserHandle.ALL, intentFilter, null, null); + } + + public int addClient(IAccessibilityManagerClient client, int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // If the client is from a process that runs across users such as + // the system UI or the system we add it to the global state that + // is shared across users. + UserState userState = getUserStateLocked(resolvedUserId); + if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) { + mGlobalClients.register(client); + if (DEBUG) { + Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid()); + } + return userState.getClientState(); + } else { + userState.mClients.register(client); + // If this client is not for the current user we do not + // return a state since it is not for the foreground user. + // We will send the state to the client on a user switch. + if (DEBUG) { + Slog.i(LOG_TAG, "Added user client for pid:" + Binder.getCallingPid() + + " and userId:" + mCurrentUserId); + } + return (resolvedUserId == mCurrentUserId) ? userState.getClientState() : 0; + } + } + } + + public boolean sendAccessibilityEvent(AccessibilityEvent event, int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // This method does nothing for a background user. + if (resolvedUserId != mCurrentUserId) { + return true; // yes, recycle the event + } + if (mSecurityPolicy.canDispatchAccessibilityEvent(event)) { + mSecurityPolicy.updateEventSourceLocked(event); + mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_ACTIVE_WINDOW, + event.getWindowId(), event.getEventType()).sendToTarget(); + notifyAccessibilityServicesDelayedLocked(event, false); + notifyAccessibilityServicesDelayedLocked(event, true); + } + if (mHasInputFilter && mInputFilter != null) { + mMainHandler.obtainMessage(MainHandler.MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER, + AccessibilityEvent.obtain(event)).sendToTarget(); + } + event.recycle(); + getUserStateLocked(resolvedUserId).mHandledFeedbackTypes = 0; + } + return (OWN_PROCESS_ID != Binder.getCallingPid()); + } + + public List getInstalledAccessibilityServiceList(int userId) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // The automation service is a fake one and should not be reported + // to clients as being installed - it really is not. + UserState userState = getUserStateLocked(resolvedUserId); + if (userState.mUiAutomationService != null) { + List installedServices = + new ArrayList(); + installedServices.addAll(userState.mInstalledServices); + installedServices.remove(userState.mUiAutomationService); + return installedServices; + } + return userState.mInstalledServices; + } + } + + public List getEnabledAccessibilityServiceList(int feedbackType, + int userId) { + List result = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + + // The automation service is a fake one and should not be reported + // to clients as being enabled. The automation service is always the + // only active one, if it exists. + UserState userState = getUserStateLocked(resolvedUserId); + if (userState.mUiAutomationService != null) { + return Collections.emptyList(); + } + + result = mEnabledServicesForFeedbackTempList; + result.clear(); + List services = userState.mBoundServices; + while (feedbackType != 0) { + final int feedbackTypeBit = (1 << Integer.numberOfTrailingZeros(feedbackType)); + feedbackType &= ~feedbackTypeBit; + final int serviceCount = services.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = services.get(i); + if ((service.mFeedbackType & feedbackTypeBit) != 0) { + result.add(service.mAccessibilityServiceInfo); + } + } + } + } + return result; + } + + public void interrupt(int userId) { + CopyOnWriteArrayList services; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + // This method does nothing for a background user. + if (resolvedUserId != mCurrentUserId) { + return; + } + services = getUserStateLocked(resolvedUserId).mBoundServices; + } + for (int i = 0, count = services.size(); i < count; i++) { + Service service = services.get(i); + try { + service.mServiceInterface.onInterrupt(); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending interrupt request to " + + service.mService, re); + } + } + } + + public int addAccessibilityInteractionConnection(IWindow windowToken, + IAccessibilityInteractionConnection connection, int userId) throws RemoteException { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked(userId); + final int windowId = sNextWindowId++; + // If the window is from a process that runs across users such as + // the system UI or the system we add it to the global state that + // is shared across users. + if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) { + AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper( + windowId, connection, UserHandle.USER_ALL); + wrapper.linkToDeath(); + mGlobalInteractionConnections.put(windowId, wrapper); + mGlobalWindowTokens.put(windowId, windowToken.asBinder()); + if (DEBUG) { + Slog.i(LOG_TAG, "Added global connection for pid:" + Binder.getCallingPid() + + " with windowId: " + windowId); + } + } else { + AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper( + windowId, connection, resolvedUserId); + wrapper.linkToDeath(); + UserState userState = getUserStateLocked(resolvedUserId); + userState.mInteractionConnections.put(windowId, wrapper); + userState.mWindowTokens.put(windowId, windowToken.asBinder()); + if (DEBUG) { + Slog.i(LOG_TAG, "Added user connection for pid:" + Binder.getCallingPid() + + " with windowId: " + windowId + " and userId:" + mCurrentUserId); + } + } + if (DEBUG) { + Slog.i(LOG_TAG, "Adding interaction connection to windowId: " + windowId); + } + return windowId; + } + } + + public void removeAccessibilityInteractionConnection(IWindow window) { + synchronized (mLock) { + mSecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + IBinder token = window.asBinder(); + final int removedWindowId = removeAccessibilityInteractionConnectionInternalLocked( + token, mGlobalWindowTokens, mGlobalInteractionConnections); + if (removedWindowId >= 0) { + if (DEBUG) { + Slog.i(LOG_TAG, "Removed global connection for pid:" + Binder.getCallingPid() + + " with windowId: " + removedWindowId); + } + return; + } + final int userCount = mUserStates.size(); + for (int i = 0; i < userCount; i++) { + UserState userState = mUserStates.valueAt(i); + final int removedWindowIdForUser = + removeAccessibilityInteractionConnectionInternalLocked( + token, userState.mWindowTokens, userState.mInteractionConnections); + if (removedWindowIdForUser >= 0) { + if (DEBUG) { + Slog.i(LOG_TAG, "Removed user connection for pid:" + Binder.getCallingPid() + + " with windowId: " + removedWindowIdForUser + " and userId:" + + mUserStates.keyAt(i)); + } + return; + } + } + } + } + + private int removeAccessibilityInteractionConnectionInternalLocked(IBinder windowToken, + SparseArray windowTokens, + SparseArray interactionConnections) { + final int count = windowTokens.size(); + for (int i = 0; i < count; i++) { + if (windowTokens.valueAt(i) == windowToken) { + final int windowId = windowTokens.keyAt(i); + windowTokens.removeAt(i); + AccessibilityConnectionWrapper wrapper = interactionConnections.get(windowId); + wrapper.unlinkToDeath(); + interactionConnections.remove(windowId); + return windowId; + } + } + return -1; + } + + public void registerUiTestAutomationService(IBinder owner, + IAccessibilityServiceClient serviceClient, + AccessibilityServiceInfo accessibilityServiceInfo) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.RETRIEVE_WINDOW_CONTENT, + FUNCTION_REGISTER_UI_TEST_AUTOMATION_SERVICE); + + accessibilityServiceInfo.setComponentName(sFakeAccessibilityServiceComponentName); + + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + + if (userState.mUiAutomationService != null) { + throw new IllegalStateException("UiAutomationService " + serviceClient + + "already registered!"); + } + + try { + owner.linkToDeath(userState.mUiAutomationSerivceOnwerDeathRecipient, 0); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Couldn't register for the death of a" + + " UiTestAutomationService!", re); + return; + } + + userState.mUiAutomationServiceOwner = owner; + userState.mUiAutomationServiceClient = serviceClient; + + // Set the temporary state. + userState.mIsAccessibilityEnabled = true; + userState.mIsTouchExplorationEnabled = false; + userState.mIsEnhancedWebAccessibilityEnabled = false; + userState.mIsDisplayMagnificationEnabled = false; + userState.mInstalledServices.add(accessibilityServiceInfo); + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(sFakeAccessibilityServiceComponentName); + userState.mTouchExplorationGrantedServices.add(sFakeAccessibilityServiceComponentName); + + // Use the new state instead of settings. + onUserStateChangedLocked(userState); + } + } + + public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) { + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + // Automation service is not bound, so pretend it died to perform clean up. + if (userState.mUiAutomationService != null + && serviceClient != null + && userState.mUiAutomationService != null + && userState.mUiAutomationService.mServiceInterface != null + && userState.mUiAutomationService.mServiceInterface.asBinder() + == serviceClient.asBinder()) { + userState.mUiAutomationService.binderDied(); + } else { + throw new IllegalStateException("UiAutomationService " + serviceClient + + " not registered!"); + } + } + } + + public void temporaryEnableAccessibilityStateUntilKeyguardRemoved( + ComponentName service, boolean touchExplorationEnabled) { + mSecurityPolicy.enforceCallingPermission( + Manifest.permission.TEMPORARY_ENABLE_ACCESSIBILITY, + TEMPORARY_ENABLE_ACCESSIBILITY_UNTIL_KEYGUARD_REMOVED); + try { + if (!mWindowManagerService.isKeyguardLocked()) { + return; + } + } catch (RemoteException re) { + return; + } + synchronized (mLock) { + // Set the temporary state. + UserState userState = getCurrentUserStateLocked(); + + // This is a nop if UI automation is enabled. + if (userState.mUiAutomationService != null) { + return; + } + + userState.mIsAccessibilityEnabled = true; + userState.mIsTouchExplorationEnabled = touchExplorationEnabled; + userState.mIsEnhancedWebAccessibilityEnabled = false; + userState.mIsDisplayMagnificationEnabled = false; + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(service); + userState.mBindingServices.clear(); + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.add(service); + + // User the current state instead settings. + onUserStateChangedLocked(userState); + } + } + + boolean onGesture(int gestureId) { + synchronized (mLock) { + boolean handled = notifyGestureLocked(gestureId, false); + if (!handled) { + handled = notifyGestureLocked(gestureId, true); + } + return handled; + } + } + + boolean notifyKeyEvent(KeyEvent event, int policyFlags) { + synchronized (mLock) { + KeyEvent localClone = KeyEvent.obtain(event); + boolean handled = notifyKeyEventLocked(localClone, policyFlags, false); + if (!handled) { + handled = notifyKeyEventLocked(localClone, policyFlags, true); + } + return handled; + } + } + + /** + * Gets the bounds of the accessibility focus in the active window. + * + * @param outBounds The output to which to write the focus bounds. + * @return Whether accessibility focus was found and the bounds are populated. + */ + // TODO: (multi-display) Make sure this works for multiple displays. + boolean getAccessibilityFocusBoundsInActiveWindow(Rect outBounds) { + // Instead of keeping track of accessibility focus events per + // window to be able to find the focus in the active window, + // we take a stateless approach and look it up. This is fine + // since we do this only when the user clicks/long presses. + Service service = getQueryBridge(); + final int connectionId = service.mId; + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + client.addConnection(connectionId, service); + try { + AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() + .getRootInActiveWindow(connectionId); + if (root == null) { + return false; + } + AccessibilityNodeInfo focus = root.findFocus( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); + if (focus == null) { + return false; + } + focus.getBoundsInScreen(outBounds); + + MagnificationSpec spec = service.getCompatibleMagnificationSpec(focus.getWindowId()); + if (spec != null && !spec.isNop()) { + outBounds.offset((int) -spec.offsetX, (int) -spec.offsetY); + outBounds.scale(1 / spec.scale); + } + + // Clip to the window rectangle. + Rect windowBounds = mTempRect; + getActiveWindowBounds(windowBounds); + outBounds.intersect(windowBounds); + // Clip to the screen rectangle. + mDefaultDisplay.getRealSize(mTempPoint); + outBounds.intersect(0, 0, mTempPoint.x, mTempPoint.y); + + return true; + } finally { + client.removeConnection(connectionId); + } + } + + /** + * Gets the bounds of the active window. + * + * @param outBounds The output to which to write the bounds. + */ + boolean getActiveWindowBounds(Rect outBounds) { + IBinder token; + synchronized (mLock) { + final int windowId = mSecurityPolicy.mActiveWindowId; + token = mGlobalWindowTokens.get(windowId); + if (token == null) { + token = getCurrentUserStateLocked().mWindowTokens.get(windowId); + } + } + try { + mWindowManagerService.getWindowFrame(token, outBounds); + if (!outBounds.isEmpty()) { + return true; + } + } catch (RemoteException re) { + /* ignore */ + } + return false; + } + + int getActiveWindowId() { + return mSecurityPolicy.mActiveWindowId; + } + + void onTouchInteractionStart() { + mSecurityPolicy.onTouchInteractionStart(); + } + + void onTouchInteractionEnd() { + mSecurityPolicy.onTouchInteractionEnd(); + } + + void onMagnificationStateChanged() { + notifyClearAccessibilityNodeInfoCacheLocked(); + } + + private void switchUser(int userId) { + synchronized (mLock) { + if (mCurrentUserId == userId && mInitialized) { + return; + } + + // Disconnect from services for the old user. + UserState oldUserState = getUserStateLocked(mCurrentUserId); + oldUserState.onSwitchToAnotherUser(); + + // Disable the local managers for the old user. + if (oldUserState.mClients.getRegisteredCallbackCount() > 0) { + mMainHandler.obtainMessage(MainHandler.MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER, + oldUserState.mUserId, 0).sendToTarget(); + } + + // Announce user changes only if more that one exist. + UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + final boolean announceNewUser = userManager.getUsers().size() > 1; + + // The user changed. + mCurrentUserId = userId; + + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService != null) { + // Switching users disables the UI automation service. + userState.mUiAutomationService.binderDied(); + } + + readConfigurationForUserStateLocked(userState); + // Even if reading did not yield change, we have to update + // the state since the context in which the current user + // state was used has changed since it was inactive. + onUserStateChangedLocked(userState); + + if (announceNewUser) { + // Schedule announcement of the current user if needed. + mMainHandler.sendEmptyMessageDelayed(MainHandler.MSG_ANNOUNCE_NEW_USER_IF_NEEDED, + WAIT_FOR_USER_STATE_FULLY_INITIALIZED_MILLIS); + } + } + } + + private void removeUser(int userId) { + synchronized (mLock) { + mUserStates.remove(userId); + } + } + + private Service getQueryBridge() { + if (mQueryBridge == null) { + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT); + mQueryBridge = new Service(UserHandle.USER_NULL, + sFakeAccessibilityServiceComponentName, info); + } + return mQueryBridge; + } + + private boolean notifyGestureLocked(int gestureId, boolean isDefault) { + // TODO: Now we are giving the gestures to the last enabled + // service that can handle them which is the last one + // in our list since we write the last enabled as the + // last record in the enabled services setting. Ideally, + // the user should make the call which service handles + // gestures. However, only one service should handle + // gestures to avoid user frustration when different + // behavior is observed from different combinations of + // enabled accessibility services. + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + if (service.mRequestTouchExplorationMode && service.mIsDefault == isDefault) { + service.notifyGesture(gestureId); + return true; + } + } + return false; + } + + private boolean notifyKeyEventLocked(KeyEvent event, int policyFlags, boolean isDefault) { + // TODO: Now we are giving the key events to the last enabled + // service that can handle them Ideally, the user should + // make the call which service handles key events. However, + // only one service should handle key events to avoid user + // frustration when different behavior is observed from + // different combinations of enabled accessibility services. + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + // Key events are handled only by services that declared + // this capability and requested to filter key events. + if (!service.mRequestFilterKeyEvents || + (service.mAccessibilityServiceInfo.getCapabilities() & AccessibilityServiceInfo + .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) == 0) { + continue; + } + if (service.mIsDefault == isDefault) { + service.notifyKeyEvent(event, policyFlags); + return true; + } + } + return false; + } + + private void notifyClearAccessibilityNodeInfoCacheLocked() { + UserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + Service service = state.mBoundServices.get(i); + service.notifyClearAccessibilityNodeInfoCache(); + } + } + + /** + * Removes an AccessibilityInteractionConnection. + * + * @param windowId The id of the window to which the connection is targeted. + * @param userId The id of the user owning the connection. UserHandle.USER_ALL + * if global. + */ + private void removeAccessibilityInteractionConnectionLocked(int windowId, int userId) { + if (userId == UserHandle.USER_ALL) { + mGlobalWindowTokens.remove(windowId); + mGlobalInteractionConnections.remove(windowId); + } else { + UserState userState = getCurrentUserStateLocked(); + userState.mWindowTokens.remove(windowId); + userState.mInteractionConnections.remove(windowId); + } + if (DEBUG) { + Slog.i(LOG_TAG, "Removing interaction connection to windowId: " + windowId); + } + } + + private boolean readInstalledAccessibilityServiceLocked(UserState userState) { + mTempAccessibilityServiceInfoList.clear(); + + List installedServices = mPackageManager.queryIntentServicesAsUser( + new Intent(AccessibilityService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + mCurrentUserId); + + for (int i = 0, count = installedServices.size(); i < count; i++) { + ResolveInfo resolveInfo = installedServices.get(i); + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals( + serviceInfo.permission)) { + Slog.w(LOG_TAG, "Skipping accessibilty service " + new ComponentName( + serviceInfo.packageName, serviceInfo.name).flattenToShortString() + + ": it does not require the permission " + + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE); + continue; + } + AccessibilityServiceInfo accessibilityServiceInfo; + try { + accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext); + mTempAccessibilityServiceInfoList.add(accessibilityServiceInfo); + } catch (XmlPullParserException xppe) { + Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", xppe); + } catch (IOException ioe) { + Slog.e(LOG_TAG, "Error while initializing AccessibilityServiceInfo", ioe); + } + } + + if (!mTempAccessibilityServiceInfoList.equals(userState.mInstalledServices)) { + userState.mInstalledServices.clear(); + userState.mInstalledServices.addAll(mTempAccessibilityServiceInfoList); + mTempAccessibilityServiceInfoList.clear(); + return true; + } + + mTempAccessibilityServiceInfoList.clear(); + return false; + } + + private boolean readEnabledAccessibilityServicesLocked(UserState userState) { + mTempComponentNameSet.clear(); + readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mUserId, mTempComponentNameSet); + if (!mTempComponentNameSet.equals(userState.mEnabledServices)) { + userState.mEnabledServices.clear(); + userState.mEnabledServices.addAll(mTempComponentNameSet); + mTempComponentNameSet.clear(); + return true; + } + mTempComponentNameSet.clear(); + return false; + } + + private boolean readTouchExplorationGrantedAccessibilityServicesLocked( + UserState userState) { + mTempComponentNameSet.clear(); + readComponentNamesFromSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mUserId, mTempComponentNameSet); + if (!mTempComponentNameSet.equals(userState.mTouchExplorationGrantedServices)) { + userState.mTouchExplorationGrantedServices.clear(); + userState.mTouchExplorationGrantedServices.addAll(mTempComponentNameSet); + mTempComponentNameSet.clear(); + return true; + } + mTempComponentNameSet.clear(); + return false; + } + + /** + * Performs {@link AccessibilityService}s delayed notification. The delay is configurable + * and denotes the period after the last event before notifying the service. + * + * @param event The event. + * @param isDefault True to notify default listeners, not default services. + */ + private void notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, + boolean isDefault) { + try { + UserState state = getCurrentUserStateLocked(); + for (int i = 0, count = state.mBoundServices.size(); i < count; i++) { + Service service = state.mBoundServices.get(i); + + if (service.mIsDefault == isDefault) { + if (canDispathEventLocked(service, event, state.mHandledFeedbackTypes)) { + state.mHandledFeedbackTypes |= service.mFeedbackType; + service.notifyAccessibilityEvent(event); + } + } + } + } catch (IndexOutOfBoundsException oobe) { + // An out of bounds exception can happen if services are going away + // as the for loop is running. If that happens, just bail because + // there are no more services to notify. + return; + } + } + + private void addServiceLocked(Service service, UserState userState) { + try { + service.linkToOwnDeathLocked(); + userState.mBoundServices.add(service); + userState.mComponentNameToServiceMap.put(service.mComponentName, service); + } catch (RemoteException re) { + /* do nothing */ + } + } + + /** + * Removes a service. + * + * @param service The service. + * @return True if the service was removed, false otherwise. + */ + private void removeServiceLocked(Service service, UserState userState) { + userState.mBoundServices.remove(service); + userState.mComponentNameToServiceMap.remove(service.mComponentName); + service.unlinkToOwnDeathLocked(); + } + + /** + * Determines if given event can be dispatched to a service based on the package of the + * event source and already notified services for that event type. Specifically, a + * service is notified if it is interested in events from the package and no other service + * providing the same feedback type has been notified. Exception are services the + * provide generic feedback (feedback type left as a safety net for unforeseen feedback + * types) which are always notified. + * + * @param service The potential receiver. + * @param event The event. + * @param handledFeedbackTypes The feedback types for which services have been notified. + * @return True if the listener should be notified, false otherwise. + */ + private boolean canDispathEventLocked(Service service, AccessibilityEvent event, + int handledFeedbackTypes) { + + if (!service.canReceiveEventsLocked()) { + return false; + } + + if (!event.isImportantForAccessibility() + && (service.mFetchFlags + & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) == 0) { + return false; + } + + int eventType = event.getEventType(); + if ((service.mEventTypes & eventType) != eventType) { + return false; + } + + Set packageNames = service.mPackageNames; + CharSequence packageName = event.getPackageName(); + + if (packageNames.isEmpty() || packageNames.contains(packageName)) { + int feedbackType = service.mFeedbackType; + if ((handledFeedbackTypes & feedbackType) != feedbackType + || feedbackType == AccessibilityServiceInfo.FEEDBACK_GENERIC) { + return true; + } + } + + return false; + } + + private void unbindAllServicesLocked(UserState userState) { + List services = userState.mBoundServices; + for (int i = 0, count = services.size(); i < count; i++) { + Service service = services.get(i); + if (service.unbindLocked()) { + i--; + count--; + } + } + } + + /** + * Populates a set with the {@link ComponentName}s stored in a colon + * separated value setting for a given user. + * + * @param settingName The setting to parse. + * @param userId The user id. + * @param outComponentNames The output component names. + */ + private void readComponentNamesFromSettingLocked(String settingName, int userId, + Set outComponentNames) { + String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(), + settingName, userId); + outComponentNames.clear(); + if (settingValue != null) { + TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(settingValue); + while (splitter.hasNext()) { + String str = splitter.next(); + if (str == null || str.length() <= 0) { + continue; + } + ComponentName enabledService = ComponentName.unflattenFromString(str); + if (enabledService != null) { + outComponentNames.add(enabledService); + } + } + } + } + + /** + * Persists the component names in the specified setting in a + * colon separated fashion. + * + * @param settingName The setting name. + * @param componentNames The component names. + */ + private void persistComponentNamesToSettingLocked(String settingName, + Set componentNames, int userId) { + StringBuilder builder = new StringBuilder(); + for (ComponentName componentName : componentNames) { + if (builder.length() > 0) { + builder.append(COMPONENT_NAME_SEPARATOR); + } + builder.append(componentName.flattenToShortString()); + } + Settings.Secure.putStringForUser(mContext.getContentResolver(), + settingName, builder.toString(), userId); + } + + private void manageServicesLocked(UserState userState) { + Map componentNameToServiceMap = + userState.mComponentNameToServiceMap; + boolean isEnabled = userState.mIsAccessibilityEnabled; + + for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { + AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); + ComponentName componentName = ComponentName.unflattenFromString( + installedService.getId()); + Service service = componentNameToServiceMap.get(componentName); + + if (isEnabled) { + // Wait for the binding if it is in process. + if (userState.mBindingServices.contains(componentName)) { + continue; + } + if (userState.mEnabledServices.contains(componentName)) { + if (service == null) { + service = new Service(userState.mUserId, componentName, installedService); + } else if (userState.mBoundServices.contains(service)) { + continue; + } + service.bindLocked(); + } else { + if (service != null) { + service.unbindLocked(); + } + } + } else { + if (service != null) { + service.unbindLocked(); + } else { + userState.mBindingServices.remove(componentName); + } + } + } + + // No enabled installed services => disable accessibility to avoid + // sending accessibility events with no recipient across processes. + if (isEnabled && userState.mEnabledServices.isEmpty()) { + userState.mIsAccessibilityEnabled = false; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId); + } + } + + private void scheduleUpdateClientsIfNeededLocked(UserState userState) { + final int clientState = userState.getClientState(); + if (userState.mLastSentClientState != clientState + && (mGlobalClients.getRegisteredCallbackCount() > 0 + || userState.mClients.getRegisteredCallbackCount() > 0)) { + userState.mLastSentClientState = clientState; + mMainHandler.obtainMessage(MainHandler.MSG_SEND_STATE_TO_CLIENTS, + clientState, userState.mUserId) .sendToTarget(); + } + } + + private void scheduleUpdateInputFilter(UserState userState) { + mMainHandler.obtainMessage(MainHandler.MSG_UPDATE_INPUT_FILTER, userState).sendToTarget(); + } + + private void updateInputFilter(UserState userState) { + boolean setInputFilter = false; + AccessibilityInputFilter inputFilter = null; + synchronized (mLock) { + int flags = 0; + if (userState.mIsDisplayMagnificationEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER; + } + // Touch exploration without accessibility makes no sense. + if (userState.mIsAccessibilityEnabled && userState.mIsTouchExplorationEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION; + } + if (userState.mIsFilterKeyEventsEnabled) { + flags |= AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS; + } + if (flags != 0) { + if (!mHasInputFilter) { + mHasInputFilter = true; + if (mInputFilter == null) { + mInputFilter = new AccessibilityInputFilter(mContext, + AccessibilityManagerService.this); + } + inputFilter = mInputFilter; + setInputFilter = true; + } + mInputFilter.setEnabledFeatures(flags); + } else { + if (mHasInputFilter) { + mHasInputFilter = false; + mInputFilter.disableFeatures(); + inputFilter = null; + setInputFilter = true; + } + } + } + if (setInputFilter) { + try { + mWindowManagerService.setInputFilter(inputFilter); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + private void showEnableTouchExplorationDialog(final Service service) { + synchronized (mLock) { + String label = service.mResolveInfo.loadLabel( + mContext.getPackageManager()).toString(); + + final UserState state = getCurrentUserStateLocked(); + if (state.mIsTouchExplorationEnabled) { + return; + } + if (mEnableTouchExplorationDialog != null + && mEnableTouchExplorationDialog.isShowing()) { + return; + } + mEnableTouchExplorationDialog = new AlertDialog.Builder(mContext) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setPositiveButton(android.R.string.ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The user allowed the service to toggle touch exploration. + state.mTouchExplorationGrantedServices.add(service.mComponentName); + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + state.mTouchExplorationGrantedServices, state.mUserId); + // Enable touch exploration. + UserState userState = getUserStateLocked(service.mUserId); + userState.mIsTouchExplorationEnabled = true; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 1, + service.mUserId); + onUserStateChangedLocked(userState); + } + }) + .setNegativeButton(android.R.string.cancel, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setTitle(R.string.enable_explore_by_touch_warning_title) + .setMessage(mContext.getString( + R.string.enable_explore_by_touch_warning_message, label)) + .create(); + mEnableTouchExplorationDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + mEnableTouchExplorationDialog.getWindow().getAttributes().privateFlags + |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + mEnableTouchExplorationDialog.setCanceledOnTouchOutside(true); + mEnableTouchExplorationDialog.show(); + } + } + + private void onUserStateChangedLocked(UserState userState) { + // TODO: Remove this hack + mInitialized = true; + updateLegacyCapabilities(userState); + updateServicesLocked(userState); + updateFilterKeyEventsLocked(userState); + updateTouchExplorationLocked(userState); + updateEnhancedWebAccessibilityLocked(userState); + scheduleUpdateInputFilter(userState); + scheduleUpdateClientsIfNeededLocked(userState); + } + + private void updateLegacyCapabilities(UserState userState) { + // Up to JB-MR1 we had a white list with services that can enable touch + // exploration. When a service is first started we show a dialog to the + // use to get a permission to white list the service. + final int installedServiceCount = userState.mInstalledServices.size(); + for (int i = 0; i < installedServiceCount; i++) { + AccessibilityServiceInfo serviceInfo = userState.mInstalledServices.get(i); + ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); + if ((serviceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) == 0 + && resolveInfo.serviceInfo.applicationInfo.targetSdkVersion + <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + ComponentName componentName = new ComponentName( + resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + if (userState.mTouchExplorationGrantedServices.contains(componentName)) { + serviceInfo.setCapabilities(serviceInfo.getCapabilities() + | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION); + } + } + } + } + + private void updateFilterKeyEventsLocked(UserState userState) { + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (service.mRequestFilterKeyEvents + && (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo + .CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS) != 0) { + userState.mIsFilterKeyEventsEnabled = true; + return; + } + } + userState.mIsFilterKeyEventsEnabled = false; + } + + private void updateServicesLocked(UserState userState) { + if (userState.mIsAccessibilityEnabled) { + manageServicesLocked(userState); + } else { + unbindAllServicesLocked(userState); + } + } + + private boolean readConfigurationForUserStateLocked(UserState userState) { + boolean somthingChanged = false; + somthingChanged |= readAccessibilityEnabledSettingLocked(userState); + somthingChanged |= readInstalledAccessibilityServiceLocked(userState); + somthingChanged |= readEnabledAccessibilityServicesLocked(userState); + somthingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState); + somthingChanged |= readTouchExplorationEnabledSettingLocked(userState); + somthingChanged |= readEnhancedWebAccessibilityEnabledChangedLocked(userState); + somthingChanged |= readDisplayMagnificationEnabledSettingLocked(userState); + return somthingChanged; + } + + private boolean readAccessibilityEnabledSettingLocked(UserState userState) { + final boolean accessibilityEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0, userState.mUserId) == 1; + if (accessibilityEnabled != userState.mIsAccessibilityEnabled) { + userState.mIsAccessibilityEnabled = accessibilityEnabled; + return true; + } + return false; + } + + private boolean readTouchExplorationEnabledSettingLocked(UserState userState) { + final boolean touchExplorationEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0, userState.mUserId) == 1; + if (touchExplorationEnabled != userState.mIsTouchExplorationEnabled) { + userState.mIsTouchExplorationEnabled = touchExplorationEnabled; + return true; + } + return false; + } + + private boolean readDisplayMagnificationEnabledSettingLocked(UserState userState) { + final boolean displayMagnificationEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, + 0, userState.mUserId) == 1; + if (displayMagnificationEnabled != userState.mIsDisplayMagnificationEnabled) { + userState.mIsDisplayMagnificationEnabled = displayMagnificationEnabled; + return true; + } + return false; + } + + private boolean readEnhancedWebAccessibilityEnabledChangedLocked(UserState userState) { + final boolean enhancedWeAccessibilityEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + 0, userState.mUserId) == 1; + if (enhancedWeAccessibilityEnabled != userState.mIsEnhancedWebAccessibilityEnabled) { + userState.mIsEnhancedWebAccessibilityEnabled = enhancedWeAccessibilityEnabled; + return true; + } + return false; + } + + private void updateTouchExplorationLocked(UserState userState) { + boolean enabled = false; + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (canRequestAndRequestsTouchExplorationLocked(service)) { + enabled = true; + break; + } + } + if (enabled != userState.mIsTouchExplorationEnabled) { + userState.mIsTouchExplorationEnabled = enabled; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.TOUCH_EXPLORATION_ENABLED, enabled ? 1 : 0, + userState.mUserId); + } + try { + mWindowManagerService.setTouchExplorationEnabled(enabled); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private boolean canRequestAndRequestsTouchExplorationLocked(Service service) { + // Service not ready or cannot request the feature - well nothing to do. + if (!service.canReceiveEventsLocked() || !service.mRequestTouchExplorationMode) { + return false; + } + // UI test automation service can always enable it. + if (service.mIsAutomation) { + return true; + } + if (service.mResolveInfo.serviceInfo.applicationInfo.targetSdkVersion + <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Up to JB-MR1 we had a white list with services that can enable touch + // exploration. When a service is first started we show a dialog to the + // use to get a permission to white list the service. + UserState userState = getUserStateLocked(service.mUserId); + if (userState.mTouchExplorationGrantedServices.contains(service.mComponentName)) { + return true; + } else if (mEnableTouchExplorationDialog == null + || !mEnableTouchExplorationDialog.isShowing()) { + mMainHandler.obtainMessage( + MainHandler.MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG, + service).sendToTarget(); + } + } else { + // Starting in JB-MR2 we request an accessibility service to declare + // certain capabilities in its meta-data to allow it to enable the + // corresponding features. + if ((service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION) != 0) { + return true; + } + } + return false; + } + + private void updateEnhancedWebAccessibilityLocked(UserState userState) { + boolean enabled = false; + final int serviceCount = userState.mBoundServices.size(); + for (int i = 0; i < serviceCount; i++) { + Service service = userState.mBoundServices.get(i); + if (canRequestAndRequestsEnhancedWebAccessibilityLocked(service)) { + enabled = true; + break; + } + } + if (enabled != userState.mIsEnhancedWebAccessibilityEnabled) { + userState.mIsEnhancedWebAccessibilityEnabled = enabled; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, enabled ? 1 : 0, + userState.mUserId); + } + } + + private boolean canRequestAndRequestsEnhancedWebAccessibilityLocked(Service service) { + if (!service.canReceiveEventsLocked() || !service.mRequestEnhancedWebAccessibility ) { + return false; + } + if (service.mIsAutomation || (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0) { + return true; + } + return false; + } + + @Override + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP); + synchronized (mLock) { + pw.println("ACCESSIBILITY MANAGER (dumpsys accessibility)"); + pw.println(); + final int userCount = mUserStates.size(); + for (int i = 0; i < userCount; i++) { + UserState userState = mUserStates.valueAt(i); + pw.append("User state[attributes:{id=" + userState.mUserId); + pw.append(", currentUser=" + (userState.mUserId == mCurrentUserId)); + pw.append(", accessibilityEnabled=" + userState.mIsAccessibilityEnabled); + pw.append(", touchExplorationEnabled=" + userState.mIsTouchExplorationEnabled); + pw.append(", displayMagnificationEnabled=" + + userState.mIsDisplayMagnificationEnabled); + if (userState.mUiAutomationService != null) { + pw.append(", "); + userState.mUiAutomationService.dump(fd, pw, args); + pw.println(); + } + pw.append("}"); + pw.println(); + pw.append(" services:{"); + final int serviceCount = userState.mBoundServices.size(); + for (int j = 0; j < serviceCount; j++) { + if (j > 0) { + pw.append(", "); + pw.println(); + pw.append(" "); + } + Service service = userState.mBoundServices.get(j); + service.dump(fd, pw, args); + } + pw.println("}]"); + pw.println(); + } + } + } + + private class AccessibilityConnectionWrapper implements DeathRecipient { + private final int mWindowId; + private final int mUserId; + private final IAccessibilityInteractionConnection mConnection; + + public AccessibilityConnectionWrapper(int windowId, + IAccessibilityInteractionConnection connection, int userId) { + mWindowId = windowId; + mUserId = userId; + mConnection = connection; + } + + public void linkToDeath() throws RemoteException { + mConnection.asBinder().linkToDeath(this, 0); + } + + public void unlinkToDeath() { + mConnection.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + unlinkToDeath(); + synchronized (mLock) { + removeAccessibilityInteractionConnectionLocked(mWindowId, mUserId); + } + } + } + + private final class MainHandler extends Handler { + public static final int MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER = 1; + public static final int MSG_SEND_STATE_TO_CLIENTS = 2; + public static final int MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER = 3; + public static final int MSG_UPDATE_ACTIVE_WINDOW = 4; + public static final int MSG_ANNOUNCE_NEW_USER_IF_NEEDED = 5; + public static final int MSG_UPDATE_INPUT_FILTER = 6; + public static final int MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG = 7; + public static final int MSG_SEND_KEY_EVENT_TO_INPUT_FILTER = 8; + + public MainHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final int type = msg.what; + switch (type) { + case MSG_SEND_ACCESSIBILITY_EVENT_TO_INPUT_FILTER: { + AccessibilityEvent event = (AccessibilityEvent) msg.obj; + synchronized (mLock) { + if (mHasInputFilter && mInputFilter != null) { + mInputFilter.notifyAccessibilityEvent(event); + } + } + event.recycle(); + } break; + case MSG_SEND_KEY_EVENT_TO_INPUT_FILTER: { + KeyEvent event = (KeyEvent) msg.obj; + final int policyFlags = msg.arg1; + synchronized (mLock) { + if (mHasInputFilter && mInputFilter != null) { + mInputFilter.sendInputEvent(event, policyFlags); + } + } + event.recycle(); + } break; + case MSG_SEND_STATE_TO_CLIENTS: { + final int clientState = msg.arg1; + final int userId = msg.arg2; + sendStateToClients(clientState, mGlobalClients); + sendStateToClientsForUser(clientState, userId); + } break; + case MSG_SEND_CLEARED_STATE_TO_CLIENTS_FOR_USER: { + final int userId = msg.arg1; + sendStateToClientsForUser(0, userId); + } break; + case MSG_UPDATE_ACTIVE_WINDOW: { + final int windowId = msg.arg1; + final int eventType = msg.arg2; + mSecurityPolicy.updateActiveWindow(windowId, eventType); + } break; + case MSG_ANNOUNCE_NEW_USER_IF_NEEDED: { + announceNewUserIfNeeded(); + } break; + case MSG_UPDATE_INPUT_FILTER: { + UserState userState = (UserState) msg.obj; + updateInputFilter(userState); + } break; + case MSG_SHOW_ENABLED_TOUCH_EXPLORATION_DIALOG: { + Service service = (Service) msg.obj; + showEnableTouchExplorationDialog(service); + } break; + } + } + + private void announceNewUserIfNeeded() { + synchronized (mLock) { + UserState userState = getCurrentUserStateLocked(); + if (userState.mIsAccessibilityEnabled) { + UserManager userManager = (UserManager) mContext.getSystemService( + Context.USER_SERVICE); + String message = mContext.getString(R.string.user_switched, + userManager.getUserInfo(mCurrentUserId).name); + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(message); + event.setWindowId(mSecurityPolicy.getRetrievalAllowingWindowLocked()); + sendAccessibilityEvent(event, mCurrentUserId); + } + } + } + + private void sendStateToClientsForUser(int clientState, int userId) { + final UserState userState; + synchronized (mLock) { + userState = getUserStateLocked(userId); + } + sendStateToClients(clientState, userState.mClients); + } + + private void sendStateToClients(int clientState, + RemoteCallbackList clients) { + try { + final int userClientCount = clients.beginBroadcast(); + for (int i = 0; i < userClientCount; i++) { + IAccessibilityManagerClient client = clients.getBroadcastItem(i); + try { + client.setState(clientState); + } catch (RemoteException re) { + /* ignore */ + } + } + } finally { + clients.finishBroadcast(); + } + } + } + + private PendingEvent obtainPendingEventLocked(KeyEvent event, int policyFlags, int sequence) { + PendingEvent pendingEvent = mPendingEventPool.acquire(); + if (pendingEvent == null) { + pendingEvent = new PendingEvent(); + } + pendingEvent.event = event; + pendingEvent.policyFlags = policyFlags; + pendingEvent.sequence = sequence; + return pendingEvent; + } + + private void recyclePendingEventLocked(PendingEvent pendingEvent) { + pendingEvent.clear(); + mPendingEventPool.release(pendingEvent); + } + + /** + * This class represents an accessibility service. It stores all per service + * data required for the service management, provides API for starting/stopping the + * service and is responsible for adding/removing the service in the data structures + * for service management. The class also exposes configuration interface that is + * passed to the service it represents as soon it is bound. It also serves as the + * connection for the service. + */ + class Service extends IAccessibilityServiceConnection.Stub + implements ServiceConnection, DeathRecipient {; + + final int mUserId; + + int mId = 0; + + AccessibilityServiceInfo mAccessibilityServiceInfo; + + IBinder mService; + + IAccessibilityServiceClient mServiceInterface; + + int mEventTypes; + + int mFeedbackType; + + Set mPackageNames = new HashSet(); + + boolean mIsDefault; + + boolean mRequestTouchExplorationMode; + + boolean mRequestEnhancedWebAccessibility; + + boolean mRequestFilterKeyEvents; + + int mFetchFlags; + + long mNotificationTimeout; + + ComponentName mComponentName; + + Intent mIntent; + + boolean mIsAutomation; + + final Rect mTempBounds = new Rect(); + + final ResolveInfo mResolveInfo; + + // the events pending events to be dispatched to this service + final SparseArray mPendingEvents = + new SparseArray(); + + final KeyEventDispatcher mKeyEventDispatcher = new KeyEventDispatcher(); + + boolean mWasConnectedAndDied; + + // Handler only for dispatching accessibility events since we use event + // types as message types allowing us to remove messages per event type. + public Handler mEventDispatchHandler = new Handler(mMainHandler.getLooper()) { + @Override + public void handleMessage(Message message) { + final int eventType = message.what; + notifyAccessibilityEventInternal(eventType); + } + }; + + // Handler for scheduling method invocations on the main thread. + public InvocationHandler mInvocationHandler = new InvocationHandler( + mMainHandler.getLooper()); + + public Service(int userId, ComponentName componentName, + AccessibilityServiceInfo accessibilityServiceInfo) { + mUserId = userId; + mResolveInfo = accessibilityServiceInfo.getResolveInfo(); + mId = sIdCounter++; + mComponentName = componentName; + mAccessibilityServiceInfo = accessibilityServiceInfo; + mIsAutomation = (sFakeAccessibilityServiceComponentName.equals(componentName)); + if (!mIsAutomation) { + mIntent = new Intent().setComponent(mComponentName); + mIntent.putExtra(Intent.EXTRA_CLIENT_LABEL, + com.android.internal.R.string.accessibility_binding_label); + mIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( + mContext, 0, new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 0)); + } + setDynamicallyConfigurableProperties(accessibilityServiceInfo); + } + + public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) { + mEventTypes = info.eventTypes; + mFeedbackType = info.feedbackType; + String[] packageNames = info.packageNames; + if (packageNames != null) { + mPackageNames.addAll(Arrays.asList(packageNames)); + } + mNotificationTimeout = info.notificationTimeout; + mIsDefault = (info.flags & DEFAULT) != 0; + + if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion + >= Build.VERSION_CODES.JELLY_BEAN) { + if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) { + mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + } else { + mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS; + } + } + + if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) { + mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS; + } else { + mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS; + } + + mRequestTouchExplorationMode = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0; + mRequestEnhancedWebAccessibility = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0; + mRequestFilterKeyEvents = (info.flags + & AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0; + } + + /** + * Binds to the accessibility service. + * + * @return True if binding is successful. + */ + public boolean bindLocked() { + UserState userState = getUserStateLocked(mUserId); + if (!mIsAutomation) { + if (mService == null && mContext.bindServiceAsUser( + mIntent, this, Context.BIND_AUTO_CREATE, new UserHandle(mUserId))) { + userState.mBindingServices.add(mComponentName); + } + } else { + userState.mBindingServices.add(mComponentName); + mService = userState.mUiAutomationServiceClient.asBinder(); + onServiceConnected(mComponentName, mService); + userState.mUiAutomationService = this; + } + return false; + } + + /** + * Unbinds form the accessibility service and removes it from the data + * structures for service management. + * + * @return True if unbinding is successful. + */ + public boolean unbindLocked() { + if (mService == null) { + return false; + } + UserState userState = getUserStateLocked(mUserId); + mKeyEventDispatcher.flush(); + if (!mIsAutomation) { + mContext.unbindService(this); + } else { + userState.destroyUiAutomationService(); + } + removeServiceLocked(this, userState); + resetLocked(); + return true; + } + + public boolean canReceiveEventsLocked() { + return (mEventTypes != 0 && mFeedbackType != 0 && mService != null); + } + + @Override + public void setOnKeyEventResult(boolean handled, int sequence) { + mKeyEventDispatcher.setOnKeyEventResult(handled, sequence); + } + + @Override + public AccessibilityServiceInfo getServiceInfo() { + synchronized (mLock) { + return mAccessibilityServiceInfo; + } + } + + @Override + public void setServiceInfo(AccessibilityServiceInfo info) { + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // If the XML manifest had data to configure the service its info + // should be already set. In such a case update only the dynamically + // configurable properties. + AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo; + if (oldInfo != null) { + oldInfo.updateDynamicallyConfigurableProperties(info); + setDynamicallyConfigurableProperties(oldInfo); + } else { + setDynamicallyConfigurableProperties(info); + } + UserState userState = getUserStateLocked(mUserId); + onUserStateChangedLocked(userState); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + synchronized (mLock) { + mService = service; + mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service); + UserState userState = getUserStateLocked(mUserId); + addServiceLocked(this, userState); + if (userState.mBindingServices.contains(mComponentName) || mWasConnectedAndDied) { + userState.mBindingServices.remove(mComponentName); + mWasConnectedAndDied = false; + try { + mServiceInterface.setConnection(this, mId); + onUserStateChangedLocked(userState); + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Error while setting connection for service: " + + service, re); + binderDied(); + } + } else { + binderDied(); + } + } + } + + @Override + public boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + long accessibilityNodeId, String viewIdResName, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + final boolean permissionGranted = mSecurityPolicy.canRetrieveWindowContent(this); + if (!permissionGranted) { + return false; + } else { + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId, + viewIdResName, interactionId, callback, mFetchFlags, interrogatingPid, + interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId()."); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findAccessibilityNodeInfosByText(int accessibilityWindowId, + long accessibilityNodeId, String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text, + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid, + spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findAccessibilityNodeInfoByAccessibilityId( + int accessibilityWindowId, long accessibilityNodeId, int interactionId, + IAccessibilityInteractionConnectionCallback callback, int flags, + long interrogatingTid) throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, + interactionId, callback, mFetchFlags | flags, interrogatingPid, + interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean findFocus(int accessibilityWindowId, long accessibilityNodeId, + int focusType, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.findFocus(accessibilityNodeId, focusType, interactionId, callback, + mFetchFlags, interrogatingPid, interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling findAccessibilityFocus()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId, + int direction, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = + mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId); + try { + connection.focusSearch(accessibilityNodeId, direction, interactionId, callback, + mFetchFlags, interrogatingPid, interrogatingTid, spec); + return true; + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling accessibilityFocusSearch()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return false; + } + + @Override + public boolean performAccessibilityAction(int accessibilityWindowId, + long accessibilityNodeId, int action, Bundle arguments, int interactionId, + IAccessibilityInteractionConnectionCallback callback, long interrogatingTid) + throws RemoteException { + final int resolvedWindowId; + IAccessibilityInteractionConnection connection = null; + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + mSecurityPolicy.enforceCanRetrieveWindowContent(this); + resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId); + final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this, + resolvedWindowId, action, arguments); + if (!permissionGranted) { + return false; + } else { + connection = getConnectionLocked(resolvedWindowId); + if (connection == null) { + return false; + } + } + } + final int interrogatingPid = Binder.getCallingPid(); + final long identityToken = Binder.clearCallingIdentity(); + try { + connection.performAccessibilityAction(accessibilityNodeId, action, arguments, + interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid); + } catch (RemoteException re) { + if (DEBUG) { + Slog.e(LOG_TAG, "Error calling performAccessibilityAction()"); + } + } finally { + Binder.restoreCallingIdentity(identityToken); + } + return true; + } + + public boolean performGlobalAction(int action) { + synchronized (mLock) { + final int resolvedUserId = mSecurityPolicy + .resolveCallingUserIdEnforcingPermissionsLocked( + UserHandle.getCallingUserId()); + if (resolvedUserId != mCurrentUserId) { + return false; + } + } + final long identity = Binder.clearCallingIdentity(); + try { + switch (action) { + case AccessibilityService.GLOBAL_ACTION_BACK: { + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK); + } return true; + case AccessibilityService.GLOBAL_ACTION_HOME: { + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME); + } return true; + case AccessibilityService.GLOBAL_ACTION_RECENTS: { + openRecents(); + } return true; + case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: { + expandNotifications(); + } return true; + case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: { + expandQuickSettings(); + } return true; + } + return false; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.DUMP, FUNCTION_DUMP); + synchronized (mLock) { + pw.append("Service[label=" + mAccessibilityServiceInfo.getResolveInfo() + .loadLabel(mContext.getPackageManager())); + pw.append(", feedbackType" + + AccessibilityServiceInfo.feedbackTypeToString(mFeedbackType)); + pw.append(", capabilities=" + mAccessibilityServiceInfo.getCapabilities()); + pw.append(", eventTypes=" + + AccessibilityEvent.eventTypeToString(mEventTypes)); + pw.append(", notificationTimeout=" + mNotificationTimeout); + pw.append("]"); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + /* do nothing - #binderDied takes care */ + } + + public void linkToOwnDeathLocked() throws RemoteException { + mService.linkToDeath(this, 0); + } + + public void unlinkToOwnDeathLocked() { + mService.unlinkToDeath(this, 0); + } + + public void resetLocked() { + try { + // Clear the proxy in the other process so this + // IAccessibilityServiceConnection can be garbage collected. + mServiceInterface.setConnection(null, mId); + } catch (RemoteException re) { + /* ignore */ + } + mService = null; + mServiceInterface = null; + } + + public boolean isConnectedLocked() { + return (mService != null); + } + + public void binderDied() { + synchronized (mLock) { + // It is possible that this service's package was force stopped during + // whose handling the death recipient is unlinked and still get a call + // on binderDied since the call was made before we unlink but was + // waiting on the lock we held during the force stop handling. + if (!isConnectedLocked()) { + return; + } + mWasConnectedAndDied = true; + mKeyEventDispatcher.flush(); + UserState userState = getUserStateLocked(mUserId); + // The death recipient is unregistered in removeServiceLocked + removeServiceLocked(this, userState); + resetLocked(); + if (mIsAutomation) { + // We no longer have an automation service, so restore + // the state based on values in the settings database. + userState.mInstalledServices.remove(mAccessibilityServiceInfo); + userState.mEnabledServices.remove(mComponentName); + userState.destroyUiAutomationService(); + } + } + } + + /** + * Performs a notification for an {@link AccessibilityEvent}. + * + * @param event The event. + */ + public void notifyAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + final int eventType = event.getEventType(); + // Make a copy since during dispatch it is possible the event to + // be modified to remove its source if the receiving service does + // not have permission to access the window content. + AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); + AccessibilityEvent oldEvent = mPendingEvents.get(eventType); + mPendingEvents.put(eventType, newEvent); + + final int what = eventType; + if (oldEvent != null) { + mEventDispatchHandler.removeMessages(what); + oldEvent.recycle(); + } + + Message message = mEventDispatchHandler.obtainMessage(what); + mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); + } + } + + /** + * Notifies an accessibility service client for a scheduled event given the event type. + * + * @param eventType The type of the event to dispatch. + */ + private void notifyAccessibilityEventInternal(int eventType) { + IAccessibilityServiceClient listener; + AccessibilityEvent event; + + synchronized (mLock) { + listener = mServiceInterface; + + // If the service died/was disabled while the message for dispatching + // the accessibility event was propagating the listener may be null. + if (listener == null) { + return; + } + + event = mPendingEvents.get(eventType); + + // Check for null here because there is a concurrent scenario in which this + // happens: 1) A binder thread calls notifyAccessibilityServiceDelayedLocked + // which posts a message for dispatching an event. 2) The message is pulled + // from the queue by the handler on the service thread and the latter is + // just about to acquire the lock and call this method. 3) Now another binder + // thread acquires the lock calling notifyAccessibilityServiceDelayedLocked + // so the service thread waits for the lock; 4) The binder thread replaces + // the event with a more recent one (assume the same event type) and posts a + // dispatch request releasing the lock. 5) Now the main thread is unblocked and + // dispatches the event which is removed from the pending ones. 6) And ... now + // the service thread handles the last message posted by the last binder call + // but the event is already dispatched and hence looking it up in the pending + // ones yields null. This check is much simpler that keeping count for each + // event type of each service to catch such a scenario since only one message + // is processed at a time. + if (event == null) { + return; + } + + mPendingEvents.remove(eventType); + if (mSecurityPolicy.canRetrieveWindowContent(this)) { + event.setConnectionId(mId); + } else { + event.setSource(null); + } + event.setSealed(true); + } + + try { + listener.onAccessibilityEvent(event); + if (DEBUG) { + Slog.i(LOG_TAG, "Event " + event + " sent to " + listener); + } + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re); + } finally { + event.recycle(); + } + } + + public void notifyGesture(int gestureId) { + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, + gestureId, 0).sendToTarget(); + } + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_KEY_EVENT, + policyFlags, 0, event).sendToTarget(); + } + + public void notifyClearAccessibilityNodeInfoCache() { + mInvocationHandler.sendEmptyMessage( + InvocationHandler.MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); + } + + private void notifyGestureInternal(int gestureId) { + IAccessibilityServiceClient listener = mServiceInterface; + if (listener != null) { + try { + listener.onGesture(gestureId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during sending gesture " + gestureId + + " to " + mService, re); + } + } + } + + private void notifyKeyEventInternal(KeyEvent event, int policyFlags) { + mKeyEventDispatcher.notifyKeyEvent(event, policyFlags); + } + + private void notifyClearAccessibilityNodeInfoCacheInternal() { + IAccessibilityServiceClient listener = mServiceInterface; + if (listener != null) { + try { + listener.clearAccessibilityNodeInfoCache(); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error during requesting accessibility info cache" + + " to be cleared.", re); + } + } + } + + private void sendDownAndUpKeyEvents(int keyCode) { + final long token = Binder.clearCallingIdentity(); + + // Inject down. + final long downTime = SystemClock.uptimeMillis(); + KeyEvent down = KeyEvent.obtain(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM, + InputDevice.SOURCE_KEYBOARD, null); + InputManager.getInstance().injectInputEvent(down, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + down.recycle(); + + // Inject up. + final long upTime = SystemClock.uptimeMillis(); + KeyEvent up = KeyEvent.obtain(downTime, upTime, KeyEvent.ACTION_UP, keyCode, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM, + InputDevice.SOURCE_KEYBOARD, null); + InputManager.getInstance().injectInputEvent(up, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + up.recycle(); + + Binder.restoreCallingIdentity(token); + } + + private void expandNotifications() { + final long token = Binder.clearCallingIdentity(); + + StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( + android.app.Service.STATUS_BAR_SERVICE); + statusBarManager.expandNotificationsPanel(); + + Binder.restoreCallingIdentity(token); + } + + private void expandQuickSettings() { + final long token = Binder.clearCallingIdentity(); + + StatusBarManager statusBarManager = (StatusBarManager) mContext.getSystemService( + android.app.Service.STATUS_BAR_SERVICE); + statusBarManager.expandSettingsPanel(); + + Binder.restoreCallingIdentity(token); + } + + private void openRecents() { + final long token = Binder.clearCallingIdentity(); + + IStatusBarService statusBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService("statusbar")); + try { + statusBarService.toggleRecentApps(); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error toggling recent apps."); + } + + Binder.restoreCallingIdentity(token); + } + + private IAccessibilityInteractionConnection getConnectionLocked(int windowId) { + if (DEBUG) { + Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId); + } + AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId); + if (wrapper == null) { + wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId); + } + if (wrapper != null && wrapper.mConnection != null) { + return wrapper.mConnection; + } + if (DEBUG) { + Slog.e(LOG_TAG, "No interaction connection to window: " + windowId); + } + return null; + } + + private int resolveAccessibilityWindowIdLocked(int accessibilityWindowId) { + if (accessibilityWindowId == AccessibilityNodeInfo.ACTIVE_WINDOW_ID) { + return mSecurityPolicy.mActiveWindowId; + } + return accessibilityWindowId; + } + + private MagnificationSpec getCompatibleMagnificationSpec(int windowId) { + try { + IBinder windowToken = mGlobalWindowTokens.get(windowId); + if (windowToken == null) { + windowToken = getCurrentUserStateLocked().mWindowTokens.get(windowId); + } + if (windowToken != null) { + return mWindowManagerService.getCompatibleMagnificationSpecForWindow( + windowToken); + } + } catch (RemoteException re) { + /* ignore */ + } + return null; + } + + private final class InvocationHandler extends Handler { + + public static final int MSG_ON_GESTURE = 1; + public static final int MSG_ON_KEY_EVENT = 2; + public static final int MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 3; + public static final int MSG_ON_KEY_EVENT_TIMEOUT = 4; + + public InvocationHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MSG_ON_GESTURE: { + final int gestureId = message.arg1; + notifyGestureInternal(gestureId); + } break; + case MSG_ON_KEY_EVENT: { + KeyEvent event = (KeyEvent) message.obj; + final int policyFlags = message.arg1; + notifyKeyEventInternal(event, policyFlags); + } break; + case MSG_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { + notifyClearAccessibilityNodeInfoCacheInternal(); + } break; + case MSG_ON_KEY_EVENT_TIMEOUT: { + PendingEvent eventState = (PendingEvent) message.obj; + setOnKeyEventResult(false, eventState.sequence); + } break; + default: { + throw new IllegalArgumentException("Unknown message: " + type); + } + } + } + } + + private final class KeyEventDispatcher { + + private static final long ON_KEY_EVENT_TIMEOUT_MILLIS = 500; + + private PendingEvent mPendingEvents; + + private final InputEventConsistencyVerifier mSentEventsVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() + ? new InputEventConsistencyVerifier( + this, 0, KeyEventDispatcher.class.getSimpleName()) : null; + + public void notifyKeyEvent(KeyEvent event, int policyFlags) { + final PendingEvent pendingEvent; + + synchronized (mLock) { + pendingEvent = addPendingEventLocked(event, policyFlags); + } + + Message message = mInvocationHandler.obtainMessage( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, pendingEvent); + mInvocationHandler.sendMessageDelayed(message, ON_KEY_EVENT_TIMEOUT_MILLIS); + + try { + // Accessibility services are exclusively not in the system + // process, therefore no need to clone the motion event to + // prevent tampering. It will be cloned in the IPC call. + mServiceInterface.onKeyEvent(pendingEvent.event, pendingEvent.sequence); + } catch (RemoteException re) { + setOnKeyEventResult(false, pendingEvent.sequence); + } + } + + public void setOnKeyEventResult(boolean handled, int sequence) { + synchronized (mLock) { + PendingEvent pendingEvent = removePendingEventLocked(sequence); + if (pendingEvent != null) { + mInvocationHandler.removeMessages( + InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + pendingEvent.handled = handled; + finishPendingEventLocked(pendingEvent); + } + } + } + + public void flush() { + synchronized (mLock) { + cancelAllPendingEventsLocked(); + if (mSentEventsVerifier != null) { + mSentEventsVerifier.reset(); + } + } + } + + private PendingEvent addPendingEventLocked(KeyEvent event, int policyFlags) { + final int sequence = event.getSequenceNumber(); + PendingEvent pendingEvent = obtainPendingEventLocked(event, policyFlags, sequence); + pendingEvent.next = mPendingEvents; + mPendingEvents = pendingEvent; + return pendingEvent; + } + + private PendingEvent removePendingEventLocked(int sequence) { + PendingEvent previous = null; + PendingEvent current = mPendingEvents; + + while (current != null) { + if (current.sequence == sequence) { + if (previous != null) { + previous.next = current.next; + } else { + mPendingEvents = current.next; + } + current.next = null; + return current; + } + previous = current; + current = current.next; + } + return null; + } + + private void finishPendingEventLocked(PendingEvent pendingEvent) { + if (!pendingEvent.handled) { + sendKeyEventToInputFilter(pendingEvent.event, pendingEvent.policyFlags); + } + // Nullify the event since we do not want it to be + // recycled yet. It will be sent to the input filter. + pendingEvent.event = null; + recyclePendingEventLocked(pendingEvent); + } + + private void sendKeyEventToInputFilter(KeyEvent event, int policyFlags) { + if (DEBUG) { + Slog.i(LOG_TAG, "Injecting event: " + event); + } + if (mSentEventsVerifier != null) { + mSentEventsVerifier.onKeyEvent(event, 0); + } + policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; + mMainHandler.obtainMessage(MainHandler.MSG_SEND_KEY_EVENT_TO_INPUT_FILTER, + policyFlags, 0, event).sendToTarget(); + } + + private void cancelAllPendingEventsLocked() { + while (mPendingEvents != null) { + PendingEvent pendingEvent = removePendingEventLocked(mPendingEvents.sequence); + pendingEvent.handled = false; + mInvocationHandler.removeMessages(InvocationHandler.MSG_ON_KEY_EVENT_TIMEOUT, + pendingEvent); + finishPendingEventLocked(pendingEvent); + } + } + } + } + + private static final class PendingEvent { + PendingEvent next; + + KeyEvent event; + int policyFlags; + int sequence; + boolean handled; + + public void clear() { + if (event != null) { + event.recycle(); + event = null; + } + next = null; + policyFlags = 0; + sequence = 0; + handled = false; + } + } + + final class SecurityPolicy { + private static final int VALID_ACTIONS = + AccessibilityNodeInfo.ACTION_CLICK + | AccessibilityNodeInfo.ACTION_LONG_CLICK + | AccessibilityNodeInfo.ACTION_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_FOCUS + | AccessibilityNodeInfo.ACTION_SELECT + | AccessibilityNodeInfo.ACTION_CLEAR_SELECTION + | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS + | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS + | AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY + | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT + | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT + | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD + | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD + | AccessibilityNodeInfo.ACTION_COPY + | AccessibilityNodeInfo.ACTION_PASTE + | AccessibilityNodeInfo.ACTION_CUT + | AccessibilityNodeInfo.ACTION_SET_SELECTION + | AccessibilityNodeInfo.ACTION_EXPAND + | AccessibilityNodeInfo.ACTION_COLLAPSE + | AccessibilityNodeInfo.ACTION_DISMISS; + + private static final int RETRIEVAL_ALLOWING_EVENT_TYPES = + AccessibilityEvent.TYPE_VIEW_CLICKED + | AccessibilityEvent.TYPE_VIEW_FOCUSED + | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER + | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT + | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED + | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED + | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + | AccessibilityEvent.TYPE_VIEW_SELECTED + | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED + | AccessibilityEvent.TYPE_VIEW_SCROLLED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED; + + private int mActiveWindowId; + private boolean mTouchInteractionInProgress; + + private boolean canDispatchAccessibilityEvent(AccessibilityEvent event) { + final int eventType = event.getEventType(); + switch (eventType) { + // All events that are for changes in a global window + // state should *always* be dispatched. + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: + // All events generated by the user touching the + // screen should *always* be dispatched. + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: + case AccessibilityEvent.TYPE_GESTURE_DETECTION_START: + case AccessibilityEvent.TYPE_GESTURE_DETECTION_END: + case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START: + case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END: + // These will change the active window, so dispatch. + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { + return true; + } + // All events for changes in window content should be + // dispatched *only* if this window is the active one. + default: + return event.getWindowId() == mActiveWindowId; + } + } + + public void updateEventSourceLocked(AccessibilityEvent event) { + if ((event.getEventType() & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) { + event.setSource(null); + } + } + + public void updateActiveWindow(int windowId, int eventType) { + // The active window is either the window that has input focus or + // the window that the user is currently touching. If the user is + // touching a window that does not have input focus as soon as the + // the user stops touching that window the focused window becomes + // the active one. + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { + if (getFocusedWindowId() == windowId) { + mActiveWindowId = windowId; + } + } break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: { + // Do not allow delayed hover events to confuse us + // which the active window is. + if (mTouchInteractionInProgress) { + mActiveWindowId = windowId; + } + } break; + } + } + + public void onTouchInteractionStart() { + mTouchInteractionInProgress = true; + } + + public void onTouchInteractionEnd() { + mTouchInteractionInProgress = false; + // We want to set the active window to be current immediately + // after the user has stopped touching the screen since if the + // user types with the IME he should get a feedback for the + // letter typed in the text view which is in the input focused + // window. Note that we always deliver hover accessibility events + // (they are a result of user touching the screen) so change of + // the active window before all hover accessibility events from + // the touched window are delivered is fine. + mActiveWindowId = getFocusedWindowId(); + } + + public int getRetrievalAllowingWindowLocked() { + return mActiveWindowId; + } + + public boolean canGetAccessibilityNodeInfoLocked(Service service, int windowId) { + return canRetrieveWindowContent(service) && isRetrievalAllowingWindow(windowId); + } + + public boolean canPerformActionLocked(Service service, int windowId, int action, + Bundle arguments) { + return canRetrieveWindowContent(service) + && isRetrievalAllowingWindow(windowId) + && isActionPermitted(action); + } + + public boolean canRetrieveWindowContent(Service service) { + return (service.mAccessibilityServiceInfo.getCapabilities() + & AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0; + } + + public void enforceCanRetrieveWindowContent(Service service) throws RemoteException { + // This happens due to incorrect registration so make it apparent. + if (!canRetrieveWindowContent(service)) { + Slog.e(LOG_TAG, "Accessibility serivce " + service.mComponentName + " does not " + + "declare android:canRetrieveWindowContent."); + throw new RemoteException(); + } + } + + public int resolveCallingUserIdEnforcingPermissionsLocked(int userId) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == 0 + || callingUid == Process.SYSTEM_UID + || callingUid == Process.SHELL_UID) { + return mCurrentUserId; + } + final int callingUserId = UserHandle.getUserId(callingUid); + if (callingUserId == userId) { + return userId; + } + if (!hasPermission(Manifest.permission.INTERACT_ACROSS_USERS) + && !hasPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)) { + throw new SecurityException("Call from user " + callingUserId + " as user " + + userId + " without permission INTERACT_ACROSS_USERS or " + + "INTERACT_ACROSS_USERS_FULL not allowed."); + } + if (userId == UserHandle.USER_CURRENT + || userId == UserHandle.USER_CURRENT_OR_SELF) { + return mCurrentUserId; + } + throw new IllegalArgumentException("Calling user can be changed to only " + + "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF."); + } + + public boolean isCallerInteractingAcrossUsers(int userId) { + final int callingUid = Binder.getCallingUid(); + return (Binder.getCallingPid() == android.os.Process.myPid() + || callingUid == Process.SHELL_UID + || userId == UserHandle.USER_CURRENT + || userId == UserHandle.USER_CURRENT_OR_SELF); + } + + private boolean isRetrievalAllowingWindow(int windowId) { + return (mActiveWindowId == windowId); + } + + private boolean isActionPermitted(int action) { + return (VALID_ACTIONS & action) != 0; + } + + private void enforceCallingPermission(String permission, String function) { + if (OWN_PROCESS_ID == Binder.getCallingPid()) { + return; + } + if (!hasPermission(permission)) { + throw new SecurityException("You do not have " + permission + + " required to call " + function + " from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); + } + } + + private boolean hasPermission(String permission) { + return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; + } + + private int getFocusedWindowId() { + try { + // We call this only on window focus change or after touch + // exploration gesture end and the shown windows are not that + // many, so the linear look up is just fine. + IBinder token = mWindowManagerService.getFocusedWindowToken(); + if (token != null) { + synchronized (mLock) { + int windowId = getFocusedWindowIdLocked(token, mGlobalWindowTokens); + if (windowId < 0) { + windowId = getFocusedWindowIdLocked(token, + getCurrentUserStateLocked().mWindowTokens); + } + return windowId; + } + } + } catch (RemoteException re) { + /* ignore */ + } + return -1; + } + + private int getFocusedWindowIdLocked(IBinder token, SparseArray windows) { + final int windowCount = windows.size(); + for (int i = 0; i < windowCount; i++) { + if (windows.valueAt(i) == token) { + return windows.keyAt(i); + } + } + return -1; + } + } + + private class UserState { + public final int mUserId; + + // Non-transient state. + + public final RemoteCallbackList mClients = + new RemoteCallbackList(); + + public final SparseArray mInteractionConnections = + new SparseArray(); + + public final SparseArray mWindowTokens = new SparseArray(); + + // Transient state. + + public final CopyOnWriteArrayList mBoundServices = + new CopyOnWriteArrayList(); + + public final Map mComponentNameToServiceMap = + new HashMap(); + + public final List mInstalledServices = + new ArrayList(); + + public final Set mBindingServices = new HashSet(); + + public final Set mEnabledServices = new HashSet(); + + public final Set mTouchExplorationGrantedServices = + new HashSet(); + + public int mHandledFeedbackTypes = 0; + + public int mLastSentClientState = -1; + + public boolean mIsAccessibilityEnabled; + public boolean mIsTouchExplorationEnabled; + public boolean mIsEnhancedWebAccessibilityEnabled; + public boolean mIsDisplayMagnificationEnabled; + public boolean mIsFilterKeyEventsEnabled; + + private Service mUiAutomationService; + private IAccessibilityServiceClient mUiAutomationServiceClient; + + private IBinder mUiAutomationServiceOwner; + private final DeathRecipient mUiAutomationSerivceOnwerDeathRecipient = + new DeathRecipient() { + @Override + public void binderDied() { + mUiAutomationServiceOwner.unlinkToDeath( + mUiAutomationSerivceOnwerDeathRecipient, 0); + mUiAutomationServiceOwner = null; + if (mUiAutomationService != null) { + mUiAutomationService.binderDied(); + } + } + }; + + public UserState(int userId) { + mUserId = userId; + } + + public int getClientState() { + int clientState = 0; + if (mIsAccessibilityEnabled) { + clientState |= AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED; + } + // Touch exploration relies on enabled accessibility. + if (mIsAccessibilityEnabled && mIsTouchExplorationEnabled) { + clientState |= AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED; + } + return clientState; + } + + public void onSwitchToAnotherUser() { + // Clear UI test automation state. + if (mUiAutomationService != null) { + mUiAutomationService.binderDied(); + } + + // Unbind all services. + unbindAllServicesLocked(this); + + // Clear service management state. + mBoundServices.clear(); + mBindingServices.clear(); + + // Clear event management state. + mHandledFeedbackTypes = 0; + mLastSentClientState = -1; + + // Clear state persisted in settings. + mEnabledServices.clear(); + mTouchExplorationGrantedServices.clear(); + mIsAccessibilityEnabled = false; + mIsTouchExplorationEnabled = false; + mIsEnhancedWebAccessibilityEnabled = false; + mIsDisplayMagnificationEnabled = false; + } + + public void destroyUiAutomationService() { + mUiAutomationService = null; + mUiAutomationServiceClient = null; + if (mUiAutomationServiceOwner != null) { + mUiAutomationServiceOwner.unlinkToDeath( + mUiAutomationSerivceOnwerDeathRecipient, 0); + mUiAutomationServiceOwner = null; + } + } + } + + private final class AccessibilityContentObserver extends ContentObserver { + + private final Uri mAccessibilityEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_ENABLED); + + private final Uri mTouchExplorationEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.TOUCH_EXPLORATION_ENABLED); + + private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + + private final Uri mEnabledAccessibilityServicesUri = Settings.Secure.getUriFor( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + + private final Uri mTouchExplorationGrantedAccessibilityServicesUri = Settings.Secure + .getUriFor(Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES); + + private final Uri mEnhancedWebAccessibilityUri = Settings.Secure + .getUriFor(Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION); + + public AccessibilityContentObserver(Handler handler) { + super(handler); + } + + public void register(ContentResolver contentResolver) { + contentResolver.registerContentObserver(mAccessibilityEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mTouchExplorationEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mDisplayMagnificationEnabledUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mEnabledAccessibilityServicesUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mTouchExplorationGrantedAccessibilityServicesUri, + false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver(mEnhancedWebAccessibilityUri, + false, this, UserHandle.USER_ALL); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mAccessibilityEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readAccessibilityEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mTouchExplorationEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readTouchExplorationEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mDisplayMagnificationEnabledUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readDisplayMagnificationEnabledSettingLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mEnabledAccessibilityServicesUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readEnabledAccessibilityServicesLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mTouchExplorationGrantedAccessibilityServicesUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readTouchExplorationGrantedAccessibilityServicesLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } else if (mEnhancedWebAccessibilityUri.equals(uri)) { + synchronized (mLock) { + // We will update when the automation service dies. + UserState userState = getCurrentUserStateLocked(); + if (userState.mUiAutomationService == null) { + if (readEnhancedWebAccessibilityEnabledChangedLocked(userState)) { + onUserStateChangedLocked(userState); + } + } + } + } + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java new file mode 100644 index 0000000..8c93e7b --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java @@ -0,0 +1,93 @@ +/* + ** Copyright 2012, The Android Open Source Project + ** + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** + ** http://www.apache.org/licenses/LICENSE-2.0 + ** + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + */ + +package com.android.server.accessibility; + +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityEvent; + +/** + * Interface for classes that can handle and potentially transform a stream of + * motion and accessibility events. Instances implementing this interface are + * ordered in a sequence to implement a transformation chain. An instance may + * consume, modify, and generate events. It is responsible to deliver the + * output events to the next transformation in the sequence set via + * {@link #setNext(EventStreamTransformation)}. + * + * Note that since instances implementing this interface are transformations + * of the event stream, an instance should work against the event stream + * potentially modified by previous ones. Hence, the order of transformations + * is important. + * + * It is a responsibility of each handler that decides to react to an event + * sequence and prevent any subsequent ones from performing an action to send + * the appropriate cancel event given it has delegated a part of the events + * that belong to the current gesture. This will ensure that subsequent + * transformations will not be left in an inconsistent state and the applications + * see a consistent event stream. + * + * For example, to cancel a {@link KeyEvent} the handler has to emit an event + * with action {@link KeyEvent#ACTION_UP} with the additional flag + * {@link KeyEvent#FLAG_CANCELED}. To cancel a {@link MotionEvent} the handler + * has to send an event with action {@link MotionEvent#ACTION_CANCEL}. + * + * It is a responsibility of each handler that received a cancel event to clear its + * internal state and to propagate the event to the next one to enable subsequent + * transformations to clear their internal state. + * + * It is a responsibility for each transformation to start handling events only + * after an event that designates the start of a well-formed event sequence. + * For example, if it received a down motion event followed by a cancel motion + * event, it should not handle subsequent move and up events until it gets a down. + */ +interface EventStreamTransformation { + + /** + * Receives a motion event. Passed are the event transformed by previous + * transformations and the raw event to which no transformations have + * been applied. + * + * @param event The transformed motion event. + * @param rawEvent The raw motion event. + * @param policyFlags Policy flags for the event. + */ + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); + + /** + * Receives an accessibility event. + * + * @param event The accessibility event. + */ + public void onAccessibilityEvent(AccessibilityEvent event); + + /** + * Sets the next transformation. + * + * @param next The next transformation. + */ + public void setNext(EventStreamTransformation next); + + /** + * Clears the internal state of this transformation. + */ + public void clear(); + + /** + * Destroys this transformation. + */ + public void onDestroy(); +} diff --git a/services/accessibility/java/com/android/server/accessibility/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java new file mode 100644 index 0000000..b68b09f --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/GestureUtils.java @@ -0,0 +1,102 @@ +package com.android.server.accessibility; + +import android.util.MathUtils; +import android.view.MotionEvent; + +/** + * Some helper functions for gesture detection. + */ +final class GestureUtils { + + private GestureUtils() { + /* cannot be instantiated */ + } + + public static boolean isTap(MotionEvent down, MotionEvent up, int tapTimeSlop, + int tapDistanceSlop, int actionIndex) { + return eventsWithinTimeAndDistanceSlop(down, up, tapTimeSlop, tapDistanceSlop, actionIndex); + } + + public static boolean isMultiTap(MotionEvent firstUp, MotionEvent secondUp, + int multiTapTimeSlop, int multiTapDistanceSlop, int actionIndex) { + return eventsWithinTimeAndDistanceSlop(firstUp, secondUp, multiTapTimeSlop, + multiTapDistanceSlop, actionIndex); + } + + private static boolean eventsWithinTimeAndDistanceSlop(MotionEvent first, MotionEvent second, + int timeout, int distance, int actionIndex) { + if (isTimedOut(first, second, timeout)) { + return false; + } + final double deltaMove = computeDistance(first, second, actionIndex); + if (deltaMove >= distance) { + return false; + } + return true; + } + + public static double computeDistance(MotionEvent first, MotionEvent second, int pointerIndex) { + return MathUtils.dist(first.getX(pointerIndex), first.getY(pointerIndex), + second.getX(pointerIndex), second.getY(pointerIndex)); + } + + public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { + final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); + return (deltaTime >= timeout); + } + + public static boolean isSamePointerContext(MotionEvent first, MotionEvent second) { + return (first.getPointerIdBits() == second.getPointerIdBits() + && first.getPointerId(first.getActionIndex()) + == second.getPointerId(second.getActionIndex())); + } + + /** + * Determines whether a two pointer gesture is a dragging one. + * + * @param event The event with the pointer data. + * @return True if the gesture is a dragging one. + */ + public static boolean isDraggingGesture(float firstPtrDownX, float firstPtrDownY, + float secondPtrDownX, float secondPtrDownY, float firstPtrX, float firstPtrY, + float secondPtrX, float secondPtrY, float maxDraggingAngleCos) { + + // Check if the pointers are moving in the same direction. + final float firstDeltaX = firstPtrX - firstPtrDownX; + final float firstDeltaY = firstPtrY - firstPtrDownY; + + if (firstDeltaX == 0 && firstDeltaY == 0) { + return true; + } + + final float firstMagnitude = + (float) Math.sqrt(firstDeltaX * firstDeltaX + firstDeltaY * firstDeltaY); + final float firstXNormalized = + (firstMagnitude > 0) ? firstDeltaX / firstMagnitude : firstDeltaX; + final float firstYNormalized = + (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY; + + final float secondDeltaX = secondPtrX - secondPtrDownX; + final float secondDeltaY = secondPtrY - secondPtrDownY; + + if (secondDeltaX == 0 && secondDeltaY == 0) { + return true; + } + + final float secondMagnitude = + (float) Math.sqrt(secondDeltaX * secondDeltaX + secondDeltaY * secondDeltaY); + final float secondXNormalized = + (secondMagnitude > 0) ? secondDeltaX / secondMagnitude : secondDeltaX; + final float secondYNormalized = + (secondMagnitude > 0) ? secondDeltaY / secondMagnitude : secondDeltaY; + + final float angleCos = + firstXNormalized * secondXNormalized + firstYNormalized * secondYNormalized; + + if (angleCos < maxDraggingAngleCos) { + return false; + } + + return true; + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java new file mode 100644 index 0000000..5f12cf4 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/ScreenMagnifier.java @@ -0,0 +1,1177 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Property; +import android.util.Slog; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.IMagnificationCallbacks; +import android.view.IWindowManager; +import android.view.MagnificationSpec; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.ScaleGestureDetector; +import android.view.ScaleGestureDetector.OnScaleGestureListener; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.DecelerateInterpolator; + +import com.android.internal.os.SomeArgs; + +import java.util.Locale; + +/** + * This class handles the screen magnification when accessibility is enabled. + * The behavior is as follows: + * + * 1. Triple tap toggles permanent screen magnification which is magnifying + * the area around the location of the triple tap. One can think of the + * location of the triple tap as the center of the magnified viewport. + * For example, a triple tap when not magnified would magnify the screen + * and leave it in a magnified state. A triple tapping when magnified would + * clear magnification and leave the screen in a not magnified state. + * + * 2. Triple tap and hold would magnify the screen if not magnified and enable + * viewport dragging mode until the finger goes up. One can think of this + * mode as a way to move the magnified viewport since the area around the + * moving finger will be magnified to fit the screen. For example, if the + * screen was not magnified and the user triple taps and holds the screen + * would magnify and the viewport will follow the user's finger. When the + * finger goes up the screen will zoom out. If the same user interaction + * is performed when the screen is magnified, the viewport movement will + * be the same but when the finger goes up the screen will stay magnified. + * In other words, the initial magnified state is sticky. + * + * 3. Pinching with any number of additional fingers when viewport dragging + * is enabled, i.e. the user triple tapped and holds, would adjust the + * magnification scale which will become the current default magnification + * scale. The next time the user magnifies the same magnification scale + * would be used. + * + * 4. When in a permanent magnified state the user can use two or more fingers + * to pan the viewport. Note that in this mode the content is panned as + * opposed to the viewport dragging mode in which the viewport is moved. + * + * 5. When in a permanent magnified state the user can use two or more + * fingers to change the magnification scale which will become the current + * default magnification scale. The next time the user magnifies the same + * magnification scale would be used. + * + * 6. The magnification scale will be persisted in settings and in the cloud. + */ +public final class ScreenMagnifier extends IMagnificationCallbacks.Stub + implements EventStreamTransformation { + + private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName(); + + private static final boolean DEBUG_STATE_TRANSITIONS = false; + private static final boolean DEBUG_DETECTING = false; + private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false; + private static final boolean DEBUG_PANNING = false; + private static final boolean DEBUG_SCALING = false; + private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false; + + private static final int STATE_DELEGATING = 1; + private static final int STATE_DETECTING = 2; + private static final int STATE_VIEWPORT_DRAGGING = 3; + private static final int STATE_MAGNIFIED_INTERACTION = 4; + + private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f; + private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50; + + private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1; + private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2; + private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3; + private static final int MESSAGE_ON_ROTATION_CHANGED = 4; + + private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1; + + private static final int MY_PID = android.os.Process.myPid(); + + private final Rect mTempRect = new Rect(); + private final Rect mTempRect1 = new Rect(); + + private final Context mContext; + private final IWindowManager mWindowManager; + private final MagnificationController mMagnificationController; + private final ScreenStateObserver mScreenStateObserver; + + private final DetectingStateHandler mDetectingStateHandler; + private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler; + private final StateViewportDraggingHandler mStateViewportDraggingHandler; + + private final AccessibilityManagerService mAms; + + private final int mTapTimeSlop = ViewConfiguration.getTapTimeout(); + private final int mMultiTapTimeSlop = + ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT; + private final int mTapDistanceSlop; + private final int mMultiTapDistanceSlop; + + private final long mLongAnimationDuration; + + private final Region mMagnifiedBounds = new Region(); + + private EventStreamTransformation mNext; + + private int mCurrentState; + private int mPreviousState; + private boolean mTranslationEnabledBeforePan; + + private PointerCoords[] mTempPointerCoords; + private PointerProperties[] mTempPointerProperties; + + private long mDelegatingStateDownTime; + + private boolean mUpdateMagnificationSpecOnNextBoundsChange; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: { + Region bounds = (Region) message.obj; + handleOnMagnifiedBoundsChanged(bounds); + bounds.recycle(); + } break; + case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: { + SomeArgs args = (SomeArgs) message.obj; + final int left = args.argi1; + final int top = args.argi2; + final int right = args.argi3; + final int bottom = args.argi4; + handleOnRectangleOnScreenRequested(left, top, right, bottom); + args.recycle(); + } break; + case MESSAGE_ON_USER_CONTEXT_CHANGED: { + handleOnUserContextChanged(); + } break; + case MESSAGE_ON_ROTATION_CHANGED: { + final int rotation = message.arg1; + handleOnRotationChanged(rotation); + } break; + } + } + }; + + public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) { + mContext = context; + mWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + mAms = service; + + mLongAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_longAnimTime); + mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + + mDetectingStateHandler = new DetectingStateHandler(); + mStateViewportDraggingHandler = new StateViewportDraggingHandler(); + mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler( + context); + + mMagnificationController = new MagnificationController(mLongAnimationDuration); + mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController); + + try { + mWindowManager.setMagnificationCallbacks(this); + } catch (RemoteException re) { + /* ignore */ + } + + transitionToState(STATE_DETECTING); + } + + @Override + public void onMagnifedBoundsChanged(Region bounds) { + Region newBounds = Region.obtain(bounds); + mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget(); + if (MY_PID != Binder.getCallingPid()) { + bounds.recycle(); + } + } + + private void handleOnMagnifiedBoundsChanged(Region bounds) { + // If there was a rotation we have to update the center of the magnified + // region since the old offset X/Y may be out of its acceptable range for + // the new display width and height. + if (mUpdateMagnificationSpecOnNextBoundsChange) { + mUpdateMagnificationSpecOnNextBoundsChange = false; + MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float scale = spec.scale; + final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale; + final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale; + mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX, + centerY, false); + } + mMagnifiedBounds.set(bounds); + mAms.onMagnificationStateChanged(); + } + + @Override + public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) { + SomeArgs args = SomeArgs.obtain(); + args.argi1 = left; + args.argi2 = top; + args.argi3 = right; + args.argi4 = bottom; + mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget(); + } + + private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + if (!magnifiedFrame.intersects(left, top, right, bottom)) { + return; + } + Rect magnifFrameInScreenCoords = mTempRect1; + getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords); + final float scrollX; + final float scrollY; + if (right - left > magnifFrameInScreenCoords.width()) { + final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); + if (direction == View.LAYOUT_DIRECTION_LTR) { + scrollX = left - magnifFrameInScreenCoords.left; + } else { + scrollX = right - magnifFrameInScreenCoords.right; + } + } else if (left < magnifFrameInScreenCoords.left) { + scrollX = left - magnifFrameInScreenCoords.left; + } else if (right > magnifFrameInScreenCoords.right) { + scrollX = right - magnifFrameInScreenCoords.right; + } else { + scrollX = 0; + } + if (bottom - top > magnifFrameInScreenCoords.height()) { + scrollY = top - magnifFrameInScreenCoords.top; + } else if (top < magnifFrameInScreenCoords.top) { + scrollY = top - magnifFrameInScreenCoords.top; + } else if (bottom > magnifFrameInScreenCoords.bottom) { + scrollY = bottom - magnifFrameInScreenCoords.bottom; + } else { + scrollY = 0; + } + final float scale = mMagnificationController.getScale(); + mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale); + } + + @Override + public void onRotationChanged(int rotation) { + mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget(); + } + + private void handleOnRotationChanged(int rotation) { + resetMagnificationIfNeeded(); + if (mMagnificationController.isMagnifying()) { + mUpdateMagnificationSpecOnNextBoundsChange = true; + } + } + + @Override + public void onUserContextChanged() { + mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED); + } + + private void handleOnUserContextChanged() { + resetMagnificationIfNeeded(); + } + + private void getMagnifiedFrameInContentCoords(Rect rect) { + MagnificationSpec spec = mMagnificationController.getMagnificationSpec(); + mMagnifiedBounds.getBounds(rect); + rect.offset((int) -spec.offsetX, (int) -spec.offsetY); + rect.scale(1.0f / spec.scale); + } + + private void resetMagnificationIfNeeded() { + if (mMagnificationController.isMagnifying() + && isScreenMagnificationAutoUpdateEnabled(mContext)) { + mMagnificationController.reset(true); + } + } + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + mMagnifiedContentInteractonStateHandler.onMotionEvent(event); + switch (mCurrentState) { + case STATE_DELEGATING: { + handleMotionEventStateDelegating(event, rawEvent, policyFlags); + } break; + case STATE_DETECTING: { + mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags); + } break; + case STATE_VIEWPORT_DRAGGING: { + mStateViewportDraggingHandler.onMotionEvent(event, policyFlags); + } break; + case STATE_MAGNIFIED_INTERACTION: { + // mMagnifiedContentInteractonStateHandler handles events only + // if this is the current state since it uses ScaleGestureDetecotr + // and a GestureDetector which need well formed event stream. + } break; + default: { + throw new IllegalStateException("Unknown state: " + mCurrentState); + } + } + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (mNext != null) { + mNext.onAccessibilityEvent(event); + } + } + + @Override + public void setNext(EventStreamTransformation next) { + mNext = next; + } + + @Override + public void clear() { + mCurrentState = STATE_DETECTING; + mDetectingStateHandler.clear(); + mStateViewportDraggingHandler.clear(); + mMagnifiedContentInteractonStateHandler.clear(); + if (mNext != null) { + mNext.clear(); + } + } + + @Override + public void onDestroy() { + mScreenStateObserver.destroy(); + try { + mWindowManager.setMagnificationCallbacks(null); + } catch (RemoteException re) { + /* ignore */ + } + } + + private void handleMotionEventStateDelegating(MotionEvent event, + MotionEvent rawEvent, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mDelegatingStateDownTime = event.getDownTime(); + } break; + case MotionEvent.ACTION_UP: { + if (mDetectingStateHandler.mDelayedEventQueue == null) { + transitionToState(STATE_DETECTING); + } + } break; + } + if (mNext != null) { + // If the event is within the magnified portion of the screen we have + // to change its location to be where the user thinks he is poking the + // UI which may have been magnified and panned. + final float eventX = event.getX(); + final float eventY = event.getY(); + if (mMagnificationController.isMagnifying() + && mMagnifiedBounds.contains((int) eventX, (int) eventY)) { + final float scale = mMagnificationController.getScale(); + final float scaledOffsetX = mMagnificationController.getOffsetX(); + final float scaledOffsetY = mMagnificationController.getOffsetY(); + final int pointerCount = event.getPointerCount(); + PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); + PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerCoords(i, coords[i]); + coords[i].x = (coords[i].x - scaledOffsetX) / scale; + coords[i].y = (coords[i].y - scaledOffsetY) / scale; + event.getPointerProperties(i, properties[i]); + } + event = MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), pointerCount, properties, + coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), + event.getFlags()); + } + // We cache some events to see if the user wants to trigger magnification. + // If no magnification is triggered we inject these events with adjusted + // time and down time to prevent subsequent transformations being confused + // by stale events. After the cached events, which always have a down, are + // injected we need to also update the down time of all subsequent non cached + // events. All delegated events cached and non-cached are delivered here. + event.setDownTime(mDelegatingStateDownTime); + mNext.onMotionEvent(event, rawEvent, policyFlags); + } + } + + private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { + final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; + if (oldSize < size) { + PointerCoords[] oldTempPointerCoords = mTempPointerCoords; + mTempPointerCoords = new PointerCoords[size]; + if (oldTempPointerCoords != null) { + System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); + } + } + for (int i = oldSize; i < size; i++) { + mTempPointerCoords[i] = new PointerCoords(); + } + return mTempPointerCoords; + } + + private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { + final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0; + if (oldSize < size) { + PointerProperties[] oldTempPointerProperties = mTempPointerProperties; + mTempPointerProperties = new PointerProperties[size]; + if (oldTempPointerProperties != null) { + System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize); + } + } + for (int i = oldSize; i < size; i++) { + mTempPointerProperties[i] = new PointerProperties(); + } + return mTempPointerProperties; + } + + private void transitionToState(int state) { + if (DEBUG_STATE_TRANSITIONS) { + switch (state) { + case STATE_DELEGATING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); + } break; + case STATE_DETECTING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); + } break; + case STATE_VIEWPORT_DRAGGING: { + Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); + } break; + case STATE_MAGNIFIED_INTERACTION: { + Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); + } break; + default: { + throw new IllegalArgumentException("Unknown state: " + state); + } + } + } + mPreviousState = mCurrentState; + mCurrentState = state; + } + + private final class MagnifiedContentInteractonStateHandler + extends SimpleOnGestureListener implements OnScaleGestureListener { + private static final float MIN_SCALE = 1.3f; + private static final float MAX_SCALE = 5.0f; + + private static final float SCALING_THRESHOLD = 0.3f; + + private final ScaleGestureDetector mScaleGestureDetector; + private final GestureDetector mGestureDetector; + + private float mInitialScaleFactor = -1; + private boolean mScaling; + + public MagnifiedContentInteractonStateHandler(Context context) { + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mScaleGestureDetector.setQuickScaleEnabled(false); + mGestureDetector = new GestureDetector(context, this); + } + + public void onMotionEvent(MotionEvent event) { + mScaleGestureDetector.onTouchEvent(event); + mGestureDetector.onTouchEvent(event); + if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { + return; + } + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + clear(); + final float scale = Math.min(Math.max(mMagnificationController.getScale(), + MIN_SCALE), MAX_SCALE); + if (scale != getPersistedScale()) { + persistScale(scale); + } + if (mPreviousState == STATE_VIEWPORT_DRAGGING) { + transitionToState(STATE_VIEWPORT_DRAGGING); + } else { + transitionToState(STATE_DETECTING); + } + } + } + + @Override + public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, + float distanceY) { + if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { + return true; + } + if (DEBUG_PANNING) { + Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX + + " scrollY: " + distanceY); + } + mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY); + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + if (!mScaling) { + if (mInitialScaleFactor < 0) { + mInitialScaleFactor = detector.getScaleFactor(); + } else { + final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; + if (Math.abs(deltaScale) > SCALING_THRESHOLD) { + mScaling = true; + return true; + } + } + return false; + } + final float newScale = mMagnificationController.getScale() + * detector.getScaleFactor(); + final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE); + if (DEBUG_SCALING) { + Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale); + } + mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(), + detector.getFocusY(), false); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return (mCurrentState == STATE_MAGNIFIED_INTERACTION); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + clear(); + } + + private void clear() { + mInitialScaleFactor = -1; + mScaling = false; + } + } + + private final class StateViewportDraggingHandler { + private boolean mLastMoveOutsideMagnifiedRegion; + + private void onMotionEvent(MotionEvent event, int policyFlags) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); + } + case MotionEvent.ACTION_POINTER_DOWN: { + clear(); + transitionToState(STATE_MAGNIFIED_INTERACTION); + } break; + case MotionEvent.ACTION_MOVE: { + if (event.getPointerCount() != 1) { + throw new IllegalStateException("Should have one pointer down."); + } + final float eventX = event.getX(); + final float eventY = event.getY(); + if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) { + if (mLastMoveOutsideMagnifiedRegion) { + mLastMoveOutsideMagnifiedRegion = false; + mMagnificationController.setMagnifiedRegionCenter(eventX, + eventY, true); + } else { + mMagnificationController.setMagnifiedRegionCenter(eventX, + eventY, false); + } + } else { + mLastMoveOutsideMagnifiedRegion = true; + } + } break; + case MotionEvent.ACTION_UP: { + if (!mTranslationEnabledBeforePan) { + mMagnificationController.reset(true); + } + clear(); + transitionToState(STATE_DETECTING); + } break; + case MotionEvent.ACTION_POINTER_UP: { + throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP"); + } + } + } + + public void clear() { + mLastMoveOutsideMagnifiedRegion = false; + } + } + + private final class DetectingStateHandler { + + private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; + + private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; + + private static final int ACTION_TAP_COUNT = 3; + + private MotionEventInfo mDelayedEventQueue; + + private MotionEvent mLastDownEvent; + private MotionEvent mLastTapUpEvent; + private int mTapCount; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + final int type = message.what; + switch (type) { + case MESSAGE_ON_ACTION_TAP_AND_HOLD: { + MotionEvent event = (MotionEvent) message.obj; + final int policyFlags = message.arg1; + onActionTapAndHold(event, policyFlags); + } break; + case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { + transitionToState(STATE_DELEGATING); + sendDelayedMotionEvents(); + clear(); + } break; + default: { + throw new IllegalArgumentException("Unknown message type: " + type); + } + } + } + }; + + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cacheDelayedMotionEvent(event, rawEvent, policyFlags); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + if (!mMagnifiedBounds.contains((int) event.getX(), + (int) event.getY())) { + transitionToDelegatingStateAndClear(); + return; + } + if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null + && GestureUtils.isMultiTap(mLastDownEvent, event, + mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { + Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, + policyFlags, 0, event); + mHandler.sendMessageDelayed(message, + ViewConfiguration.getLongPressTimeout()); + } else if (mTapCount < ACTION_TAP_COUNT) { + Message message = mHandler.obtainMessage( + MESSAGE_TRANSITION_TO_DELEGATING_STATE); + mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); + } + clearLastDownEvent(); + mLastDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + if (mMagnificationController.isMagnifying()) { + transitionToState(STATE_MAGNIFIED_INTERACTION); + clear(); + } else { + transitionToDelegatingStateAndClear(); + } + } break; + case MotionEvent.ACTION_MOVE: { + if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { + final double distance = GestureUtils.computeDistance(mLastDownEvent, + event, 0); + if (Math.abs(distance) > mTapDistanceSlop) { + transitionToDelegatingStateAndClear(); + } + } + } break; + case MotionEvent.ACTION_UP: { + if (mLastDownEvent == null) { + return; + } + mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); + if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) { + transitionToDelegatingStateAndClear(); + return; + } + if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, + mTapDistanceSlop, 0)) { + transitionToDelegatingStateAndClear(); + return; + } + if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent, + event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { + transitionToDelegatingStateAndClear(); + return; + } + mTapCount++; + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "Tap count:" + mTapCount); + } + if (mTapCount == ACTION_TAP_COUNT) { + clear(); + onActionTap(event, policyFlags); + return; + } + clearLastTapUpEvent(); + mLastTapUpEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_POINTER_UP: { + /* do nothing */ + } break; + } + } + + public void clear() { + mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); + mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + clearTapDetectionState(); + clearDelayedMotionEvents(); + } + + private void clearTapDetectionState() { + mTapCount = 0; + clearLastTapUpEvent(); + clearLastDownEvent(); + } + + private void clearLastTapUpEvent() { + if (mLastTapUpEvent != null) { + mLastTapUpEvent.recycle(); + mLastTapUpEvent = null; + } + } + + private void clearLastDownEvent() { + if (mLastDownEvent != null) { + mLastDownEvent.recycle(); + mLastDownEvent = null; + } + } + + private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, + policyFlags); + if (mDelayedEventQueue == null) { + mDelayedEventQueue = info; + } else { + MotionEventInfo tail = mDelayedEventQueue; + while (tail.mNext != null) { + tail = tail.mNext; + } + tail.mNext = info; + } + } + + private void sendDelayedMotionEvents() { + while (mDelayedEventQueue != null) { + MotionEventInfo info = mDelayedEventQueue; + mDelayedEventQueue = info.mNext; + final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis; + MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset); + MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset); + ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags); + event.recycle(); + rawEvent.recycle(); + info.recycle(); + } + } + + private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) { + final int pointerCount = event.getPointerCount(); + PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); + PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerCoords(i, coords[i]); + event.getPointerProperties(i, properties[i]); + } + final long downTime = event.getDownTime() + offset; + final long eventTime = event.getEventTime() + offset; + return MotionEvent.obtain(downTime, eventTime, + event.getAction(), pointerCount, properties, coords, + event.getMetaState(), event.getButtonState(), + 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), + event.getSource(), event.getFlags()); + } + + private void clearDelayedMotionEvents() { + while (mDelayedEventQueue != null) { + MotionEventInfo info = mDelayedEventQueue; + mDelayedEventQueue = info.mNext; + info.recycle(); + } + } + + private void transitionToDelegatingStateAndClear() { + transitionToState(STATE_DELEGATING); + sendDelayedMotionEvents(); + clear(); + } + + private void onActionTap(MotionEvent up, int policyFlags) { + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "onActionTap()"); + } + if (!mMagnificationController.isMagnifying()) { + mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), + up.getX(), up.getY(), true); + } else { + mMagnificationController.reset(true); + } + } + + private void onActionTapAndHold(MotionEvent down, int policyFlags) { + if (DEBUG_DETECTING) { + Slog.i(LOG_TAG, "onActionTapAndHold()"); + } + clear(); + mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); + mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(), + down.getX(), down.getY(), true); + transitionToState(STATE_VIEWPORT_DRAGGING); + } + } + + private void persistScale(final float scale) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + Settings.Secure.putFloat(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale); + return null; + } + }.execute(); + } + + private float getPersistedScale() { + return Settings.Secure.getFloat(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + DEFAULT_MAGNIFICATION_SCALE); + } + + private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) { + return (Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE, + DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1); + } + + private static final class MotionEventInfo { + + private static final int MAX_POOL_SIZE = 10; + + private static final Object sLock = new Object(); + private static MotionEventInfo sPool; + private static int sPoolSize; + + private MotionEventInfo mNext; + private boolean mInPool; + + public MotionEvent mEvent; + public MotionEvent mRawEvent; + public int mPolicyFlags; + public long mCachedTimeMillis; + + public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + synchronized (sLock) { + MotionEventInfo info; + if (sPoolSize > 0) { + sPoolSize--; + info = sPool; + sPool = info.mNext; + info.mNext = null; + info.mInPool = false; + } else { + info = new MotionEventInfo(); + } + info.initialize(event, rawEvent, policyFlags); + return info; + } + } + + private void initialize(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + mEvent = MotionEvent.obtain(event); + mRawEvent = MotionEvent.obtain(rawEvent); + mPolicyFlags = policyFlags; + mCachedTimeMillis = SystemClock.uptimeMillis(); + } + + public void recycle() { + synchronized (sLock) { + if (mInPool) { + throw new IllegalStateException("Already recycled."); + } + clear(); + if (sPoolSize < MAX_POOL_SIZE) { + sPoolSize++; + mNext = sPool; + sPool = this; + mInPool = true; + } + } + } + + private void clear() { + mEvent.recycle(); + mEvent = null; + mRawEvent.recycle(); + mRawEvent = null; + mPolicyFlags = 0; + mCachedTimeMillis = 0; + } + } + + private final class MagnificationController { + + private static final String PROPERTY_NAME_MAGNIFICATION_SPEC = + "magnificationSpec"; + + private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain(); + + private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain(); + + private final Rect mTempRect = new Rect(); + + private final ValueAnimator mTransformationAnimator; + + public MagnificationController(long animationDuration) { + Property property = + Property.of(MagnificationController.class, MagnificationSpec.class, + PROPERTY_NAME_MAGNIFICATION_SPEC); + TypeEvaluator evaluator = new TypeEvaluator() { + private final MagnificationSpec mTempTransformationSpec = + MagnificationSpec.obtain(); + @Override + public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec, + MagnificationSpec toSpec) { + MagnificationSpec result = mTempTransformationSpec; + result.scale = fromSpec.scale + + (toSpec.scale - fromSpec.scale) * fraction; + result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX) + * fraction; + result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY) + * fraction; + return result; + } + }; + mTransformationAnimator = ObjectAnimator.ofObject(this, property, + evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec); + mTransformationAnimator.setDuration((long) (animationDuration)); + mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); + } + + public boolean isMagnifying() { + return mCurrentMagnificationSpec.scale > 1.0f; + } + + public void reset(boolean animate) { + if (mTransformationAnimator.isRunning()) { + mTransformationAnimator.cancel(); + } + mCurrentMagnificationSpec.clear(); + if (animate) { + animateMangificationSpec(mSentMagnificationSpec, + mCurrentMagnificationSpec); + } else { + setMagnificationSpec(mCurrentMagnificationSpec); + } + Rect bounds = mTempRect; + bounds.setEmpty(); + mAms.onMagnificationStateChanged(); + } + + public float getScale() { + return mCurrentMagnificationSpec.scale; + } + + public float getOffsetX() { + return mCurrentMagnificationSpec.offsetX; + } + + public float getOffsetY() { + return mCurrentMagnificationSpec.offsetY; + } + + public void setScale(float scale, float pivotX, float pivotY, boolean animate) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + MagnificationSpec spec = mCurrentMagnificationSpec; + final float oldScale = spec.scale; + final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale; + final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale; + final float normPivotX = (-spec.offsetX + pivotX) / oldScale; + final float normPivotY = (-spec.offsetY + pivotY) / oldScale; + final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale); + final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale); + final float centerX = normPivotX + offsetX; + final float centerY = normPivotY + offsetY; + setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate); + } + + public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) { + setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY, + animate); + } + + public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) { + final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; + mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, + getMinOffsetX()), 0); + final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; + mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, + getMinOffsetY()), 0); + setMagnificationSpec(mCurrentMagnificationSpec); + } + + public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY, + boolean animate) { + if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0 + && Float.compare(mCurrentMagnificationSpec.offsetX, + centerX) == 0 + && Float.compare(mCurrentMagnificationSpec.offsetY, + centerY) == 0) { + return; + } + if (mTransformationAnimator.isRunning()) { + mTransformationAnimator.cancel(); + } + if (DEBUG_MAGNIFICATION_CONTROLLER) { + Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX + + " offsetY: " + centerY); + } + updateMagnificationSpec(scale, centerX, centerY); + if (animate) { + animateMangificationSpec(mSentMagnificationSpec, + mCurrentMagnificationSpec); + } else { + setMagnificationSpec(mCurrentMagnificationSpec); + } + mAms.onMagnificationStateChanged(); + } + + public void updateMagnificationSpec(float scale, float magnifiedCenterX, + float magnifiedCenterY) { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + mCurrentMagnificationSpec.scale = scale; + final int viewportWidth = magnifiedFrame.width(); + final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale; + mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX, + getMinOffsetX()), 0); + final int viewportHeight = magnifiedFrame.height(); + final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale; + mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY, + getMinOffsetY()), 0); + } + + private float getMinOffsetX() { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float viewportWidth = magnifiedFrame.width(); + return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale; + } + + private float getMinOffsetY() { + Rect magnifiedFrame = mTempRect; + mMagnifiedBounds.getBounds(magnifiedFrame); + final float viewportHeight = magnifiedFrame.height(); + return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale; + } + + private void animateMangificationSpec(MagnificationSpec fromSpec, + MagnificationSpec toSpec) { + mTransformationAnimator.setObjectValues(fromSpec, toSpec); + mTransformationAnimator.start(); + } + + public MagnificationSpec getMagnificationSpec() { + return mSentMagnificationSpec; + } + + public void setMagnificationSpec(MagnificationSpec spec) { + if (DEBUG_SET_MAGNIFICATION_SPEC) { + Slog.i(LOG_TAG, "Sending: " + spec); + } + try { + mSentMagnificationSpec.scale = spec.scale; + mSentMagnificationSpec.offsetX = spec.offsetX; + mSentMagnificationSpec.offsetY = spec.offsetY; + mWindowManager.setMagnificationSpec( + MagnificationSpec.obtain(spec)); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + private final class ScreenStateObserver extends BroadcastReceiver { + private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1; + + private final Context mContext; + private final MagnificationController mMagnificationController; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ON_SCREEN_STATE_CHANGE: { + String action = (String) message.obj; + handleOnScreenStateChange(action); + } break; + } + } + }; + + public ScreenStateObserver(Context context, + MagnificationController magnificationController) { + mContext = context; + mMagnificationController = magnificationController; + mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); + } + + public void destroy() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE, + intent.getAction()).sendToTarget(); + } + + private void handleOnScreenStateChange(String action) { + if (mMagnificationController.isMagnifying() + && isScreenMagnificationAutoUpdateEnabled(mContext)) { + mMagnificationController.reset(false); + } + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java new file mode 100644 index 0000000..43f12eb --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java @@ -0,0 +1,1937 @@ +/* + ** Copyright 2011, The Android Open Source Project + ** + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** + ** http://www.apache.org/licenses/LICENSE-2.0 + ** + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + */ + +package com.android.server.accessibility; + +import android.content.Context; +import android.gesture.Gesture; +import android.gesture.GestureLibraries; +import android.gesture.GestureLibrary; +import android.gesture.GesturePoint; +import android.gesture.GestureStore; +import android.gesture.GestureStroke; +import android.gesture.Prediction; +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemClock; +import android.util.Slog; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.WindowManagerPolicy; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class is a strategy for performing touch exploration. It + * transforms the motion event stream by modifying, adding, replacing, + * and consuming certain events. The interaction model is: + * + *
    + *
  1. 1. One finger moving slow around performs touch exploration.
  2. + *
  3. 2. One finger moving fast around performs gestures.
  4. + *
  5. 3. Two close fingers moving in the same direction perform a drag.
  6. + *
  7. 4. Multi-finger gestures are delivered to view hierarchy.
  8. + *
  9. 5. Two fingers moving in different directions are considered a multi-finger gesture.
  10. + *
  11. 7. Double tapping clicks on the on the last touch explored location if it was in + * a window that does not take focus, otherwise the click is within the accessibility + * focused rectangle.
  12. + *
  13. 7. Tapping and holding for a while performs a long press in a similar fashion + * as the click above.
  14. + *
      + * + * @hide + */ +class TouchExplorer implements EventStreamTransformation { + + private static final boolean DEBUG = false; + + // Tag for logging received events. + private static final String LOG_TAG = "TouchExplorer"; + + // States this explorer can be in. + private static final int STATE_TOUCH_EXPLORING = 0x00000001; + private static final int STATE_DRAGGING = 0x00000002; + private static final int STATE_DELEGATING = 0x00000004; + private static final int STATE_GESTURE_DETECTING = 0x00000005; + + // The maximum of the cosine between the vectors of two moving + // pointers so they can be considered moving in the same direction. + private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) + + // Constant referring to the ids bits of all pointers. + private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; + + // This constant captures the current implementation detail that + // pointer IDs are between 0 and 31 inclusive (subject to change). + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + private static final int MAX_POINTER_COUNT = 32; + + // Invalid pointer ID. + private static final int INVALID_POINTER_ID = -1; + + // The velocity above which we detect gestures. + private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000; + + // The minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200; + + // The timeout after which we are no longer trying to detect a gesture. + private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000; + + // Timeout before trying to decide what the user is trying to do. + private final int mDetermineUserIntentTimeout; + + // Timeout within which we try to detect a tap. + private final int mTapTimeout; + + // Timeout within which we try to detect a double tap. + private final int mDoubleTapTimeout; + + // Slop between the down and up tap to be a tap. + private final int mTouchSlop; + + // Slop between the first and second tap to be a double tap. + private final int mDoubleTapSlop; + + // The current state of the touch explorer. + private int mCurrentState = STATE_TOUCH_EXPLORING; + + // The ID of the pointer used for dragging. + private int mDraggingPointerId; + + // Handler for performing asynchronous operations. + private final Handler mHandler; + + // Command for delayed sending of a hover enter and move event. + private final SendHoverEnterAndMoveDelayed mSendHoverEnterAndMoveDelayed; + + // Command for delayed sending of a hover exit event. + private final SendHoverExitDelayed mSendHoverExitDelayed; + + // Command for delayed sending of touch exploration end events. + private final SendAccessibilityEventDelayed mSendTouchExplorationEndDelayed; + + // Command for delayed sending of touch interaction end events. + private final SendAccessibilityEventDelayed mSendTouchInteractionEndDelayed; + + // Command for delayed sending of a long press. + private final PerformLongPressDelayed mPerformLongPressDelayed; + + // Command for exiting gesture detection mode after a timeout. + private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed; + + // Helper to detect and react to double tap in touch explore mode. + private final DoubleTapDetector mDoubleTapDetector; + + // The scaled minimal distance before we take the middle of the distance between + // the two dragging pointers as opposed to use the location of the primary one. + private final int mScaledMinPointerDistanceToUseMiddleLocation; + + // The scaled velocity above which we detect gestures. + private final int mScaledGestureDetectionVelocity; + + // The handler to which to delegate events. + private EventStreamTransformation mNext; + + // Helper to track gesture velocity. + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + + // Helper class to track received pointers. + private final ReceivedPointerTracker mReceivedPointerTracker; + + // Helper class to track injected pointers. + private final InjectedPointerTracker mInjectedPointerTracker; + + // Handle to the accessibility manager service. + private final AccessibilityManagerService mAms; + + // Temporary rectangle to avoid instantiation. + private final Rect mTempRect = new Rect(); + + // Context in which this explorer operates. + private final Context mContext; + + // The X of the previous event. + private float mPreviousX; + + // The Y of the previous event. + private float mPreviousY; + + // Buffer for storing points for gesture detection. + private final ArrayList mStrokeBuffer = new ArrayList(100); + + // The minimal delta between moves to add a gesture point. + private static final int TOUCH_TOLERANCE = 3; + + // The minimal score for accepting a predicted gesture. + private static final float MIN_PREDICTION_SCORE = 2.0f; + + // The library for gesture detection. + private GestureLibrary mGestureLibrary; + + // The long pressing pointer id if coordinate remapping is needed. + private int mLongPressingPointerId = -1; + + // The long pressing pointer X if coordinate remapping is needed. + private int mLongPressingPointerDeltaX; + + // The long pressing pointer Y if coordinate remapping is needed. + private int mLongPressingPointerDeltaY; + + // The id of the last touch explored window. + private int mLastTouchedWindowId; + + // Whether touch exploration is in progress. + private boolean mTouchExplorationInProgress; + + /** + * Creates a new instance. + * + * @param inputFilter The input filter associated with this explorer. + * @param context A context handle for accessing resources. + */ + public TouchExplorer(Context context, AccessibilityManagerService service) { + mContext = context; + mAms = service; + mReceivedPointerTracker = new ReceivedPointerTracker(); + mInjectedPointerTracker = new InjectedPointerTracker(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout(); + mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + mHandler = new Handler(context.getMainLooper()); + mPerformLongPressDelayed = new PerformLongPressDelayed(); + mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed(); + mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures); + mGestureLibrary.setOrientationStyle(8); + mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE); + mGestureLibrary.load(); + mSendHoverEnterAndMoveDelayed = new SendHoverEnterAndMoveDelayed(); + mSendHoverExitDelayed = new SendHoverExitDelayed(); + mSendTouchExplorationEndDelayed = new SendAccessibilityEventDelayed( + AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END, + mDetermineUserIntentTimeout); + mSendTouchInteractionEndDelayed = new SendAccessibilityEventDelayed( + AccessibilityEvent.TYPE_TOUCH_INTERACTION_END, + mDetermineUserIntentTimeout); + mDoubleTapDetector = new DoubleTapDetector(); + final float density = context.getResources().getDisplayMetrics().density; + mScaledMinPointerDistanceToUseMiddleLocation = + (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density); + mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density); + } + + public void clear() { + // If we have not received an event then we are in initial + // state. Therefore, there is not need to clean anything. + MotionEvent event = mReceivedPointerTracker.getLastReceivedEvent(); + if (event != null) { + clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED); + } + } + + public void onDestroy() { + // TODO: Implement + } + + private void clear(MotionEvent event, int policyFlags) { + switch (mCurrentState) { + case STATE_TOUCH_EXPLORING: { + // If a touch exploration gesture is in progress send events for its end. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } break; + case STATE_DRAGGING: { + mDraggingPointerId = INVALID_POINTER_ID; + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_DELEGATING: { + // Send exit to all pointers that we have delivered. + sendUpForInjectedDownPointers(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + // Clear the current stroke. + mStrokeBuffer.clear(); + } break; + } + // Remove all pending callbacks. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + mExitGestureDetectionModeDelayed.cancel(); + mSendTouchExplorationEndDelayed.cancel(); + mSendTouchInteractionEndDelayed.cancel(); + // Reset the pointer trackers. + mReceivedPointerTracker.clear(); + mInjectedPointerTracker.clear(); + // Clear the double tap detector + mDoubleTapDetector.clear(); + // Go to initial state. + // Clear the long pressing pointer remap data. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + mCurrentState = STATE_TOUCH_EXPLORING; + if (mNext != null) { + mNext.clear(); + } + mTouchExplorationInProgress = false; + mAms.onTouchInteractionEnd(); + } + + @Override + public void setNext(EventStreamTransformation next) { + mNext = next; + } + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (DEBUG) { + Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState)); + } + + mReceivedPointerTracker.onMotionEvent(rawEvent); + + switch(mCurrentState) { + case STATE_TOUCH_EXPLORING: { + handleMotionEventStateTouchExploring(event, rawEvent, policyFlags); + } break; + case STATE_DRAGGING: { + handleMotionEventStateDragging(event, policyFlags); + } break; + case STATE_DELEGATING: { + handleMotionEventStateDelegating(event, policyFlags); + } break; + case STATE_GESTURE_DETECTING: { + handleMotionEventGestureDetecting(rawEvent, policyFlags); + } break; + default: + throw new IllegalStateException("Illegal state: " + mCurrentState); + } + } + + public void onAccessibilityEvent(AccessibilityEvent event) { + final int eventType = event.getEventType(); + + // The event for gesture end should be strictly after the + // last hover exit event. + if (mSendTouchExplorationEndDelayed.isPending() + && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { + mSendTouchExplorationEndDelayed.cancel(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END); + } + + // The event for touch interaction end should be strictly after the + // last hover exit and the touch exploration gesture end events. + if (mSendTouchInteractionEndDelayed.isPending() + && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { + mSendTouchInteractionEndDelayed.cancel(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + } + + // If a new window opens or the accessibility focus moves we no longer + // want to click/long press on the last touch explored location. + switch (eventType) { + case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { + if (mInjectedPointerTracker.mLastInjectedHoverEventForClick != null) { + mInjectedPointerTracker.mLastInjectedHoverEventForClick.recycle(); + mInjectedPointerTracker.mLastInjectedHoverEventForClick = null; + } + mLastTouchedWindowId = -1; + } break; + case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: + case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { + mLastTouchedWindowId = event.getWindowId(); + } break; + } + if (mNext != null) { + mNext.onAccessibilityEvent(event); + } + } + + /** + * Handles a motion event in touch exploring state. + * + * @param event The event to be handled. + * @param rawEvent The raw (unmodified) motion event. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateTouchExploring(MotionEvent event, MotionEvent rawEvent, + int policyFlags) { + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; + + mVelocityTracker.addMovement(rawEvent); + + mDoubleTapDetector.onMotionEvent(event, policyFlags); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + mAms.onTouchInteractionStart(); + + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(rawEvent, policyFlags); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START); + + // If we still have not notified the user for the last + // touch, we figure out what to do. If were waiting + // we resent the delayed callback and wait again. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + + if (mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.forceSendAndRemove(); + } + + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.forceSendAndRemove(); + } + + // If we have the first tap, schedule a long press and break + // since we do not want to schedule hover enter because + // the delayed callback will kick in before the long click. + // This would lead to a state transition resulting in long + // pressing the item below the double taped area which is + // not necessary where accessibility focus is. + if (mDoubleTapDetector.firstTapDetected()) { + // We got a tap now post a long press action. + mPerformLongPressDelayed.post(event, policyFlags); + break; + } + if (!mTouchExplorationInProgress) { + if (!mSendHoverEnterAndMoveDelayed.isPending()) { + // Deliver hover enter with a delay to have a chance + // to detect what the user is trying to do. + final int pointerId = receivedTracker.getPrimaryPointerId(); + final int pointerIdBits = (1 << pointerId); + mSendHoverEnterAndMoveDelayed.post(event, true, pointerIdBits, + policyFlags); + } else { + // Cache the event until we discern exploration from gesturing. + mSendHoverEnterAndMoveDelayed.addEvent(event); + } + } + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + // Another finger down means that if we have not started to deliver + // hover events, we will not have to. The code for ACTION_MOVE will + // decide what we will actually do next. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } break; + case MotionEvent.ACTION_MOVE: { + final int pointerId = receivedTracker.getPrimaryPointerId(); + final int pointerIndex = event.findPointerIndex(pointerId); + final int pointerIdBits = (1 << pointerId); + switch (event.getPointerCount()) { + case 1: { + // We have not started sending events since we try to + // figure out what the user is doing. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // Pre-feed the motion events to the gesture detector since we + // have a distance slop before getting into gesture detection + // mode and not using the points within this slop significantly + // decreases the quality of gesture recognition. + handleMotionEventGestureDetecting(rawEvent, policyFlags); + + // Cache the event until we discern exploration from gesturing. + mSendHoverEnterAndMoveDelayed.addEvent(event); + + // It is *important* to use the distance traveled by the pointers + // on the screen which may or may not be magnified. + final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId) + - rawEvent.getX(pointerIndex); + final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId) + - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + // The user has moved enough for us to decide. + if (moveDelta > mDoubleTapSlop) { + // Check whether the user is performing a gesture. We + // detect gestures if the pointer is moving above a + // given velocity. + mVelocityTracker.computeCurrentVelocity(1000); + final float maxAbsVelocity = Math.max( + Math.abs(mVelocityTracker.getXVelocity(pointerId)), + Math.abs(mVelocityTracker.getYVelocity(pointerId))); + if (maxAbsVelocity > mScaledGestureDetectionVelocity) { + // We have to perform gesture detection, so + // clear the current state and try to detect. + mCurrentState = STATE_GESTURE_DETECTING; + mVelocityTracker.clear(); + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + mExitGestureDetectionModeDelayed.post(); + // Send accessibility event to announce the start + // of gesture recognition. + sendAccessibilityEvent( + AccessibilityEvent.TYPE_GESTURE_DETECTION_START); + } else { + // We have just decided that the user is touch, + // exploring so start sending events. + mSendHoverEnterAndMoveDelayed.forceSendAndRemove(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, + pointerIdBits, policyFlags); + } + break; + } + } else { + // Cancel the long press if pending and the user + // moved more than the slop. + if (mPerformLongPressDelayed.isPending()) { + final float deltaX = + receivedTracker.getReceivedPointerDownX(pointerId) + - rawEvent.getX(pointerIndex); + final float deltaY = + receivedTracker.getReceivedPointerDownY(pointerId) + - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + // The user has moved enough for us to decide. + if (moveDelta > mTouchSlop) { + mPerformLongPressDelayed.cancel(); + } + } + if (mTouchExplorationInProgress) { + sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, + policyFlags); + } + } + } break; + case 2: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } else { + mPerformLongPressDelayed.cancel(); + if (mTouchExplorationInProgress) { + // If the user is touch exploring the second pointer may be + // performing a double tap to activate an item without need + // for the user to lift his exploring finger. + // It is *important* to use the distance traveled by the pointers + // on the screen which may or may not be magnified. + final float deltaX = receivedTracker.getReceivedPointerDownX( + pointerId) - rawEvent.getX(pointerIndex); + final float deltaY = receivedTracker.getReceivedPointerDownY( + pointerId) - rawEvent.getY(pointerIndex); + final double moveDelta = Math.hypot(deltaX, deltaY); + if (moveDelta < mDoubleTapSlop) { + break; + } + // We are sending events so send exit and gesture + // end since we transition to another state. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + } + + // We know that a new state transition is to happen and the + // new state will not be gesture recognition, so clear the + // stashed gesture strokes. + mStrokeBuffer.clear(); + + if (isDraggingGesture(event)) { + // Two pointers moving in the same direction within + // a given distance perform a drag. + mCurrentState = STATE_DRAGGING; + mDraggingPointerId = pointerId; + event.setEdgeFlags(receivedTracker.getLastReceivedDownEdgeFlags()); + sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, + policyFlags); + } else { + // Two pointers moving arbitrary are delegated to the view hierarchy. + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(event, policyFlags); + } + mVelocityTracker.clear(); + } break; + default: { + // More than one pointer so the user is not touch exploring + // and now we have to decide whether to delegate or drag. + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // We have not started sending events so cancel + // scheduled sending events. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + } else { + mPerformLongPressDelayed.cancel(); + // We are sending events so send exit and gesture + // end since we transition to another state. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + + // More than two pointers are delegated to the view hierarchy. + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(event, policyFlags); + mVelocityTracker.clear(); + } + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // We know that we do not need the pre-fed gesture points are not + // needed anymore since the last pointer just went up. + mStrokeBuffer.clear(); + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + + mPerformLongPressDelayed.cancel(); + mVelocityTracker.clear(); + + if (mSendHoverEnterAndMoveDelayed.isPending()) { + // If we have not delivered the enter schedule an exit. + mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags); + } else { + // The user is touch exploring so we send events for end. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } + + if (!mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.post(); + } + + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Handles a motion event in dragging state. + * + * @param event The event to be handled. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateDragging(MotionEvent event, int policyFlags) { + final int pointerIdBits = (1 << mDraggingPointerId); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalStateException("Dragging state can be reached only if two " + + "pointers are already down"); + } + case MotionEvent.ACTION_POINTER_DOWN: { + // We are in dragging state so we have two pointers and another one + // goes down => delegate the three pointers to the view hierarchy + mCurrentState = STATE_DELEGATING; + if (mDraggingPointerId != INVALID_POINTER_ID) { + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + sendDownForAllNotInjectedPointers(event, policyFlags); + } break; + case MotionEvent.ACTION_MOVE: { + switch (event.getPointerCount()) { + case 1: { + // do nothing + } break; + case 2: { + if (isDraggingGesture(event)) { + final float firstPtrX = event.getX(0); + final float firstPtrY = event.getY(0); + final float secondPtrX = event.getX(1); + final float secondPtrY = event.getY(1); + + final float deltaX = firstPtrX - secondPtrX; + final float deltaY = firstPtrY - secondPtrY; + final double distance = Math.hypot(deltaX, deltaY); + + if (distance > mScaledMinPointerDistanceToUseMiddleLocation) { + event.setLocation(deltaX / 2, deltaY / 2); + } + + // If still dragging send a drag event. + sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits, + policyFlags); + } else { + // The two pointers are moving either in different directions or + // no close enough => delegate the gesture to the view hierarchy. + mCurrentState = STATE_DELEGATING; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, + policyFlags); + // Deliver all pointers to the view hierarchy. + sendDownForAllNotInjectedPointers(event, policyFlags); + } + } break; + default: { + mCurrentState = STATE_DELEGATING; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, + policyFlags); + // Deliver all pointers to the view hierarchy. + sendDownForAllNotInjectedPointers(event, policyFlags); + } + } + } break; + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = event.getPointerId(event.getActionIndex()); + if (pointerId == mDraggingPointerId) { + mDraggingPointerId = INVALID_POINTER_ID; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // Announce the end of a new touch interaction. + sendAccessibilityEvent( + AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + final int pointerId = event.getPointerId(event.getActionIndex()); + if (pointerId == mDraggingPointerId) { + mDraggingPointerId = INVALID_POINTER_ID; + // Send an event to the end of the drag gesture. + sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Handles a motion event in delegating state. + * + * @param event The event to be handled. + * @param policyFlags The policy flags associated with the event. + */ + private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + throw new IllegalStateException("Delegating state can only be reached if " + + "there is at least one pointer down!"); + } + case MotionEvent.ACTION_UP: { + // Offset the event if we are doing a long press as the + // target is not necessarily under the user's finger. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, - mLongPressingPointerDeltaX, + - mLongPressingPointerDeltaY); + // Clear the long press state. + mLongPressingPointerId = -1; + mLongPressingPointerDeltaX = 0; + mLongPressingPointerDeltaY = 0; + } + + // Deliver the event. + sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); + + // Announce the end of a the touch interaction. + mAms.onTouchInteractionEnd(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + default: { + // Deliver the event. + sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); + } + } + } + + private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + final float x = event.getX(); + final float y = event.getY(); + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } break; + case MotionEvent.ACTION_MOVE: { + final float x = event.getX(); + final float y = event.getY(); + final float dX = Math.abs(x - mPreviousX); + final float dY = Math.abs(y - mPreviousY); + if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) { + mPreviousX = x; + mPreviousY = y; + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + } + } break; + case MotionEvent.ACTION_UP: { + mAms.onTouchInteractionEnd(); + // Announce the end of the gesture recognition. + sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); + // Announce the end of a the touch interaction. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); + + float x = event.getX(); + float y = event.getY(); + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + + Gesture gesture = new Gesture(); + gesture.addStroke(new GestureStroke(mStrokeBuffer)); + + ArrayList predictions = mGestureLibrary.recognize(gesture); + if (!predictions.isEmpty()) { + Prediction bestPrediction = predictions.get(0); + if (bestPrediction.score >= MIN_PREDICTION_SCORE) { + if (DEBUG) { + Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: " + + bestPrediction.score); + } + try { + final int gestureId = Integer.parseInt(bestPrediction.name); + mAms.onGesture(gestureId); + } catch (NumberFormatException nfe) { + Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name); + } + } + } + + mStrokeBuffer.clear(); + mExitGestureDetectionModeDelayed.cancel(); + mCurrentState = STATE_TOUCH_EXPLORING; + } break; + case MotionEvent.ACTION_CANCEL: { + clear(event, policyFlags); + } break; + } + } + + /** + * Sends an accessibility event of the given type. + * + * @param type The event type. + */ + private void sendAccessibilityEvent(int type) { + AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(type); + event.setWindowId(mAms.getActiveWindowId()); + accessibilityManager.sendAccessibilityEvent(event); + switch (type) { + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: { + mTouchExplorationInProgress = true; + } break; + case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: { + mTouchExplorationInProgress = false; + } break; + } + } + } + + /** + * Sends down events to the view hierarchy for all pointers which are + * not already being delivered i.e. pointers that are not yet injected. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendDownForAllNotInjectedPointers(MotionEvent prototype, int policyFlags) { + InjectedPointerTracker injectedPointers = mInjectedPointerTracker; + + // Inject the injected pointers. + int pointerIdBits = 0; + final int pointerCount = prototype.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = prototype.getPointerId(i); + // Do not send event for already delivered pointers. + if (!injectedPointers.isInjectedPointerDown(pointerId)) { + pointerIdBits |= (1 << pointerId); + final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); + sendMotionEvent(prototype, action, pointerIdBits, policyFlags); + } + } + } + + /** + * Sends the exit events if needed. Such events are hover exit and touch explore + * gesture end. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + if (!mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.post(); + } + sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); + } + } + + /** + * Sends the enter events if needed. Such events are hover enter and touch explore + * gesture start. + * + * @param policyFlags The policy flags associated with the event. + */ + private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) { + MotionEvent event = mInjectedPointerTracker.getLastInjectedHoverEvent(); + if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { + final int pointerIdBits = event.getPointerIdBits(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); + } + } + + /** + * Sends up events to the view hierarchy for all pointers which are + * already being delivered i.e. pointers that are injected. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) { + final InjectedPointerTracker injectedTracked = mInjectedPointerTracker; + int pointerIdBits = 0; + final int pointerCount = prototype.getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = prototype.getPointerId(i); + // Skip non injected down pointers. + if (!injectedTracked.isInjectedPointerDown(pointerId)) { + continue; + } + pointerIdBits |= (1 << pointerId); + final int action = computeInjectionAction(MotionEvent.ACTION_UP, i); + sendMotionEvent(prototype, action, pointerIdBits, policyFlags); + } + } + + /** + * Sends an up and down events. + * + * @param prototype The prototype from which to create the injected events. + * @param policyFlags The policy flags associated with the event. + */ + private void sendActionDownAndUp(MotionEvent prototype, int policyFlags) { + // Tap with the pointer that last explored. + final int pointerId = prototype.getPointerId(prototype.getActionIndex()); + final int pointerIdBits = (1 << pointerId); + sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); + sendMotionEvent(prototype, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); + } + + /** + * Sends an event. + * + * @param prototype The prototype from which to create the injected events. + * @param action The action of the event. + * @param pointerIdBits The bits of the pointers to send. + * @param policyFlags The policy flags associated with the event. + */ + private void sendMotionEvent(MotionEvent prototype, int action, int pointerIdBits, + int policyFlags) { + prototype.setAction(action); + + MotionEvent event = null; + if (pointerIdBits == ALL_POINTER_ID_BITS) { + event = prototype; + } else { + event = prototype.split(pointerIdBits); + } + if (action == MotionEvent.ACTION_DOWN) { + event.setDownTime(event.getEventTime()); + } else { + event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime()); + } + + // If the user is long pressing but the long pressing pointer + // was not exactly over the accessibility focused item we need + // to remap the location of that pointer so the user does not + // have to explicitly touch explore something to be able to + // long press it, or even worse to avoid the user long pressing + // on the wrong item since click and long press behave differently. + if (mLongPressingPointerId >= 0) { + event = offsetEvent(event, - mLongPressingPointerDeltaX, + - mLongPressingPointerDeltaY); + } + + if (DEBUG) { + Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + + Integer.toHexString(policyFlags)); + } + + // Make sure that the user will see the event. + policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; + if (mNext != null) { + // TODO: For now pass null for the raw event since the touch + // explorer is the last event transformation and it does + // not care about the raw event. + mNext.onMotionEvent(event, null, policyFlags); + } + + mInjectedPointerTracker.onMotionEvent(event); + + if (event != prototype) { + event.recycle(); + } + } + + /** + * Offsets all pointers in the given event by adding the specified X and Y + * offsets. + * + * @param event The event to offset. + * @param offsetX The X offset. + * @param offsetY The Y offset. + * @return An event with the offset pointers or the original event if both + * offsets are zero. + */ + private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) { + if (offsetX == 0 && offsetY == 0) { + return event; + } + final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); + final int pointerCount = event.getPointerCount(); + PointerProperties[] props = PointerProperties.createArray(pointerCount); + PointerCoords[] coords = PointerCoords.createArray(pointerCount); + for (int i = 0; i < pointerCount; i++) { + event.getPointerProperties(i, props[i]); + event.getPointerCoords(i, coords[i]); + if (i == remappedIndex) { + coords[i].x += offsetX; + coords[i].y += offsetY; + } + } + return MotionEvent.obtain(event.getDownTime(), + event.getEventTime(), event.getAction(), event.getPointerCount(), + props, coords, event.getMetaState(), event.getButtonState(), + 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), + event.getSource(), event.getFlags()); + } + + /** + * Computes the action for an injected event based on a masked action + * and a pointer index. + * + * @param actionMasked The masked action. + * @param pointerIndex The index of the pointer which has changed. + * @return The action to be used for injection. + */ + private int computeInjectionAction(int actionMasked, int pointerIndex) { + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; + // Compute the action based on how many down pointers are injected. + if (injectedTracker.getInjectedPointerDownCount() == 0) { + return MotionEvent.ACTION_DOWN; + } else { + return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + | MotionEvent.ACTION_POINTER_DOWN; + } + } + case MotionEvent.ACTION_POINTER_UP: { + InjectedPointerTracker injectedTracker = mInjectedPointerTracker; + // Compute the action based on how many down pointers are injected. + if (injectedTracker.getInjectedPointerDownCount() == 1) { + return MotionEvent.ACTION_UP; + } else { + return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + | MotionEvent.ACTION_POINTER_UP; + } + } + default: + return actionMasked; + } + } + + private class DoubleTapDetector { + private MotionEvent mDownEvent; + private MotionEvent mFirstTapEvent; + + public void onMotionEvent(MotionEvent event, int policyFlags) { + final int actionIndex = event.getActionIndex(); + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + if (mFirstTapEvent != null + && !GestureUtils.isSamePointerContext(mFirstTapEvent, event)) { + clear(); + } + mDownEvent = MotionEvent.obtain(event); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + if (mDownEvent == null) { + return; + } + if (!GestureUtils.isSamePointerContext(mDownEvent, event)) { + clear(); + return; + } + if (GestureUtils.isTap(mDownEvent, event, mTapTimeout, mTouchSlop, + actionIndex)) { + if (mFirstTapEvent == null || GestureUtils.isTimedOut(mFirstTapEvent, + event, mDoubleTapTimeout)) { + mFirstTapEvent = MotionEvent.obtain(event); + mDownEvent.recycle(); + mDownEvent = null; + return; + } + if (GestureUtils.isMultiTap(mFirstTapEvent, event, mDoubleTapTimeout, + mDoubleTapSlop, actionIndex)) { + onDoubleTap(event, policyFlags); + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + mDownEvent.recycle(); + mDownEvent = null; + return; + } + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } else { + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + mDownEvent.recycle(); + mDownEvent = null; + } break; + } + } + + public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) { + // This should never be called when more than two pointers are down. + if (secondTapUp.getPointerCount() > 2) { + return; + } + + // Remove pending event deliveries. + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + mPerformLongPressDelayed.cancel(); + + if (mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.forceSendAndRemove(); + } + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.forceSendAndRemove(); + } + + int clickLocationX; + int clickLocationY; + + final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex()); + final int pointerIndex = secondTapUp.findPointerIndex(pointerId); + + MotionEvent lastExploreEvent = + mInjectedPointerTracker.getLastInjectedHoverEventForClick(); + if (lastExploreEvent == null) { + // No last touch explored event but there is accessibility focus in + // the active window. We click in the middle of the focus bounds. + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } else { + // Out of luck - do nothing. + return; + } + } else { + // If the click is within the active window but not within the + // accessibility focus bounds we click in the focus center. + final int lastExplorePointerIndex = lastExploreEvent.getActionIndex(); + clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex); + clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex); + Rect activeWindowBounds = mTempRect; + if (mLastTouchedWindowId == mAms.getActiveWindowId()) { + mAms.getActiveWindowBounds(activeWindowBounds); + if (activeWindowBounds.contains(clickLocationX, clickLocationY)) { + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + if (!focusBounds.contains(clickLocationX, clickLocationY)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } + } + } + } + } + + // Do the click. + PointerProperties[] properties = new PointerProperties[1]; + properties[0] = new PointerProperties(); + secondTapUp.getPointerProperties(pointerIndex, properties[0]); + PointerCoords[] coords = new PointerCoords[1]; + coords[0] = new PointerCoords(); + coords[0].x = clickLocationX; + coords[0].y = clickLocationY; + MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(), + secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties, + coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0, + secondTapUp.getSource(), secondTapUp.getFlags()); + sendActionDownAndUp(event, policyFlags); + event.recycle(); + } + + public void clear() { + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + if (mFirstTapEvent != null) { + mFirstTapEvent.recycle(); + mFirstTapEvent = null; + } + } + + public boolean firstTapDetected() { + return mFirstTapEvent != null + && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout; + } + } + + /** + * Determines whether a two pointer gesture is a dragging one. + * + * @param event The event with the pointer data. + * @return True if the gesture is a dragging one. + */ + private boolean isDraggingGesture(MotionEvent event) { + ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; + + final float firstPtrX = event.getX(0); + final float firstPtrY = event.getY(0); + final float secondPtrX = event.getX(1); + final float secondPtrY = event.getY(1); + + final float firstPtrDownX = receivedTracker.getReceivedPointerDownX(0); + final float firstPtrDownY = receivedTracker.getReceivedPointerDownY(0); + final float secondPtrDownX = receivedTracker.getReceivedPointerDownX(1); + final float secondPtrDownY = receivedTracker.getReceivedPointerDownY(1); + + return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX, + secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY, + MAX_DRAGGING_ANGLE_COS); + } + + /** + * Gets the symbolic name of a state. + * + * @param state A state. + * @return The state symbolic name. + */ + private static String getStateSymbolicName(int state) { + switch (state) { + case STATE_TOUCH_EXPLORING: + return "STATE_TOUCH_EXPLORING"; + case STATE_DRAGGING: + return "STATE_DRAGGING"; + case STATE_DELEGATING: + return "STATE_DELEGATING"; + case STATE_GESTURE_DETECTING: + return "STATE_GESTURE_DETECTING"; + default: + throw new IllegalArgumentException("Unknown state: " + state); + } + } + + /** + * Class for delayed exiting from gesture detecting mode. + */ + private final class ExitGestureDetectionModeDelayed implements Runnable { + + public void post() { + mHandler.postDelayed(this, EXIT_GESTURE_DETECTION_TIMEOUT); + } + + public void cancel() { + mHandler.removeCallbacks(this); + } + + @Override + public void run() { + // Announce the end of gesture recognition. + sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); + // Clearing puts is in touch exploration state with a finger already + // down, so announce the transition to exploration state. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + clear(); + } + } + + /** + * Class for delayed sending of long press. + */ + private final class PerformLongPressDelayed implements Runnable { + private MotionEvent mEvent; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int policyFlags) { + mEvent = MotionEvent.obtain(prototype); + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout()); + } + + public void cancel() { + if (mEvent != null) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + @Override + public void run() { + // Pointers should not be zero when running this command. + if (mReceivedPointerTracker.getLastReceivedEvent().getPointerCount() == 0) { + return; + } + + int clickLocationX; + int clickLocationY; + + final int pointerId = mEvent.getPointerId(mEvent.getActionIndex()); + final int pointerIndex = mEvent.findPointerIndex(pointerId); + + MotionEvent lastExploreEvent = + mInjectedPointerTracker.getLastInjectedHoverEventForClick(); + if (lastExploreEvent == null) { + // No last touch explored event but there is accessibility focus in + // the active window. We click in the middle of the focus bounds. + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } else { + // Out of luck - do nothing. + return; + } + } else { + // If the click is within the active window but not within the + // accessibility focus bounds we click in the focus center. + final int lastExplorePointerIndex = lastExploreEvent.getActionIndex(); + clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex); + clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex); + Rect activeWindowBounds = mTempRect; + if (mLastTouchedWindowId == mAms.getActiveWindowId()) { + mAms.getActiveWindowBounds(activeWindowBounds); + if (activeWindowBounds.contains(clickLocationX, clickLocationY)) { + Rect focusBounds = mTempRect; + if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) { + if (!focusBounds.contains(clickLocationX, clickLocationY)) { + clickLocationX = focusBounds.centerX(); + clickLocationY = focusBounds.centerY(); + } + } + } + } + } + + mLongPressingPointerId = pointerId; + mLongPressingPointerDeltaX = (int) mEvent.getX(pointerIndex) - clickLocationX; + mLongPressingPointerDeltaY = (int) mEvent.getY(pointerIndex) - clickLocationY; + + sendHoverExitAndTouchExplorationGestureEndIfNeeded(mPolicyFlags); + + mCurrentState = STATE_DELEGATING; + sendDownForAllNotInjectedPointers(mEvent, mPolicyFlags); + clear(); + } + + private void clear() { + mEvent.recycle(); + mEvent = null; + mPolicyFlags = 0; + } + } + + /** + * Class for delayed sending of hover enter and move events. + */ + class SendHoverEnterAndMoveDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverEnterAndMoveDelayed"; + + private final List mEvents = new ArrayList(); + + private int mPointerIdBits; + private int mPolicyFlags; + + public void post(MotionEvent event, boolean touchExplorationInProgress, + int pointerIdBits, int policyFlags) { + cancel(); + addEvent(event); + mPointerIdBits = pointerIdBits; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, mDetermineUserIntentTimeout); + } + + public void addEvent(MotionEvent event) { + mEvents.add(MotionEvent.obtain(event)); + } + + public void cancel() { + if (isPending()) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + private void clear() { + mPointerIdBits = -1; + mPolicyFlags = 0; + final int eventCount = mEvents.size(); + for (int i = eventCount - 1; i >= 0; i--) { + mEvents.remove(i).recycle(); + } + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + public void run() { + // Send an accessibility event to announce the touch exploration start. + sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); + + if (!mEvents.isEmpty()) { + // Deliver a down event. + sendMotionEvent(mEvents.get(0), MotionEvent.ACTION_HOVER_ENTER, + mPointerIdBits, mPolicyFlags); + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, + "Injecting motion event: ACTION_HOVER_ENTER"); + } + + // Deliver move events. + final int eventCount = mEvents.size(); + for (int i = 1; i < eventCount; i++) { + sendMotionEvent(mEvents.get(i), MotionEvent.ACTION_HOVER_MOVE, + mPointerIdBits, mPolicyFlags); + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, + "Injecting motion event: ACTION_HOVER_MOVE"); + } + } + } + clear(); + } + } + + /** + * Class for delayed sending of hover exit events. + */ + class SendHoverExitDelayed implements Runnable { + private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverExitDelayed"; + + private MotionEvent mPrototype; + private int mPointerIdBits; + private int mPolicyFlags; + + public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) { + cancel(); + mPrototype = MotionEvent.obtain(prototype); + mPointerIdBits = pointerIdBits; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, mDetermineUserIntentTimeout); + } + + public void cancel() { + if (isPending()) { + mHandler.removeCallbacks(this); + clear(); + } + } + + private boolean isPending() { + return mHandler.hasCallbacks(this); + } + + private void clear() { + mPrototype.recycle(); + mPrototype = null; + mPointerIdBits = -1; + mPolicyFlags = 0; + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + public void run() { + if (DEBUG) { + Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event:" + + " ACTION_HOVER_EXIT"); + } + sendMotionEvent(mPrototype, MotionEvent.ACTION_HOVER_EXIT, + mPointerIdBits, mPolicyFlags); + if (!mSendTouchExplorationEndDelayed.isPending()) { + mSendTouchExplorationEndDelayed.cancel(); + mSendTouchExplorationEndDelayed.post(); + } + if (mSendTouchInteractionEndDelayed.isPending()) { + mSendTouchInteractionEndDelayed.cancel(); + mSendTouchInteractionEndDelayed.post(); + } + clear(); + } + } + + private class SendAccessibilityEventDelayed implements Runnable { + private final int mEventType; + private final int mDelay; + + public SendAccessibilityEventDelayed(int eventType, int delay) { + mEventType = eventType; + mDelay = delay; + } + + public void cancel() { + mHandler.removeCallbacks(this); + } + + public void post() { + mHandler.postDelayed(this, mDelay); + } + + public boolean isPending() { + return mHandler.hasCallbacks(this); + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + @Override + public void run() { + sendAccessibilityEvent(mEventType); + } + } + + @Override + public String toString() { + return LOG_TAG; + } + + class InjectedPointerTracker { + private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker"; + + // Keep track of which pointers sent to the system are down. + private int mInjectedPointersDown; + + // The time of the last injected down. + private long mLastInjectedDownEventTime; + + // The last injected hover event. + private MotionEvent mLastInjectedHoverEvent; + + // The last injected hover event used for performing clicks. + private MotionEvent mLastInjectedHoverEventForClick; + + /** + * Processes an injected {@link MotionEvent} event. + * + * @param event The event to process. + */ + public void onMotionEvent(MotionEvent event) { + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown |= pointerFlag; + mLastInjectedDownEventTime = event.getDownTime(); + } break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: { + final int pointerId = event.getPointerId(event.getActionIndex()); + final int pointerFlag = (1 << pointerId); + mInjectedPointersDown &= ~pointerFlag; + if (mInjectedPointersDown == 0) { + mLastInjectedDownEventTime = 0; + } + } break; + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: { + if (mLastInjectedHoverEvent != null) { + mLastInjectedHoverEvent.recycle(); + } + mLastInjectedHoverEvent = MotionEvent.obtain(event); + if (mLastInjectedHoverEventForClick != null) { + mLastInjectedHoverEventForClick.recycle(); + } + mLastInjectedHoverEventForClick = MotionEvent.obtain(event); + } break; + } + if (DEBUG) { + Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString()); + } + } + + /** + * Clears the internals state. + */ + public void clear() { + mInjectedPointersDown = 0; + } + + /** + * @return The time of the last injected down event. + */ + public long getLastInjectedDownEventTime() { + return mLastInjectedDownEventTime; + } + + /** + * @return The number of down pointers injected to the view hierarchy. + */ + public int getInjectedPointerDownCount() { + return Integer.bitCount(mInjectedPointersDown); + } + + /** + * @return The bits of the injected pointers that are down. + */ + public int getInjectedPointersDown() { + return mInjectedPointersDown; + } + + /** + * Whether an injected pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isInjectedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mInjectedPointersDown & pointerFlag) != 0; + } + + /** + * @return The the last injected hover event. + */ + public MotionEvent getLastInjectedHoverEvent() { + return mLastInjectedHoverEvent; + } + + /** + * @return The the last injected hover event. + */ + public MotionEvent getLastInjectedHoverEventForClick() { + return mLastInjectedHoverEventForClick; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("========================="); + builder.append("\nDown pointers #"); + builder.append(Integer.bitCount(mInjectedPointersDown)); + builder.append(" [ "); + for (int i = 0; i < MAX_POINTER_COUNT; i++) { + if ((mInjectedPointersDown & i) != 0) { + builder.append(i); + builder.append(" "); + } + } + builder.append("]"); + builder.append("\n========================="); + return builder.toString(); + } + } + + class ReceivedPointerTracker { + private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker"; + + // Keep track of where and when a pointer went down. + private final float[] mReceivedPointerDownX = new float[MAX_POINTER_COUNT]; + private final float[] mReceivedPointerDownY = new float[MAX_POINTER_COUNT]; + private final long[] mReceivedPointerDownTime = new long[MAX_POINTER_COUNT]; + + // Which pointers are down. + private int mReceivedPointersDown; + + // The edge flags of the last received down event. + private int mLastReceivedDownEdgeFlags; + + // Primary pointer which is either the first that went down + // or if it goes up the next one that most recently went down. + private int mPrimaryPointerId; + + // Keep track of the last up pointer data. + private long mLastReceivedUpPointerDownTime; + private float mLastReceivedUpPointerDownX; + private float mLastReceivedUpPointerDownY; + + private MotionEvent mLastReceivedEvent; + + /** + * Clears the internals state. + */ + public void clear() { + Arrays.fill(mReceivedPointerDownX, 0); + Arrays.fill(mReceivedPointerDownY, 0); + Arrays.fill(mReceivedPointerDownTime, 0); + mReceivedPointersDown = 0; + mPrimaryPointerId = 0; + mLastReceivedUpPointerDownTime = 0; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownY = 0; + } + + /** + * Processes a received {@link MotionEvent} event. + * + * @param event The event to process. + */ + public void onMotionEvent(MotionEvent event) { + if (mLastReceivedEvent != null) { + mLastReceivedEvent.recycle(); + } + mLastReceivedEvent = MotionEvent.obtain(event); + + final int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + handleReceivedPointerDown(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_POINTER_DOWN: { + handleReceivedPointerDown(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_UP: { + handleReceivedPointerUp(event.getActionIndex(), event); + } break; + case MotionEvent.ACTION_POINTER_UP: { + handleReceivedPointerUp(event.getActionIndex(), event); + } break; + } + if (DEBUG) { + Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer:\n" + toString()); + } + } + + /** + * @return The last received event. + */ + public MotionEvent getLastReceivedEvent() { + return mLastReceivedEvent; + } + + /** + * @return The number of received pointers that are down. + */ + public int getReceivedPointerDownCount() { + return Integer.bitCount(mReceivedPointersDown); + } + + /** + * Whether an received pointer is down. + * + * @param pointerId The unique pointer id. + * @return True if the pointer is down. + */ + public boolean isReceivedPointerDown(int pointerId) { + final int pointerFlag = (1 << pointerId); + return (mReceivedPointersDown & pointerFlag) != 0; + } + + /** + * @param pointerId The unique pointer id. + * @return The X coordinate where the pointer went down. + */ + public float getReceivedPointerDownX(int pointerId) { + return mReceivedPointerDownX[pointerId]; + } + + /** + * @param pointerId The unique pointer id. + * @return The Y coordinate where the pointer went down. + */ + public float getReceivedPointerDownY(int pointerId) { + return mReceivedPointerDownY[pointerId]; + } + + /** + * @param pointerId The unique pointer id. + * @return The time when the pointer went down. + */ + public long getReceivedPointerDownTime(int pointerId) { + return mReceivedPointerDownTime[pointerId]; + } + + /** + * @return The id of the primary pointer. + */ + public int getPrimaryPointerId() { + if (mPrimaryPointerId == INVALID_POINTER_ID) { + mPrimaryPointerId = findPrimaryPointerId(); + } + return mPrimaryPointerId; + } + + /** + * @return The time when the last up received pointer went down. + */ + public long getLastReceivedUpPointerDownTime() { + return mLastReceivedUpPointerDownTime; + } + + /** + * @return The down X of the last received pointer that went up. + */ + public float getLastReceivedUpPointerDownX() { + return mLastReceivedUpPointerDownX; + } + + /** + * @return The down Y of the last received pointer that went up. + */ + public float getLastReceivedUpPointerDownY() { + return mLastReceivedUpPointerDownY; + } + + /** + * @return The edge flags of the last received down event. + */ + public int getLastReceivedDownEdgeFlags() { + return mLastReceivedDownEdgeFlags; + } + + /** + * Handles a received pointer down event. + * + * @param pointerIndex The index of the pointer that has changed. + * @param event The event to be handled. + */ + private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) { + final int pointerId = event.getPointerId(pointerIndex); + final int pointerFlag = (1 << pointerId); + + mLastReceivedUpPointerDownTime = 0; + mLastReceivedUpPointerDownX = 0; + mLastReceivedUpPointerDownX = 0; + + mLastReceivedDownEdgeFlags = event.getEdgeFlags(); + + mReceivedPointersDown |= pointerFlag; + mReceivedPointerDownX[pointerId] = event.getX(pointerIndex); + mReceivedPointerDownY[pointerId] = event.getY(pointerIndex); + mReceivedPointerDownTime[pointerId] = event.getEventTime(); + + mPrimaryPointerId = pointerId; + } + + /** + * Handles a received pointer up event. + * + * @param pointerIndex The index of the pointer that has changed. + * @param event The event to be handled. + */ + private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) { + final int pointerId = event.getPointerId(pointerIndex); + final int pointerFlag = (1 << pointerId); + + mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId); + mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId]; + mLastReceivedUpPointerDownY = mReceivedPointerDownY[pointerId]; + + mReceivedPointersDown &= ~pointerFlag; + mReceivedPointerDownX[pointerId] = 0; + mReceivedPointerDownY[pointerId] = 0; + mReceivedPointerDownTime[pointerId] = 0; + + if (mPrimaryPointerId == pointerId) { + mPrimaryPointerId = INVALID_POINTER_ID; + } + } + + /** + * @return The primary pointer id. + */ + private int findPrimaryPointerId() { + int primaryPointerId = INVALID_POINTER_ID; + long minDownTime = Long.MAX_VALUE; + + // Find the pointer that went down first. + int pointerIdBits = mReceivedPointersDown; + while (pointerIdBits > 0) { + final int pointerId = Integer.numberOfTrailingZeros(pointerIdBits); + pointerIdBits &= ~(1 << pointerId); + final long downPointerTime = mReceivedPointerDownTime[pointerId]; + if (downPointerTime < minDownTime) { + minDownTime = downPointerTime; + primaryPointerId = pointerId; + } + } + return primaryPointerId; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("========================="); + builder.append("\nDown pointers #"); + builder.append(getReceivedPointerDownCount()); + builder.append(" [ "); + for (int i = 0; i < MAX_POINTER_COUNT; i++) { + if (isReceivedPointerDown(i)) { + builder.append(i); + builder.append(" "); + } + } + builder.append("]"); + builder.append("\nPrimary pointer id [ "); + builder.append(getPrimaryPointerId()); + builder.append(" ]"); + builder.append("\n========================="); + return builder.toString(); + } + } +} diff --git a/services/accessibility/java/service.mk b/services/accessibility/java/service.mk new file mode 100644 index 0000000..5e8f375 --- /dev/null +++ b/services/accessibility/java/service.mk @@ -0,0 +1,11 @@ +# Include only if the service is required +ifneq ($(findstring accessibility,$(REQUIRED_SERVICES)),) + +SUB_DIR := accessibility/java + +LOCAL_SRC_FILES += \ + $(call all-java-files-under,$(SUB_DIR)) + +#DEFINED_SERVICES += com.android.server.accessibility.AccessibilityManagerService + +endif diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java new file mode 100644 index 0000000..6fd8871 --- /dev/null +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetService.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appwidget; + +import android.app.ActivityManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import com.android.internal.appwidget.IAppWidgetHost; +import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.os.BackgroundThread; +import com.android.internal.util.IndentingPrintWriter; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.Locale; + + +/** + * Redirects calls to this service to the instance of the service for the appropriate user. + */ +public class AppWidgetService extends IAppWidgetService.Stub +{ + private static final String TAG = "AppWidgetService"; + + Context mContext; + Locale mLocale; + PackageManager mPackageManager; + boolean mSafeMode; + private final Handler mSaveStateHandler; + + private final SparseArray mAppWidgetServices; + + public AppWidgetService(Context context) { + mContext = context; + + mSaveStateHandler = BackgroundThread.getHandler(); + + mAppWidgetServices = new SparseArray(5); + AppWidgetServiceImpl primary = new AppWidgetServiceImpl(context, 0, mSaveStateHandler); + mAppWidgetServices.append(0, primary); + } + + public void systemRunning(boolean safeMode) { + mSafeMode = safeMode; + + mAppWidgetServices.get(0).systemReady(safeMode); + + // Register for the boot completed broadcast, so we can send the + // ENABLE broacasts. If we try to send them now, they time out, + // because the system isn't ready to handle them yet. + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null); + + // Register for configuration changes so we can update the names + // of the widgets when the locale changes. + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED), null, null); + + // Register for broadcasts about package install, etc., so we can + // update the provider list. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + filter, null, null); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, + sdFilter, null, null); + + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + userFilter.addAction(Intent.ACTION_USER_STOPPING); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) { + onUserRemoved(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL)); + } else if (Intent.ACTION_USER_STOPPING.equals(intent.getAction())) { + onUserStopping(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL)); + } + } + }, userFilter); + } + + @Override + public int allocateAppWidgetId(String packageName, int hostId, int userId) + throws RemoteException { + return getImplForUser(userId).allocateAppWidgetId(packageName, hostId); + } + + @Override + public int[] getAppWidgetIdsForHost(int hostId, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetIdsForHost(hostId); + } + + @Override + public void deleteAppWidgetId(int appWidgetId, int userId) throws RemoteException { + getImplForUser(userId).deleteAppWidgetId(appWidgetId); + } + + @Override + public void deleteHost(int hostId, int userId) throws RemoteException { + getImplForUser(userId).deleteHost(hostId); + } + + @Override + public void deleteAllHosts(int userId) throws RemoteException { + getImplForUser(userId).deleteAllHosts(); + } + + @Override + public void bindAppWidgetId(int appWidgetId, ComponentName provider, Bundle options, int userId) + throws RemoteException { + getImplForUser(userId).bindAppWidgetId(appWidgetId, provider, options); + } + + @Override + public boolean bindAppWidgetIdIfAllowed( + String packageName, int appWidgetId, ComponentName provider, Bundle options, int userId) + throws RemoteException { + return getImplForUser(userId).bindAppWidgetIdIfAllowed( + packageName, appWidgetId, provider, options); + } + + @Override + public boolean hasBindAppWidgetPermission(String packageName, int userId) + throws RemoteException { + return getImplForUser(userId).hasBindAppWidgetPermission(packageName); + } + + @Override + public void setBindAppWidgetPermission(String packageName, boolean permission, int userId) + throws RemoteException { + getImplForUser(userId).setBindAppWidgetPermission(packageName, permission); + } + + @Override + public void bindRemoteViewsService(int appWidgetId, Intent intent, IBinder connection, + int userId) throws RemoteException { + getImplForUser(userId).bindRemoteViewsService(appWidgetId, intent, connection); + } + + @Override + public int[] startListening(IAppWidgetHost host, String packageName, int hostId, + List updatedViews, int userId) throws RemoteException { + return getImplForUser(userId).startListening(host, packageName, hostId, updatedViews); + } + + public void onUserRemoved(int userId) { + if (userId < 1) return; + synchronized (mAppWidgetServices) { + AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); + mAppWidgetServices.remove(userId); + + if (impl == null) { + AppWidgetServiceImpl.getSettingsFile(userId).delete(); + } else { + impl.onUserRemoved(); + } + } + } + + public void onUserStopping(int userId) { + if (userId < 1) return; + synchronized (mAppWidgetServices) { + AppWidgetServiceImpl impl = mAppWidgetServices.get(userId); + if (impl != null) { + mAppWidgetServices.remove(userId); + impl.onUserStopping(); + } + } + } + + private void checkPermission(int userId) { + int realUserId = ActivityManager.handleIncomingUser( + Binder.getCallingPid(), + Binder.getCallingUid(), + userId, + false, /* allowAll */ + true, /* requireFull */ + this.getClass().getSimpleName(), + this.getClass().getPackage().getName()); + } + + private AppWidgetServiceImpl getImplForUser(int userId) { + checkPermission(userId); + boolean sendInitial = false; + AppWidgetServiceImpl service; + synchronized (mAppWidgetServices) { + service = mAppWidgetServices.get(userId); + if (service == null) { + Slog.i(TAG, "Unable to find AppWidgetServiceImpl for user " + userId + ", adding"); + // TODO: Verify that it's a valid user + service = new AppWidgetServiceImpl(mContext, userId, mSaveStateHandler); + service.systemReady(mSafeMode); + // Assume that BOOT_COMPLETED was received, as this is a non-primary user. + mAppWidgetServices.append(userId, service); + sendInitial = true; + } + } + if (sendInitial) { + service.sendInitialBroadcasts(); + } + return service; + } + + @Override + public int[] getAppWidgetIds(ComponentName provider, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetIds(provider); + } + + @Override + public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId, int userId) + throws RemoteException { + return getImplForUser(userId).getAppWidgetInfo(appWidgetId); + } + + @Override + public RemoteViews getAppWidgetViews(int appWidgetId, int userId) throws RemoteException { + return getImplForUser(userId).getAppWidgetViews(appWidgetId); + } + + @Override + public void updateAppWidgetOptions(int appWidgetId, Bundle options, int userId) { + getImplForUser(userId).updateAppWidgetOptions(appWidgetId, options); + } + + @Override + public Bundle getAppWidgetOptions(int appWidgetId, int userId) { + return getImplForUser(userId).getAppWidgetOptions(appWidgetId); + } + + @Override + public List getInstalledProviders(int categoryFilter, int userId) + throws RemoteException { + return getImplForUser(userId).getInstalledProviders(categoryFilter); + } + + @Override + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId, int userId) + throws RemoteException { + getImplForUser(userId).notifyAppWidgetViewDataChanged( + appWidgetIds, viewId); + } + + @Override + public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).partiallyUpdateAppWidgetIds( + appWidgetIds, views); + } + + @Override + public void stopListening(int hostId, int userId) throws RemoteException { + getImplForUser(userId).stopListening(hostId); + } + + @Override + public void unbindRemoteViewsService(int appWidgetId, Intent intent, int userId) + throws RemoteException { + getImplForUser(userId).unbindRemoteViewsService( + appWidgetId, intent); + } + + @Override + public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).updateAppWidgetIds(appWidgetIds, views); + } + + @Override + public void updateAppWidgetProvider(ComponentName provider, RemoteViews views, int userId) + throws RemoteException { + getImplForUser(userId).updateAppWidgetProvider(provider, views); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + // Dump the state of all the app widget providers + synchronized (mAppWidgetServices) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + for (int i = 0; i < mAppWidgetServices.size(); i++) { + pw.println("User: " + mAppWidgetServices.keyAt(i)); + ipw.increaseIndent(); + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.dump(fd, ipw, args); + ipw.decreaseIndent(); + } + } + } + + BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + // Slog.d(TAG, "received " + action); + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId >= 0) { + getImplForUser(userId).sendInitialBroadcasts(); + } else { + Slog.w(TAG, "Incorrect user handle supplied in " + intent); + } + } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { + for (int i = 0; i < mAppWidgetServices.size(); i++) { + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.onConfigurationChanged(); + } + } else { + int sendingUser = getSendingUserId(); + if (sendingUser == UserHandle.USER_ALL) { + for (int i = 0; i < mAppWidgetServices.size(); i++) { + AppWidgetServiceImpl service = mAppWidgetServices.valueAt(i); + service.onBroadcastReceived(intent); + } + } else { + AppWidgetServiceImpl service = mAppWidgetServices.get(sendingUser); + if (service != null) { + service.onBroadcastReceived(intent); + } + } + } + } + }; +} diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java new file mode 100644 index 0000000..98dead3 --- /dev/null +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -0,0 +1,2126 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appwidget; + +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.Intent.FilterComparison; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Point; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.AtomicFile; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.TypedValue; +import android.util.Xml; +import android.view.Display; +import android.view.WindowManager; +import android.widget.RemoteViews; + +import com.android.internal.appwidget.IAppWidgetHost; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.widget.IRemoteViewsAdapterConnection; +import com.android.internal.widget.IRemoteViewsFactory; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +class AppWidgetServiceImpl { + + private static final String KEYGUARD_HOST_PACKAGE = "com.android.keyguard"; + private static final int KEYGUARD_HOST_ID = 0x4b455947; + private static final String TAG = "AppWidgetServiceImpl"; + private static final String SETTINGS_FILENAME = "appwidgets.xml"; + private static final int MIN_UPDATE_PERIOD = 30 * 60 * 1000; // 30 minutes + private static final int CURRENT_VERSION = 1; // Bump if the stored widgets need to be upgraded. + + private static boolean DBG = false; + + /* + * When identifying a Host or Provider based on the calling process, use the uid field. When + * identifying a Host or Provider based on a package manager broadcast, use the package given. + */ + + static class Provider { + int uid; + AppWidgetProviderInfo info; + ArrayList instances = new ArrayList(); + PendingIntent broadcast; + boolean zombie; // if we're in safe mode, don't prune this just because nobody references it + + int tag; // for use while saving state (the index) + } + + static class Host { + int uid; + int hostId; + String packageName; + ArrayList instances = new ArrayList(); + IAppWidgetHost callbacks; + boolean zombie; // if we're in safe mode, don't prune this just because nobody references it + + int tag; // for use while saving state (the index) + + boolean uidMatches(int callingUid) { + if (UserHandle.getAppId(callingUid) == Process.myUid()) { + // For a host that's in the system process, ignore the user id + return UserHandle.isSameApp(this.uid, callingUid); + } else { + return this.uid == callingUid; + } + } + } + + static class AppWidgetId { + int appWidgetId; + Provider provider; + RemoteViews views; + Bundle options; + Host host; + } + + /** + * Acts as a proxy between the ServiceConnection and the RemoteViewsAdapterConnection. This + * needs to be a static inner class since a reference to the ServiceConnection is held globally + * and may lead us to leak AppWidgetService instances (if there were more than one). + */ + static class ServiceConnectionProxy implements ServiceConnection { + private final IBinder mConnectionCb; + + ServiceConnectionProxy(Pair key, IBinder connectionCb) { + mConnectionCb = connectionCb; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub + .asInterface(mConnectionCb); + try { + cb.onServiceConnected(service); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void onServiceDisconnected(ComponentName name) { + disconnect(); + } + + public void disconnect() { + final IRemoteViewsAdapterConnection cb = IRemoteViewsAdapterConnection.Stub + .asInterface(mConnectionCb); + try { + cb.onServiceDisconnected(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + // Manages active connections to RemoteViewsServices + private final HashMap, ServiceConnection> mBoundRemoteViewsServices = new HashMap, ServiceConnection>(); + // Manages persistent references to RemoteViewsServices from different App Widgets + private final HashMap> mRemoteViewsServicesAppWidgets = new HashMap>(); + + final Context mContext; + final IPackageManager mPm; + final AlarmManager mAlarmManager; + final ArrayList mInstalledProviders = new ArrayList(); + final int mUserId; + final boolean mHasFeature; + + Locale mLocale; + int mNextAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + 1; + final ArrayList mAppWidgetIds = new ArrayList(); + final ArrayList mHosts = new ArrayList(); + // set of package names + final HashSet mPackagesWithBindWidgetPermission = new HashSet(); + boolean mSafeMode; + boolean mStateLoaded; + int mMaxWidgetBitmapMemory; + + private final Handler mSaveStateHandler; + + // These are for debugging only -- widgets are going missing in some rare instances + ArrayList mDeletedProviders = new ArrayList(); + ArrayList mDeletedHosts = new ArrayList(); + + AppWidgetServiceImpl(Context context, int userId, Handler saveStateHandler) { + mContext = context; + mPm = AppGlobals.getPackageManager(); + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mUserId = userId; + mSaveStateHandler = saveStateHandler; + mHasFeature = context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_APP_WIDGETS); + computeMaximumWidgetBitmapMemory(); + } + + void computeMaximumWidgetBitmapMemory() { + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getRealSize(size); + // Cap memory usage at 1.5 times the size of the display + // 1.5 * 4 bytes/pixel * w * h ==> 6 * w * h + mMaxWidgetBitmapMemory = 6 * size.x * size.y; + } + + public void systemReady(boolean safeMode) { + mSafeMode = safeMode; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + } + } + + private void log(String msg) { + Slog.i(TAG, "u=" + mUserId + ": " + msg); + } + + void onConfigurationChanged() { + if (DBG) log("Got onConfigurationChanged()"); + Locale revised = Locale.getDefault(); + if (revised == null || mLocale == null || !(revised.equals(mLocale))) { + mLocale = revised; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + // Note: updateProvidersForPackageLocked() may remove providers, so we must copy the + // list of installed providers and skip providers that we don't need to update. + // Also note that remove the provider does not clear the Provider component data. + ArrayList installedProviders = + new ArrayList(mInstalledProviders); + HashSet removedProviders = new HashSet(); + int N = installedProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = installedProviders.get(i); + ComponentName cn = p.info.provider; + if (!removedProviders.contains(cn)) { + updateProvidersForPackageLocked(cn.getPackageName(), removedProviders); + } + } + saveStateAsync(); + } + } + } + + void onBroadcastReceived(Intent intent) { + if (DBG) log("onBroadcast " + intent); + final String action = intent.getAction(); + boolean added = false; + boolean changed = false; + boolean providersModified = false; + String pkgList[] = null; + if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + added = true; + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + added = false; + } else { + Uri uri = intent.getData(); + if (uri == null) { + return; + } + String pkgName = uri.getSchemeSpecificPart(); + if (pkgName == null) { + return; + } + pkgList = new String[] { pkgName }; + added = Intent.ACTION_PACKAGE_ADDED.equals(action); + changed = Intent.ACTION_PACKAGE_CHANGED.equals(action); + } + if (pkgList == null || pkgList.length == 0) { + return; + } + if (added || changed) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Bundle extras = intent.getExtras(); + if (changed + || (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false))) { + for (String pkgName : pkgList) { + // The package was just upgraded + providersModified |= updateProvidersForPackageLocked(pkgName, null); + } + } else { + // The package was just added + for (String pkgName : pkgList) { + providersModified |= addProvidersForPackageLocked(pkgName); + } + } + saveStateAsync(); + } + } else { + Bundle extras = intent.getExtras(); + if (extras != null && extras.getBoolean(Intent.EXTRA_REPLACING, false)) { + // The package is being updated. We'll receive a PACKAGE_ADDED shortly. + } else { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (String pkgName : pkgList) { + providersModified |= removeProvidersForPackageLocked(pkgName); + saveStateAsync(); + } + } + } + } + + if (providersModified) { + // If the set of providers has been modified, notify each active AppWidgetHost + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + notifyHostsForProvidersChangedLocked(); + } + } + } + + private void dumpProvider(Provider p, int index, PrintWriter pw) { + AppWidgetProviderInfo info = p.info; + pw.print(" ["); pw.print(index); pw.print("] provider "); + pw.print(info.provider.flattenToShortString()); + pw.println(':'); + pw.print(" min=("); pw.print(info.minWidth); + pw.print("x"); pw.print(info.minHeight); + pw.print(") minResize=("); pw.print(info.minResizeWidth); + pw.print("x"); pw.print(info.minResizeHeight); + pw.print(") updatePeriodMillis="); + pw.print(info.updatePeriodMillis); + pw.print(" resizeMode="); + pw.print(info.resizeMode); + pw.print(info.widgetCategory); + pw.print(" autoAdvanceViewId="); + pw.print(info.autoAdvanceViewId); + pw.print(" initialLayout=#"); + pw.print(Integer.toHexString(info.initialLayout)); + pw.print(" uid="); pw.print(p.uid); + pw.print(" zombie="); pw.println(p.zombie); + } + + private void dumpHost(Host host, int index, PrintWriter pw) { + pw.print(" ["); pw.print(index); pw.print("] hostId="); + pw.print(host.hostId); pw.print(' '); + pw.print(host.packageName); pw.print('/'); + pw.print(host.uid); pw.println(':'); + pw.print(" callbacks="); pw.println(host.callbacks); + pw.print(" instances.size="); pw.print(host.instances.size()); + pw.print(" zombie="); pw.println(host.zombie); + } + + private void dumpAppWidgetId(AppWidgetId id, int index, PrintWriter pw) { + pw.print(" ["); pw.print(index); pw.print("] id="); + pw.println(id.appWidgetId); + pw.print(" hostId="); + pw.print(id.host.hostId); pw.print(' '); + pw.print(id.host.packageName); pw.print('/'); + pw.println(id.host.uid); + if (id.provider != null) { + pw.print(" provider="); + pw.println(id.provider.info.provider.flattenToShortString()); + } + if (id.host != null) { + pw.print(" host.callbacks="); pw.println(id.host.callbacks); + } + if (id.views != null) { + pw.print(" views="); pw.println(id.views); + } + } + + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mAppWidgetIds) { + int N = mInstalledProviders.size(); + pw.println("Providers:"); + for (int i=0; i key = Pair.create(appWidgetId, fc); + if (mBoundRemoteViewsServices.containsKey(key)) { + conn = (ServiceConnectionProxy) mBoundRemoteViewsServices.get(key); + conn.disconnect(); + mContext.unbindService(conn); + mBoundRemoteViewsServices.remove(key); + } + + int userId = UserHandle.getUserId(id.provider.uid); + if (userId != mUserId) { + Slog.w(TAG, "AppWidgetServiceImpl of user " + mUserId + + " binding to provider on user " + userId); + } + // Bind to the RemoteViewsService (which will trigger a callback to the + // RemoteViewsAdapter.onServiceConnected()) + final long token = Binder.clearCallingIdentity(); + try { + conn = new ServiceConnectionProxy(key, connection); + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + mBoundRemoteViewsServices.put(key, conn); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Add it to the mapping of RemoteViewsService to appWidgetIds so that we can determine + // when we can call back to the RemoteViewsService later to destroy associated + // factories. + incrementAppWidgetServiceRefCount(appWidgetId, fc); + } + } + + // Unbinds from a specific RemoteViewsService + public void unbindRemoteViewsService(int appWidgetId, Intent intent) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + ensureStateLoadedLocked(); + // Unbind from the RemoteViewsService (which will trigger a callback to the bound + // RemoteViewsAdapter) + Pair key = Pair.create(appWidgetId, new FilterComparison( + intent)); + if (mBoundRemoteViewsServices.containsKey(key)) { + // We don't need to use the appWidgetId until after we are sure there is something + // to unbind. Note that this may mask certain issues with apps calling unbind() + // more than necessary. + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id == null) { + throw new IllegalArgumentException("bad appWidgetId"); + } + + ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices + .get(key); + conn.disconnect(); + mContext.unbindService(conn); + mBoundRemoteViewsServices.remove(key); + } + } + } + + // Unbinds from a RemoteViewsService when we delete an app widget + private void unbindAppWidgetRemoteViewsServicesLocked(AppWidgetId id) { + int appWidgetId = id.appWidgetId; + // Unbind all connections to Services bound to this AppWidgetId + Iterator> it = mBoundRemoteViewsServices.keySet() + .iterator(); + while (it.hasNext()) { + final Pair key = it.next(); + if (key.first.intValue() == appWidgetId) { + final ServiceConnectionProxy conn = (ServiceConnectionProxy) mBoundRemoteViewsServices + .get(key); + conn.disconnect(); + mContext.unbindService(conn); + it.remove(); + } + } + + // Check if we need to destroy any services (if no other app widgets are + // referencing the same service) + decrementAppWidgetServiceRefCount(id); + } + + // Destroys the cached factory on the RemoteViewsService's side related to the specified intent + private void destroyRemoteViewsService(final Intent intent, AppWidgetId id) { + final ServiceConnection conn = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + final IRemoteViewsFactory cb = IRemoteViewsFactory.Stub.asInterface(service); + try { + cb.onDestroy(intent); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + mContext.unbindService(this); + } + + @Override + public void onServiceDisconnected(android.content.ComponentName name) { + // Do nothing + } + }; + + int userId = UserHandle.getUserId(id.provider.uid); + // Bind to the service and remove the static intent->factory mapping in the + // RemoteViewsService. + final long token = Binder.clearCallingIdentity(); + try { + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Adds to the ref-count for a given RemoteViewsService intent + private void incrementAppWidgetServiceRefCount(int appWidgetId, FilterComparison fc) { + HashSet appWidgetIds = null; + if (mRemoteViewsServicesAppWidgets.containsKey(fc)) { + appWidgetIds = mRemoteViewsServicesAppWidgets.get(fc); + } else { + appWidgetIds = new HashSet(); + mRemoteViewsServicesAppWidgets.put(fc, appWidgetIds); + } + appWidgetIds.add(appWidgetId); + } + + // Subtracts from the ref-count for a given RemoteViewsService intent, prompting a delete if + // the ref-count reaches zero. + private void decrementAppWidgetServiceRefCount(AppWidgetId id) { + Iterator it = mRemoteViewsServicesAppWidgets.keySet().iterator(); + while (it.hasNext()) { + final FilterComparison key = it.next(); + final HashSet ids = mRemoteViewsServicesAppWidgets.get(key); + if (ids.remove(id.appWidgetId)) { + // If we have removed the last app widget referencing this service, then we + // should destroy it and remove it from this set + if (ids.isEmpty()) { + destroyRemoteViewsService(key.getIntent(), id); + it.remove(); + } + } + } + } + + public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return null; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null && id.provider != null && !id.provider.zombie) { + return cloneIfLocalBinder(id.provider.info); + } + return null; + } + } + + public RemoteViews getAppWidgetViews(int appWidgetId) { + if (DBG) log("getAppWidgetViews id=" + appWidgetId); + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return null; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null) { + return cloneIfLocalBinder(id.views); + } + if (DBG) log(" couldn't find appwidgetid"); + return null; + } + } + + public List getInstalledProviders(int categoryFilter) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return new ArrayList(0); + } + ensureStateLoadedLocked(); + final int N = mInstalledProviders.size(); + ArrayList result = new ArrayList(N); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (!p.zombie && (p.info.widgetCategory & categoryFilter) != 0) { + result.add(cloneIfLocalBinder(p.info)); + } + } + return result; + } + } + + public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (DBG) log("updateAppWidgetIds views: " + views); + int bitmapMemoryUsage = 0; + if (views != null) { + bitmapMemoryUsage = views.estimateMemoryUsage(); + } + if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) { + throw new IllegalArgumentException("RemoteViews for widget update exceeds maximum" + + " bitmap memory usage (used: " + bitmapMemoryUsage + ", max: " + + mMaxWidgetBitmapMemory + ") The total memory cannot exceed that required to" + + " fill the device's screen once."); + } + + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + updateAppWidgetInstanceLocked(id, views); + } + } + } + + private void saveStateAsync() { + mSaveStateHandler.post(mSaveStateRunnable); + } + + private final Runnable mSaveStateRunnable = new Runnable() { + @Override + public void run() { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + saveStateLocked(); + } + } + }; + + public void updateAppWidgetOptions(int appWidgetId, Bundle options) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + options = cloneIfLocalBinder(options); + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + + if (id == null) { + return; + } + + Provider p = id.provider; + // Merge the options + id.options.putAll(options); + + // send the broacast saying that this appWidgetId has been deleted + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED); + intent.setComponent(p.info.provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id.appWidgetId); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, id.options); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + saveStateAsync(); + } + } + + public Bundle getAppWidgetOptions(int appWidgetId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return Bundle.EMPTY; + } + ensureStateLoadedLocked(); + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetId); + if (id != null && id.options != null) { + return cloneIfLocalBinder(id.options); + } else { + return Bundle.EMPTY; + } + } + } + + public void partiallyUpdateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + if (id == null) { + Slog.w(TAG, "widget id " + appWidgetIds[i] + " not found!"); + } else if (id.views != null) { + // Only trigger a partial update for a widget if it has received a full update + updateAppWidgetInstanceLocked(id, views, true); + } + } + } + } + + public void notifyAppWidgetViewDataChanged(int[] appWidgetIds, int viewId) { + if (!mHasFeature) { + return; + } + if (appWidgetIds == null) { + return; + } + if (appWidgetIds.length == 0) { + return; + } + final int N = appWidgetIds.length; + + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + for (int i = 0; i < N; i++) { + AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); + notifyAppWidgetViewDataChangedInstanceLocked(id, viewId); + } + } + } + + public void updateAppWidgetProvider(ComponentName provider, RemoteViews views) { + if (!mHasFeature) { + return; + } + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Provider p = lookupProviderLocked(provider); + if (p == null) { + Slog.w(TAG, "updateAppWidgetProvider: provider doesn't exist: " + provider); + return; + } + ArrayList instances = p.instances; + final int callingUid = Binder.getCallingUid(); + final int N = instances.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = instances.get(i); + if (canAccessAppWidgetId(id, callingUid)) { + updateAppWidgetInstanceLocked(id, views); + } + } + } + } + + void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views) { + updateAppWidgetInstanceLocked(id, views, false); + } + + void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views, boolean isPartialUpdate) { + // allow for stale appWidgetIds and other badness + // lookup also checks that the calling process can access the appWidgetId + // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) + if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { + + if (!isPartialUpdate) { + // For a full update we replace the RemoteViews completely. + id.views = views; + } else { + // For a partial update, we merge the new RemoteViews with the old. + id.views.mergeRemoteViews(views); + } + + // is anyone listening? + if (id.host.callbacks != null) { + try { + // the lock is held, but this is a oneway call + id.host.callbacks.updateAppWidget(id.appWidgetId, views, mUserId); + } catch (RemoteException e) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this instance. + id.host.callbacks = null; + } + } + } + } + + void notifyAppWidgetViewDataChangedInstanceLocked(AppWidgetId id, int viewId) { + // allow for stale appWidgetIds and other badness + // lookup also checks that the calling process can access the appWidgetId + // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) + if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { + // is anyone listening? + if (id.host.callbacks != null) { + try { + // the lock is held, but this is a oneway call + id.host.callbacks.viewDataChanged(id.appWidgetId, viewId, mUserId); + } catch (RemoteException e) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this instance. + id.host.callbacks = null; + } + } + + // If the host is unavailable, then we call the associated + // RemoteViewsFactory.onDataSetChanged() directly + if (id.host.callbacks == null) { + Set keys = mRemoteViewsServicesAppWidgets.keySet(); + for (FilterComparison key : keys) { + if (mRemoteViewsServicesAppWidgets.get(key).contains(id.appWidgetId)) { + Intent intent = key.getIntent(); + + final ServiceConnection conn = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + IRemoteViewsFactory cb = IRemoteViewsFactory.Stub + .asInterface(service); + try { + cb.onDataSetChangedAsync(); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + mContext.unbindService(this); + } + + @Override + public void onServiceDisconnected(android.content.ComponentName name) { + // Do nothing + } + }; + + int userId = UserHandle.getUserId(id.provider.uid); + // Bind to the service and call onDataSetChanged() + final long token = Binder.clearCallingIdentity(); + try { + mContext.bindServiceAsUser(intent, conn, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + } + } + } + + private boolean isLocalBinder() { + return Process.myPid() == Binder.getCallingPid(); + } + + private RemoteViews cloneIfLocalBinder(RemoteViews rv) { + if (isLocalBinder() && rv != null) { + return rv.clone(); + } + return rv; + } + + private AppWidgetProviderInfo cloneIfLocalBinder(AppWidgetProviderInfo info) { + if (isLocalBinder() && info != null) { + return info.clone(); + } + return info; + } + + private Bundle cloneIfLocalBinder(Bundle bundle) { + // Note: this is only a shallow copy. For now this will be fine, but it could be problematic + // if we start adding objects to the options. Further, it would only be an issue if keyguard + // used such options. + if (isLocalBinder() && bundle != null) { + return (Bundle) bundle.clone(); + } + return bundle; + } + + public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId, + List updatedViews) { + if (!mHasFeature) { + return new int[0]; + } + int callingUid = enforceCallingUid(packageName); + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Host host = lookupOrAddHostLocked(callingUid, packageName, hostId); + host.callbacks = callbacks; + + updatedViews.clear(); + + ArrayList instances = host.instances; + int N = instances.size(); + int[] updatedIds = new int[N]; + for (int i = 0; i < N; i++) { + AppWidgetId id = instances.get(i); + updatedIds[i] = id.appWidgetId; + updatedViews.add(cloneIfLocalBinder(id.views)); + } + return updatedIds; + } + } + + public void stopListening(int hostId) { + synchronized (mAppWidgetIds) { + if (!mHasFeature) { + return; + } + ensureStateLoadedLocked(); + Host host = lookupHostLocked(Binder.getCallingUid(), hostId); + if (host != null) { + host.callbacks = null; + pruneHostLocked(host); + } + } + } + + boolean canAccessAppWidgetId(AppWidgetId id, int callingUid) { + if (id.host.uidMatches(callingUid)) { + // Apps hosting the AppWidget have access to it. + return true; + } + if (id.provider != null && id.provider.uid == callingUid) { + // Apps providing the AppWidget have access to it (if the appWidgetId has been bound) + return true; + } + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.BIND_APPWIDGET) == PackageManager.PERMISSION_GRANTED) { + // Apps that can bind have access to all appWidgetIds. + return true; + } + // Nobody else can access it. + return false; + } + + AppWidgetId lookupAppWidgetIdLocked(int appWidgetId) { + int callingUid = Binder.getCallingUid(); + final int N = mAppWidgetIds.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = mAppWidgetIds.get(i); + if (id.appWidgetId == appWidgetId && canAccessAppWidgetId(id, callingUid)) { + return id; + } + } + return null; + } + + Provider lookupProviderLocked(ComponentName provider) { + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.info.provider.equals(provider)) { + return p; + } + } + return null; + } + + Host lookupHostLocked(int uid, int hostId) { + final int N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host h = mHosts.get(i); + if (h.uidMatches(uid) && h.hostId == hostId) { + return h; + } + } + return null; + } + + Host lookupOrAddHostLocked(int uid, String packageName, int hostId) { + final int N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host h = mHosts.get(i); + if (h.hostId == hostId && h.packageName.equals(packageName)) { + return h; + } + } + Host host = new Host(); + host.packageName = packageName; + host.uid = uid; + host.hostId = hostId; + mHosts.add(host); + return host; + } + + void pruneHostLocked(Host host) { + if (host.instances.size() == 0 && host.callbacks == null) { + mHosts.remove(host); + } + } + + void loadAppWidgetListLocked() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + try { + List broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + + final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + addProviderLocked(ri); + } + } catch (RemoteException re) { + // Shouldn't happen, local call + } + } + + boolean addProviderLocked(ResolveInfo ri) { + if ((ri.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + return false; + } + if (!ri.activityInfo.isEnabled()) { + return false; + } + Provider p = parseProviderInfoXml(new ComponentName(ri.activityInfo.packageName, + ri.activityInfo.name), ri); + if (p != null) { + mInstalledProviders.add(p); + return true; + } else { + return false; + } + } + + void removeProviderLocked(int index, Provider p) { + int N = p.instances.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = p.instances.get(i); + // Call back with empty RemoteViews + updateAppWidgetInstanceLocked(id, null); + // Stop telling the host about updates for this from now on + cancelBroadcasts(p); + // clear out references to this appWidgetId + id.host.instances.remove(id); + mAppWidgetIds.remove(id); + id.provider = null; + pruneHostLocked(id.host); + id.host = null; + } + p.instances.clear(); + mInstalledProviders.remove(index); + mDeletedProviders.add(p); + // no need to send the DISABLE broadcast, since the receiver is gone anyway + cancelBroadcasts(p); + } + + void sendEnableIntentLocked(Provider p) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED); + intent.setComponent(p.info.provider); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + } + + void sendUpdateIntentLocked(Provider p, int[] appWidgetIds) { + if (appWidgetIds != null && appWidgetIds.length > 0) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); + intent.setComponent(p.info.provider); + mContext.sendBroadcastAsUser(intent, new UserHandle(mUserId)); + } + } + + void registerForBroadcastsLocked(Provider p, int[] appWidgetIds) { + if (p.info.updatePeriodMillis > 0) { + // if this is the first instance, set the alarm. otherwise, + // rely on the fact that we've already set it and that + // PendingIntent.getBroadcast will update the extras. + boolean alreadyRegistered = p.broadcast != null; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); + intent.setComponent(p.info.provider); + long token = Binder.clearCallingIdentity(); + try { + p.broadcast = PendingIntent.getBroadcastAsUser(mContext, 1, intent, + PendingIntent.FLAG_UPDATE_CURRENT, new UserHandle(mUserId)); + } finally { + Binder.restoreCallingIdentity(token); + } + if (!alreadyRegistered) { + long period = p.info.updatePeriodMillis; + if (period < MIN_UPDATE_PERIOD) { + period = MIN_UPDATE_PERIOD; + } + mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock + .elapsedRealtime() + + period, period, p.broadcast); + } + } + } + + static int[] getAppWidgetIds(Provider p) { + int instancesSize = p.instances.size(); + int appWidgetIds[] = new int[instancesSize]; + for (int i = 0; i < instancesSize; i++) { + appWidgetIds[i] = p.instances.get(i).appWidgetId; + } + return appWidgetIds; + } + + public int[] getAppWidgetIds(ComponentName provider) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + Provider p = lookupProviderLocked(provider); + if (p != null && Binder.getCallingUid() == p.uid) { + return getAppWidgetIds(p); + } else { + return new int[0]; + } + } + } + + static int[] getAppWidgetIds(Host h) { + int instancesSize = h.instances.size(); + int appWidgetIds[] = new int[instancesSize]; + for (int i = 0; i < instancesSize; i++) { + appWidgetIds[i] = h.instances.get(i).appWidgetId; + } + return appWidgetIds; + } + + public int[] getAppWidgetIdsForHost(int hostId) { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + int callingUid = Binder.getCallingUid(); + Host host = lookupHostLocked(callingUid, hostId); + if (host != null) { + return getAppWidgetIds(host); + } else { + return new int[0]; + } + } + } + + private Provider parseProviderInfoXml(ComponentName component, ResolveInfo ri) { + Provider p = null; + + ActivityInfo activityInfo = ri.activityInfo; + XmlResourceParser parser = null; + try { + parser = activityInfo.loadXmlMetaData(mContext.getPackageManager(), + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER); + if (parser == null) { + Slog.w(TAG, "No " + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER + + " meta-data for " + "AppWidget provider '" + component + '\''); + return null; + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // drain whitespace, comments, etc. + } + + String nodeName = parser.getName(); + if (!"appwidget-provider".equals(nodeName)) { + Slog.w(TAG, "Meta-data does not start with appwidget-provider tag for" + + " AppWidget provider '" + component + '\''); + return null; + } + + p = new Provider(); + AppWidgetProviderInfo info = p.info = new AppWidgetProviderInfo(); + info.provider = component; + p.uid = activityInfo.applicationInfo.uid; + + Resources res = mContext.getPackageManager() + .getResourcesForApplicationAsUser(activityInfo.packageName, mUserId); + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AppWidgetProviderInfo); + + // These dimensions has to be resolved in the application's context. + // We simply send back the raw complex data, which will be + // converted to dp in {@link AppWidgetManager#getAppWidgetInfo}. + TypedValue value = sa + .peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minWidth); + info.minWidth = value != null ? value.data : 0; + value = sa.peekValue(com.android.internal.R.styleable.AppWidgetProviderInfo_minHeight); + info.minHeight = value != null ? value.data : 0; + value = sa.peekValue( + com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeWidth); + info.minResizeWidth = value != null ? value.data : info.minWidth; + value = sa.peekValue( + com.android.internal.R.styleable.AppWidgetProviderInfo_minResizeHeight); + info.minResizeHeight = value != null ? value.data : info.minHeight; + info.updatePeriodMillis = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_updatePeriodMillis, 0); + info.initialLayout = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_initialLayout, 0); + info.initialKeyguardLayout = sa.getResourceId(com.android.internal.R.styleable. + AppWidgetProviderInfo_initialKeyguardLayout, 0); + String className = sa + .getString(com.android.internal.R.styleable.AppWidgetProviderInfo_configure); + if (className != null) { + info.configure = new ComponentName(component.getPackageName(), className); + } + info.label = activityInfo.loadLabel(mContext.getPackageManager()).toString(); + info.icon = ri.getIconResource(); + info.previewImage = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_previewImage, 0); + info.autoAdvanceViewId = sa.getResourceId( + com.android.internal.R.styleable.AppWidgetProviderInfo_autoAdvanceViewId, -1); + info.resizeMode = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_resizeMode, + AppWidgetProviderInfo.RESIZE_NONE); + info.widgetCategory = sa.getInt( + com.android.internal.R.styleable.AppWidgetProviderInfo_widgetCategory, + AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN); + + sa.recycle(); + } catch (Exception e) { + // Ok to catch Exception here, because anything going wrong because + // of what a client process passes to us should not be fatal for the + // system process. + Slog.w(TAG, "XML parsing failed for AppWidget provider '" + component + '\'', e); + return null; + } finally { + if (parser != null) + parser.close(); + } + return p; + } + + int getUidForPackage(String packageName) throws PackageManager.NameNotFoundException { + PackageInfo pkgInfo = null; + try { + pkgInfo = mPm.getPackageInfo(packageName, 0, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + } + if (pkgInfo == null || pkgInfo.applicationInfo == null) { + throw new PackageManager.NameNotFoundException(); + } + return pkgInfo.applicationInfo.uid; + } + + int enforceSystemOrCallingUid(String packageName) throws IllegalArgumentException { + int callingUid = Binder.getCallingUid(); + if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID || callingUid == 0) { + return callingUid; + } + return enforceCallingUid(packageName); + } + + int enforceCallingUid(String packageName) throws IllegalArgumentException { + int callingUid = Binder.getCallingUid(); + int packageUid; + try { + packageUid = getUidForPackage(packageName); + } catch (PackageManager.NameNotFoundException ex) { + throw new IllegalArgumentException("packageName and uid don't match packageName=" + + packageName); + } + if (!UserHandle.isSameApp(callingUid, packageUid)) { + throw new IllegalArgumentException("packageName and uid don't match packageName=" + + packageName); + } + return callingUid; + } + + void sendInitialBroadcasts() { + synchronized (mAppWidgetIds) { + ensureStateLoadedLocked(); + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.instances.size() > 0) { + sendEnableIntentLocked(p); + int[] appWidgetIds = getAppWidgetIds(p); + sendUpdateIntentLocked(p, appWidgetIds); + registerForBroadcastsLocked(p, appWidgetIds); + } + } + } + } + + // only call from initialization -- it assumes that the data structures are all empty + void loadStateLocked() { + AtomicFile file = savedStateFile(); + try { + FileInputStream stream = file.openRead(); + readStateFromFileLocked(stream); + + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Slog.w(TAG, "Failed to close state FileInputStream " + e); + } + } + } catch (FileNotFoundException e) { + Slog.w(TAG, "Failed to read state: " + e); + } + } + + void saveStateLocked() { + if (!mHasFeature) { + return; + } + AtomicFile file = savedStateFile(); + FileOutputStream stream; + try { + stream = file.startWrite(); + if (writeStateToFileLocked(stream)) { + file.finishWrite(stream); + } else { + file.failWrite(stream); + Slog.w(TAG, "Failed to save state, restoring backup."); + } + } catch (IOException e) { + Slog.w(TAG, "Failed open state file for write: " + e); + } + } + + boolean writeStateToFileLocked(FileOutputStream stream) { + int N; + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, "utf-8"); + out.startDocument(null, true); + out.startTag(null, "gs"); + out.attribute(null, "version", String.valueOf(CURRENT_VERSION)); + int providerIndex = 0; + N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + Provider p = mInstalledProviders.get(i); + if (p.instances.size() > 0) { + out.startTag(null, "p"); + out.attribute(null, "pkg", p.info.provider.getPackageName()); + out.attribute(null, "cl", p.info.provider.getClassName()); + out.endTag(null, "p"); + p.tag = providerIndex; + providerIndex++; + } + } + + N = mHosts.size(); + for (int i = 0; i < N; i++) { + Host host = mHosts.get(i); + out.startTag(null, "h"); + out.attribute(null, "pkg", host.packageName); + out.attribute(null, "id", Integer.toHexString(host.hostId)); + out.endTag(null, "h"); + host.tag = i; + } + + N = mAppWidgetIds.size(); + for (int i = 0; i < N; i++) { + AppWidgetId id = mAppWidgetIds.get(i); + out.startTag(null, "g"); + out.attribute(null, "id", Integer.toHexString(id.appWidgetId)); + out.attribute(null, "h", Integer.toHexString(id.host.tag)); + if (id.provider != null) { + out.attribute(null, "p", Integer.toHexString(id.provider.tag)); + } + if (id.options != null) { + out.attribute(null, "min_width", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH))); + out.attribute(null, "min_height", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT))); + out.attribute(null, "max_width", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH))); + out.attribute(null, "max_height", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT))); + out.attribute(null, "host_category", Integer.toHexString(id.options.getInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY))); + } + out.endTag(null, "g"); + } + + Iterator it = mPackagesWithBindWidgetPermission.iterator(); + while (it.hasNext()) { + out.startTag(null, "b"); + out.attribute(null, "packageName", it.next()); + out.endTag(null, "b"); + } + + out.endTag(null, "gs"); + + out.endDocument(); + return true; + } catch (IOException e) { + Slog.w(TAG, "Failed to write state: " + e); + return false; + } + } + + @SuppressWarnings("unused") + void readStateFromFileLocked(FileInputStream stream) { + boolean success = false; + int version = 0; + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, null); + + int type; + int providerIndex = 0; + HashMap loadedProviders = new HashMap(); + do { + type = parser.next(); + if (type == XmlPullParser.START_TAG) { + String tag = parser.getName(); + if ("gs".equals(tag)) { + String attributeValue = parser.getAttributeValue(null, "version"); + try { + version = Integer.parseInt(attributeValue); + } catch (NumberFormatException e) { + version = 0; + } + } else if ("p".equals(tag)) { + // TODO: do we need to check that this package has the same signature + // as before? + String pkg = parser.getAttributeValue(null, "pkg"); + String cl = parser.getAttributeValue(null, "cl"); + + final IPackageManager packageManager = AppGlobals.getPackageManager(); + try { + packageManager.getReceiverInfo(new ComponentName(pkg, cl), 0, mUserId); + } catch (RemoteException e) { + String[] pkgs = mContext.getPackageManager() + .currentToCanonicalPackageNames(new String[] { pkg }); + pkg = pkgs[0]; + } + + Provider p = lookupProviderLocked(new ComponentName(pkg, cl)); + if (p == null && mSafeMode) { + // if we're in safe mode, make a temporary one + p = new Provider(); + p.info = new AppWidgetProviderInfo(); + p.info.provider = new ComponentName(pkg, cl); + p.zombie = true; + mInstalledProviders.add(p); + } + if (p != null) { + // if it wasn't uninstalled or something + loadedProviders.put(providerIndex, p); + } + providerIndex++; + } else if ("h".equals(tag)) { + Host host = new Host(); + + // TODO: do we need to check that this package has the same signature + // as before? + host.packageName = parser.getAttributeValue(null, "pkg"); + try { + host.uid = getUidForPackage(host.packageName); + } catch (PackageManager.NameNotFoundException ex) { + host.zombie = true; + } + if (!host.zombie || mSafeMode) { + // In safe mode, we don't discard the hosts we don't recognize + // so that they're not pruned from our list. Otherwise, we do. + host.hostId = Integer + .parseInt(parser.getAttributeValue(null, "id"), 16); + mHosts.add(host); + } + } else if ("b".equals(tag)) { + String packageName = parser.getAttributeValue(null, "packageName"); + if (packageName != null) { + mPackagesWithBindWidgetPermission.add(packageName); + } + } else if ("g".equals(tag)) { + AppWidgetId id = new AppWidgetId(); + id.appWidgetId = Integer.parseInt(parser.getAttributeValue(null, "id"), 16); + if (id.appWidgetId >= mNextAppWidgetId) { + mNextAppWidgetId = id.appWidgetId + 1; + } + + Bundle options = new Bundle(); + String minWidthString = parser.getAttributeValue(null, "min_width"); + if (minWidthString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + Integer.parseInt(minWidthString, 16)); + } + String minHeightString = parser.getAttributeValue(null, "min_height"); + if (minHeightString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + Integer.parseInt(minHeightString, 16)); + } + String maxWidthString = parser.getAttributeValue(null, "max_width"); + if (maxWidthString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + Integer.parseInt(maxWidthString, 16)); + } + String maxHeightString = parser.getAttributeValue(null, "max_height"); + if (maxHeightString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + Integer.parseInt(maxHeightString, 16)); + } + String categoryString = parser.getAttributeValue(null, "host_category"); + if (categoryString != null) { + options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + Integer.parseInt(categoryString, 16)); + } + id.options = options; + + String providerString = parser.getAttributeValue(null, "p"); + if (providerString != null) { + // there's no provider if it hasn't been bound yet. + // maybe we don't have to save this, but it brings the system + // to the state it was in. + int pIndex = Integer.parseInt(providerString, 16); + id.provider = loadedProviders.get(pIndex); + if (false) { + Slog.d(TAG, "bound appWidgetId=" + id.appWidgetId + " to provider " + + pIndex + " which is " + id.provider); + } + if (id.provider == null) { + // This provider is gone. We just let the host figure out + // that this happened when it fails to load it. + continue; + } + } + + int hIndex = Integer.parseInt(parser.getAttributeValue(null, "h"), 16); + id.host = mHosts.get(hIndex); + if (id.host == null) { + // This host is gone. + continue; + } + + if (id.provider != null) { + id.provider.instances.add(id); + } + id.host.instances.add(id); + mAppWidgetIds.add(id); + } + } + } while (type != XmlPullParser.END_DOCUMENT); + success = true; + } catch (NullPointerException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (IOException e) { + Slog.w(TAG, "failed parsing " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "failed parsing " + e); + } + + if (success) { + // delete any hosts that didn't manage to get connected (should happen) + // if it matters, they'll be reconnected. + for (int i = mHosts.size() - 1; i >= 0; i--) { + pruneHostLocked(mHosts.get(i)); + } + // upgrade the database if needed + performUpgrade(version); + } else { + // failed reading, clean up + Slog.w(TAG, "Failed to read state, clearing widgets and hosts."); + + mAppWidgetIds.clear(); + mHosts.clear(); + final int N = mInstalledProviders.size(); + for (int i = 0; i < N; i++) { + mInstalledProviders.get(i).instances.clear(); + } + } + } + + private void performUpgrade(int fromVersion) { + if (fromVersion < CURRENT_VERSION) { + Slog.v(TAG, "Upgrading widget database from " + fromVersion + " to " + CURRENT_VERSION + + " for user " + mUserId); + } + + int version = fromVersion; + + // Update 1: keyguard moved from package "android" to "com.android.keyguard" + if (version == 0) { + for (int i = 0; i < mHosts.size(); i++) { + Host host = mHosts.get(i); + if (host != null && "android".equals(host.packageName) + && host.hostId == KEYGUARD_HOST_ID) { + host.packageName = KEYGUARD_HOST_PACKAGE; + } + } + version = 1; + } + + if (version != CURRENT_VERSION) { + throw new IllegalStateException("Failed to upgrade widget database"); + } + } + + static File getSettingsFile(int userId) { + return new File(Environment.getUserSystemDirectory(userId), SETTINGS_FILENAME); + } + + AtomicFile savedStateFile() { + File dir = Environment.getUserSystemDirectory(mUserId); + File settingsFile = getSettingsFile(mUserId); + if (!settingsFile.exists() && mUserId == 0) { + if (!dir.exists()) { + dir.mkdirs(); + } + // Migrate old data + File oldFile = new File("/data/system/" + SETTINGS_FILENAME); + // Method doesn't throw an exception on failure. Ignore any errors + // in moving the file (like non-existence) + oldFile.renameTo(settingsFile); + } + return new AtomicFile(settingsFile); + } + + void onUserStopping() { + // prune the ones we don't want to keep + int N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + cancelBroadcasts(p); + } + } + + void onUserRemoved() { + getSettingsFile(mUserId).delete(); + } + + boolean addProvidersForPackageLocked(String pkgName) { + boolean providersAdded = false; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.setPackage(pkgName); + List broadcastReceivers; + try { + broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + return false; + } + final int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + ActivityInfo ai = ri.activityInfo; + if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + continue; + } + if (pkgName.equals(ai.packageName)) { + addProviderLocked(ri); + providersAdded = true; + } + } + + return providersAdded; + } + + /** + * Updates all providers with the specified package names, and records any providers that were + * pruned. + * + * @return whether any providers were updated + */ + boolean updateProvidersForPackageLocked(String pkgName, Set removedProviders) { + boolean providersUpdated = false; + HashSet keep = new HashSet(); + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + intent.setPackage(pkgName); + List broadcastReceivers; + try { + broadcastReceivers = mPm.queryIntentReceivers(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException re) { + // Shouldn't happen, local call + return false; + } + + // add the missing ones and collect which ones to keep + int N = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + for (int i = 0; i < N; i++) { + ResolveInfo ri = broadcastReceivers.get(i); + ActivityInfo ai = ri.activityInfo; + if ((ai.applicationInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) { + continue; + } + if (pkgName.equals(ai.packageName)) { + ComponentName component = new ComponentName(ai.packageName, ai.name); + Provider p = lookupProviderLocked(component); + if (p == null) { + if (addProviderLocked(ri)) { + keep.add(ai.name); + providersUpdated = true; + } + } else { + Provider parsed = parseProviderInfoXml(component, ri); + if (parsed != null) { + keep.add(ai.name); + // Use the new AppWidgetProviderInfo. + p.info = parsed.info; + // If it's enabled + final int M = p.instances.size(); + if (M > 0) { + int[] appWidgetIds = getAppWidgetIds(p); + // Reschedule for the new updatePeriodMillis (don't worry about handling + // it specially if updatePeriodMillis didn't change because we just sent + // an update, and the next one will be updatePeriodMillis from now). + cancelBroadcasts(p); + registerForBroadcastsLocked(p, appWidgetIds); + // If it's currently showing, call back with the new + // AppWidgetProviderInfo. + for (int j = 0; j < M; j++) { + AppWidgetId id = p.instances.get(j); + id.views = null; + if (id.host != null && id.host.callbacks != null) { + try { + id.host.callbacks.providerChanged(id.appWidgetId, p.info, + mUserId); + } catch (RemoteException ex) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this + // instance. + id.host.callbacks = null; + } + } + } + // Now that we've told the host, push out an update. + sendUpdateIntentLocked(p, appWidgetIds); + providersUpdated = true; + } + } + } + } + } + + // prune the ones we don't want to keep + N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + if (pkgName.equals(p.info.provider.getPackageName()) + && !keep.contains(p.info.provider.getClassName())) { + if (removedProviders != null) { + removedProviders.add(p.info.provider); + } + removeProviderLocked(i, p); + providersUpdated = true; + } + } + + return providersUpdated; + } + + boolean removeProvidersForPackageLocked(String pkgName) { + boolean providersRemoved = false; + int N = mInstalledProviders.size(); + for (int i = N - 1; i >= 0; i--) { + Provider p = mInstalledProviders.get(i); + if (pkgName.equals(p.info.provider.getPackageName())) { + removeProviderLocked(i, p); + providersRemoved = true; + } + } + + // Delete the hosts for this package too + // + // By now, we have removed any AppWidgets that were in any hosts here, + // so we don't need to worry about sending DISABLE broadcasts to them. + N = mHosts.size(); + for (int i = N - 1; i >= 0; i--) { + Host host = mHosts.get(i); + if (pkgName.equals(host.packageName)) { + deleteHostLocked(host); + } + } + + return providersRemoved; + } + + void notifyHostsForProvidersChangedLocked() { + final int N = mHosts.size(); + for (int i = N - 1; i >= 0; i--) { + Host host = mHosts.get(i); + try { + if (host.callbacks != null) { + host.callbacks.providersChanged(mUserId); + } + } catch (RemoteException ex) { + // It failed; remove the callback. No need to prune because + // we know that this host is still referenced by this + // instance. + host.callbacks = null; + } + } + } +} diff --git a/services/appwidget/java/service.mk b/services/appwidget/java/service.mk new file mode 100644 index 0000000..0e33446 --- /dev/null +++ b/services/appwidget/java/service.mk @@ -0,0 +1,11 @@ +# Include only if the service is required +ifneq ($(findstring appwidget,$(REQUIRED_SERVICES)),) + +SUB_DIR := appwidget/java + +LOCAL_SRC_FILES += \ + $(call all-java-files-under,$(SUB_DIR)) + +#DEFINED_SERVICES += com.android.server.appwidget.AppWidgetService + +endif diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java new file mode 100644 index 0000000..c2b0d10 --- /dev/null +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -0,0 +1,6196 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.AppGlobals; +import android.app.IActivityManager; +import android.app.IApplicationThread; +import android.app.IBackupAgent; +import android.app.PendingIntent; +import android.app.backup.BackupAgent; +import android.app.backup.BackupDataOutput; +import android.app.backup.FullBackup; +import android.app.backup.RestoreSet; +import android.app.backup.IBackupManager; +import android.app.backup.IFullBackupRestoreObserver; +import android.app.backup.IRestoreObserver; +import android.app.backup.IRestoreSession; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageInstallObserver; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.SELinux; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.WorkSource; +import android.os.Environment.UserEnvironment; +import android.os.storage.IMountService; +import android.provider.Settings; +import android.util.EventLog; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.StringBuilderPrinter; + +import com.android.internal.backup.BackupConstants; +import com.android.internal.backup.IBackupTransport; +import com.android.internal.backup.IObbBackupService; +import com.android.internal.backup.LocalTransport; +import com.android.server.EventLogTags; +import com.android.server.backup.PackageManagerBackupAgent.Metadata; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class BackupManagerService extends IBackupManager.Stub { + private static final String TAG = "BackupManagerService"; + private static final boolean DEBUG = true; + private static final boolean MORE_DEBUG = false; + + // Name and current contents version of the full-backup manifest file + static final String BACKUP_MANIFEST_FILENAME = "_manifest"; + static final int BACKUP_MANIFEST_VERSION = 1; + static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n"; + static final int BACKUP_FILE_VERSION = 1; + static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production + + static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup"; + static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; + + // How often we perform a backup pass. Privileged external callers can + // trigger an immediate pass. + private static final long BACKUP_INTERVAL = AlarmManager.INTERVAL_HOUR; + + // Random variation in backup scheduling time to avoid server load spikes + private static final int FUZZ_MILLIS = 5 * 60 * 1000; + + // The amount of time between the initial provisioning of the device and + // the first backup pass. + private static final long FIRST_BACKUP_INTERVAL = 12 * AlarmManager.INTERVAL_HOUR; + + // Retry interval for clear/init when the transport is unavailable + private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR; + + private static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN"; + private static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT"; + private static final String RUN_CLEAR_ACTION = "android.app.backup.intent.CLEAR"; + private static final int MSG_RUN_BACKUP = 1; + private static final int MSG_RUN_FULL_BACKUP = 2; + private static final int MSG_RUN_RESTORE = 3; + private static final int MSG_RUN_CLEAR = 4; + private static final int MSG_RUN_INITIALIZE = 5; + private static final int MSG_RUN_GET_RESTORE_SETS = 6; + private static final int MSG_TIMEOUT = 7; + private static final int MSG_RESTORE_TIMEOUT = 8; + private static final int MSG_FULL_CONFIRMATION_TIMEOUT = 9; + private static final int MSG_RUN_FULL_RESTORE = 10; + private static final int MSG_RETRY_INIT = 11; + private static final int MSG_RETRY_CLEAR = 12; + + // backup task state machine tick + static final int MSG_BACKUP_RESTORE_STEP = 20; + static final int MSG_OP_COMPLETE = 21; + + // Timeout interval for deciding that a bind or clear-data has taken too long + static final long TIMEOUT_INTERVAL = 10 * 1000; + + // Timeout intervals for agent backup & restore operations + static final long TIMEOUT_BACKUP_INTERVAL = 30 * 1000; + static final long TIMEOUT_FULL_BACKUP_INTERVAL = 5 * 60 * 1000; + static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000; + static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000; + + // User confirmation timeout for a full backup/restore operation. It's this long in + // order to give them time to enter the backup password. + static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000; + + private Context mContext; + private PackageManager mPackageManager; + IPackageManager mPackageManagerBinder; + private IActivityManager mActivityManager; + private PowerManager mPowerManager; + private AlarmManager mAlarmManager; + private IMountService mMountService; + IBackupManager mBackupManagerBinder; + + boolean mEnabled; // access to this is synchronized on 'this' + boolean mProvisioned; + boolean mAutoRestore; + PowerManager.WakeLock mWakelock; + HandlerThread mHandlerThread; + BackupHandler mBackupHandler; + PendingIntent mRunBackupIntent, mRunInitIntent; + BroadcastReceiver mRunBackupReceiver, mRunInitReceiver; + // map UIDs to the set of participating packages under that UID + final SparseArray> mBackupParticipants + = new SparseArray>(); + // set of backup services that have pending changes + class BackupRequest { + public String packageName; + + BackupRequest(String pkgName) { + packageName = pkgName; + } + + public String toString() { + return "BackupRequest{pkg=" + packageName + "}"; + } + } + // Backups that we haven't started yet. Keys are package names. + HashMap mPendingBackups + = new HashMap(); + + // Pseudoname that we use for the Package Manager metadata "package" + static final String PACKAGE_MANAGER_SENTINEL = "@pm@"; + + // locking around the pending-backup management + final Object mQueueLock = new Object(); + + // The thread performing the sequence of queued backups binds to each app's agent + // in succession. Bind notifications are asynchronously delivered through the + // Activity Manager; use this lock object to signal when a requested binding has + // completed. + final Object mAgentConnectLock = new Object(); + IBackupAgent mConnectedAgent; + volatile boolean mBackupRunning; + volatile boolean mConnecting; + volatile long mLastBackupPass; + volatile long mNextBackupPass; + + // For debugging, we maintain a progress trace of operations during backup + static final boolean DEBUG_BACKUP_TRACE = true; + final List mBackupTrace = new ArrayList(); + + // A similar synchronization mechanism around clearing apps' data for restore + final Object mClearDataLock = new Object(); + volatile boolean mClearingData; + + // Transport bookkeeping + final HashMap mTransportNames + = new HashMap(); // component name -> registration name + final HashMap mTransports + = new HashMap(); // registration name -> binder + final ArrayList mTransportConnections + = new ArrayList(); + String mCurrentTransport; + ActiveRestoreSession mActiveRestoreSession; + + // Watch the device provisioning operation during setup + ContentObserver mProvisionedObserver; + + class ProvisionedObserver extends ContentObserver { + public ProvisionedObserver(Handler handler) { + super(handler); + } + + public void onChange(boolean selfChange) { + final boolean wasProvisioned = mProvisioned; + final boolean isProvisioned = deviceIsProvisioned(); + // latch: never unprovision + mProvisioned = wasProvisioned || isProvisioned; + if (MORE_DEBUG) { + Slog.d(TAG, "Provisioning change: was=" + wasProvisioned + + " is=" + isProvisioned + " now=" + mProvisioned); + } + + synchronized (mQueueLock) { + if (mProvisioned && !wasProvisioned && mEnabled) { + // we're now good to go, so start the backup alarms + if (MORE_DEBUG) Slog.d(TAG, "Now provisioned, so starting backups"); + startBackupAlarmsLocked(FIRST_BACKUP_INTERVAL); + } + } + } + } + + class RestoreGetSetsParams { + public IBackupTransport transport; + public ActiveRestoreSession session; + public IRestoreObserver observer; + + RestoreGetSetsParams(IBackupTransport _transport, ActiveRestoreSession _session, + IRestoreObserver _observer) { + transport = _transport; + session = _session; + observer = _observer; + } + } + + class RestoreParams { + public IBackupTransport transport; + public String dirName; + public IRestoreObserver observer; + public long token; + public PackageInfo pkgInfo; + public int pmToken; // in post-install restore, the PM's token for this transaction + public boolean needFullBackup; + public String[] filterSet; + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, PackageInfo _pkg, int _pmToken, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = _pkg; + pmToken = _pmToken; + needFullBackup = _needFullBackup; + filterSet = null; + } + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = null; + pmToken = 0; + needFullBackup = _needFullBackup; + filterSet = null; + } + + RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs, + long _token, String[] _filterSet, boolean _needFullBackup) { + transport = _transport; + dirName = _dirName; + observer = _obs; + token = _token; + pkgInfo = null; + pmToken = 0; + needFullBackup = _needFullBackup; + filterSet = _filterSet; + } + } + + class ClearParams { + public IBackupTransport transport; + public PackageInfo packageInfo; + + ClearParams(IBackupTransport _transport, PackageInfo _info) { + transport = _transport; + packageInfo = _info; + } + } + + class ClearRetryParams { + public String transportName; + public String packageName; + + ClearRetryParams(String transport, String pkg) { + transportName = transport; + packageName = pkg; + } + } + + class FullParams { + public ParcelFileDescriptor fd; + public final AtomicBoolean latch; + public IFullBackupRestoreObserver observer; + public String curPassword; // filled in by the confirmation step + public String encryptPassword; + + FullParams() { + latch = new AtomicBoolean(false); + } + } + + class FullBackupParams extends FullParams { + public boolean includeApks; + public boolean includeObbs; + public boolean includeShared; + public boolean allApps; + public boolean includeSystem; + public String[] packages; + + FullBackupParams(ParcelFileDescriptor output, boolean saveApks, boolean saveObbs, + boolean saveShared, boolean doAllApps, boolean doSystem, String[] pkgList) { + fd = output; + includeApks = saveApks; + includeObbs = saveObbs; + includeShared = saveShared; + allApps = doAllApps; + includeSystem = doSystem; + packages = pkgList; + } + } + + class FullRestoreParams extends FullParams { + FullRestoreParams(ParcelFileDescriptor input) { + fd = input; + } + } + + // Bookkeeping of in-flight operations for timeout etc. purposes. The operation + // token is the index of the entry in the pending-operations list. + static final int OP_PENDING = 0; + static final int OP_ACKNOWLEDGED = 1; + static final int OP_TIMEOUT = -1; + + class Operation { + public int state; + public BackupRestoreTask callback; + + Operation(int initialState, BackupRestoreTask callbackObj) { + state = initialState; + callback = callbackObj; + } + } + final SparseArray mCurrentOperations = new SparseArray(); + final Object mCurrentOpLock = new Object(); + final Random mTokenGenerator = new Random(); + + final SparseArray mFullConfirmations = new SparseArray(); + + // Where we keep our journal files and other bookkeeping + File mBaseStateDir; + File mDataDir; + File mJournalDir; + File mJournal; + + // Backup password, if any, and the file where it's saved. What is stored is not the + // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but + // persisted) salt. Validation is performed by running the challenge text through the + // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches + // the saved hash string, then the challenge text matches the originally supplied + // password text. + private final SecureRandom mRng = new SecureRandom(); + private String mPasswordHash; + private File mPasswordHashFile; + private byte[] mPasswordSalt; + + // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys + static final int PBKDF2_HASH_ROUNDS = 10000; + static final int PBKDF2_KEY_SIZE = 256; // bits + static final int PBKDF2_SALT_SIZE = 512; // bits + static final String ENCRYPTION_ALGORITHM_NAME = "AES-256"; + + // Keep a log of all the apps we've ever backed up, and what the + // dataset tokens are for both the current backup dataset and + // the ancestral dataset. + private File mEverStored; + HashSet mEverStoredApps = new HashSet(); + + static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1; // increment when the schema changes + File mTokenFile; + Set mAncestralPackages = null; + long mAncestralToken = 0; + long mCurrentToken = 0; + + // Persistently track the need to do a full init + static final String INIT_SENTINEL_FILE_NAME = "_need_init_"; + HashSet mPendingInits = new HashSet(); // transport names + + // Utility: build a new random integer token + int generateToken() { + int token; + do { + synchronized (mTokenGenerator) { + token = mTokenGenerator.nextInt(); + } + } while (token < 0); + return token; + } + + // ----- Asynchronous backup/restore handler thread ----- + + private class BackupHandler extends Handler { + public BackupHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + + switch (msg.what) { + case MSG_RUN_BACKUP: + { + mLastBackupPass = System.currentTimeMillis(); + mNextBackupPass = mLastBackupPass + BACKUP_INTERVAL; + + IBackupTransport transport = getTransport(mCurrentTransport); + if (transport == null) { + Slog.v(TAG, "Backup requested but no transport available"); + synchronized (mQueueLock) { + mBackupRunning = false; + } + mWakelock.release(); + break; + } + + // snapshot the pending-backup set and work on that + ArrayList queue = new ArrayList(); + File oldJournal = mJournal; + synchronized (mQueueLock) { + // Do we have any work to do? Construct the work queue + // then release the synchronization lock to actually run + // the backup. + if (mPendingBackups.size() > 0) { + for (BackupRequest b: mPendingBackups.values()) { + queue.add(b); + } + if (DEBUG) Slog.v(TAG, "clearing pending backups"); + mPendingBackups.clear(); + + // Start a new backup-queue journal file too + mJournal = null; + + } + } + + // At this point, we have started a new journal file, and the old + // file identity is being passed to the backup processing task. + // When it completes successfully, that old journal file will be + // deleted. If we crash prior to that, the old journal is parsed + // at next boot and the journaled requests fulfilled. + boolean staged = true; + if (queue.size() > 0) { + // Spin up a backup state sequence and set it running + try { + String dirName = transport.transportDirName(); + PerformBackupTask pbt = new PerformBackupTask(transport, dirName, + queue, oldJournal); + Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt); + sendMessage(pbtMessage); + } catch (RemoteException e) { + // unable to ask the transport its dir name -- transient failure, since + // the above check succeeded. Try again next time. + Slog.e(TAG, "Transport became unavailable attempting backup"); + staged = false; + } + } else { + Slog.v(TAG, "Backup requested but nothing pending"); + staged = false; + } + + if (!staged) { + // if we didn't actually hand off the wakelock, rewind until next time + synchronized (mQueueLock) { + mBackupRunning = false; + } + mWakelock.release(); + } + break; + } + + case MSG_BACKUP_RESTORE_STEP: + { + try { + BackupRestoreTask task = (BackupRestoreTask) msg.obj; + if (MORE_DEBUG) Slog.v(TAG, "Got next step for " + task + ", executing"); + task.execute(); + } catch (ClassCastException e) { + Slog.e(TAG, "Invalid backup task in flight, obj=" + msg.obj); + } + break; + } + + case MSG_OP_COMPLETE: + { + try { + BackupRestoreTask task = (BackupRestoreTask) msg.obj; + task.operationComplete(); + } catch (ClassCastException e) { + Slog.e(TAG, "Invalid completion in flight, obj=" + msg.obj); + } + break; + } + + case MSG_RUN_FULL_BACKUP: + { + // TODO: refactor full backup to be a looper-based state machine + // similar to normal backup/restore. + FullBackupParams params = (FullBackupParams)msg.obj; + PerformFullBackupTask task = new PerformFullBackupTask(params.fd, + params.observer, params.includeApks, params.includeObbs, + params.includeShared, params.curPassword, params.encryptPassword, + params.allApps, params.includeSystem, params.packages, params.latch); + (new Thread(task)).start(); + break; + } + + case MSG_RUN_RESTORE: + { + RestoreParams params = (RestoreParams)msg.obj; + Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer); + PerformRestoreTask task = new PerformRestoreTask( + params.transport, params.dirName, params.observer, + params.token, params.pkgInfo, params.pmToken, + params.needFullBackup, params.filterSet); + Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task); + sendMessage(restoreMsg); + break; + } + + case MSG_RUN_FULL_RESTORE: + { + // TODO: refactor full restore to be a looper-based state machine + // similar to normal backup/restore. + FullRestoreParams params = (FullRestoreParams)msg.obj; + PerformFullRestoreTask task = new PerformFullRestoreTask(params.fd, + params.curPassword, params.encryptPassword, + params.observer, params.latch); + (new Thread(task)).start(); + break; + } + + case MSG_RUN_CLEAR: + { + ClearParams params = (ClearParams)msg.obj; + (new PerformClearTask(params.transport, params.packageInfo)).run(); + break; + } + + case MSG_RETRY_CLEAR: + { + // reenqueues if the transport remains unavailable + ClearRetryParams params = (ClearRetryParams)msg.obj; + clearBackupData(params.transportName, params.packageName); + break; + } + + case MSG_RUN_INITIALIZE: + { + HashSet queue; + + // Snapshot the pending-init queue and work on that + synchronized (mQueueLock) { + queue = new HashSet(mPendingInits); + mPendingInits.clear(); + } + + (new PerformInitializeTask(queue)).run(); + break; + } + + case MSG_RETRY_INIT: + { + synchronized (mQueueLock) { + recordInitPendingLocked(msg.arg1 != 0, (String)msg.obj); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), + mRunInitIntent); + } + break; + } + + case MSG_RUN_GET_RESTORE_SETS: + { + // Like other async operations, this is entered with the wakelock held + RestoreSet[] sets = null; + RestoreGetSetsParams params = (RestoreGetSetsParams)msg.obj; + try { + sets = params.transport.getAvailableRestoreSets(); + // cache the result in the active session + synchronized (params.session) { + params.session.mRestoreSets = sets; + } + if (sets == null) EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + } catch (Exception e) { + Slog.e(TAG, "Error from transport getting set list"); + } finally { + if (params.observer != null) { + try { + params.observer.restoreSetsAvailable(sets); + } catch (RemoteException re) { + Slog.e(TAG, "Unable to report listing to observer"); + } catch (Exception e) { + Slog.e(TAG, "Restore observer threw", e); + } + } + + // Done: reset the session timeout clock + removeMessages(MSG_RESTORE_TIMEOUT); + sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); + + mWakelock.release(); + } + break; + } + + case MSG_TIMEOUT: + { + handleTimeout(msg.arg1, msg.obj); + break; + } + + case MSG_RESTORE_TIMEOUT: + { + synchronized (BackupManagerService.this) { + if (mActiveRestoreSession != null) { + // Client app left the restore session dangling. We know that it + // can't be in the middle of an actual restore operation because + // the timeout is suspended while a restore is in progress. Clean + // up now. + Slog.w(TAG, "Restore session timed out; aborting"); + post(mActiveRestoreSession.new EndRestoreRunnable( + BackupManagerService.this, mActiveRestoreSession)); + } + } + } + + case MSG_FULL_CONFIRMATION_TIMEOUT: + { + synchronized (mFullConfirmations) { + FullParams params = mFullConfirmations.get(msg.arg1); + if (params != null) { + Slog.i(TAG, "Full backup/restore timed out waiting for user confirmation"); + + // Release the waiter; timeout == completion + signalFullBackupRestoreCompletion(params); + + // Remove the token from the set + mFullConfirmations.delete(msg.arg1); + + // Report a timeout to the observer, if any + if (params.observer != null) { + try { + params.observer.onTimeout(); + } catch (RemoteException e) { + /* don't care if the app has gone away */ + } + } + } else { + Slog.d(TAG, "couldn't find params for token " + msg.arg1); + } + } + break; + } + } + } + } + + // ----- Debug-only backup operation trace ----- + void addBackupTrace(String s) { + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + mBackupTrace.add(s); + } + } + } + + void clearBackupTrace() { + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + mBackupTrace.clear(); + } + } + } + + // ----- Main service implementation ----- + + public BackupManagerService(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + mPackageManagerBinder = AppGlobals.getPackageManager(); + mActivityManager = ActivityManagerNative.getDefault(); + + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + + mBackupManagerBinder = asInterface(asBinder()); + + // spin up the backup/restore handler thread + mHandlerThread = new HandlerThread("backup", Process.THREAD_PRIORITY_BACKGROUND); + mHandlerThread.start(); + mBackupHandler = new BackupHandler(mHandlerThread.getLooper()); + + // Set up our bookkeeping + final ContentResolver resolver = context.getContentResolver(); + boolean areEnabled = Settings.Secure.getInt(resolver, + Settings.Secure.BACKUP_ENABLED, 0) != 0; + mProvisioned = Settings.Global.getInt(resolver, + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + mAutoRestore = Settings.Secure.getInt(resolver, + Settings.Secure.BACKUP_AUTO_RESTORE, 1) != 0; + + mProvisionedObserver = new ProvisionedObserver(mBackupHandler); + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mProvisionedObserver); + + // If Encrypted file systems is enabled or disabled, this call will return the + // correct directory. + mBaseStateDir = new File(Environment.getSecureDataDirectory(), "backup"); + mBaseStateDir.mkdirs(); + if (!SELinux.restorecon(mBaseStateDir)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir); + } + mDataDir = Environment.getDownloadCacheDirectory(); + + mPasswordHashFile = new File(mBaseStateDir, "pwhash"); + if (mPasswordHashFile.exists()) { + FileInputStream fin = null; + DataInputStream in = null; + try { + fin = new FileInputStream(mPasswordHashFile); + in = new DataInputStream(new BufferedInputStream(fin)); + // integer length of the salt array, followed by the salt, + // then the hex pw hash string + int saltLen = in.readInt(); + byte[] salt = new byte[saltLen]; + in.readFully(salt); + mPasswordHash = in.readUTF(); + mPasswordSalt = salt; + } catch (IOException e) { + Slog.e(TAG, "Unable to read saved backup pw hash"); + } finally { + try { + if (in != null) in.close(); + if (fin != null) fin.close(); + } catch (IOException e) { + Slog.w(TAG, "Unable to close streams"); + } + } + } + + // Alarm receivers for scheduled backups & initialization operations + mRunBackupReceiver = new RunBackupReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(RUN_BACKUP_ACTION); + context.registerReceiver(mRunBackupReceiver, filter, + android.Manifest.permission.BACKUP, null); + + mRunInitReceiver = new RunInitializeReceiver(); + filter = new IntentFilter(); + filter.addAction(RUN_INITIALIZE_ACTION); + context.registerReceiver(mRunInitReceiver, filter, + android.Manifest.permission.BACKUP, null); + + Intent backupIntent = new Intent(RUN_BACKUP_ACTION); + backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mRunBackupIntent = PendingIntent.getBroadcast(context, MSG_RUN_BACKUP, backupIntent, 0); + + Intent initIntent = new Intent(RUN_INITIALIZE_ACTION); + backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mRunInitIntent = PendingIntent.getBroadcast(context, MSG_RUN_INITIALIZE, initIntent, 0); + + // Set up the backup-request journaling + mJournalDir = new File(mBaseStateDir, "pending"); + mJournalDir.mkdirs(); // creates mBaseStateDir along the way + mJournal = null; // will be created on first use + + // Set up the various sorts of package tracking we do + initPackageTracking(); + + // Build our mapping of uid to backup client services. This implicitly + // schedules a backup pass on the Package Manager metadata the first + // time anything needs to be backed up. + synchronized (mBackupParticipants) { + addPackageParticipantsLocked(null); + } + + // Set up our transport options and initialize the default transport + // TODO: Don't create transports that we don't need to? + mCurrentTransport = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.BACKUP_TRANSPORT); + if ("".equals(mCurrentTransport)) { + mCurrentTransport = null; + } + if (DEBUG) Slog.v(TAG, "Starting with transport " + mCurrentTransport); + + // Find transport hosts and bind to their services + Intent transportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST); + List hosts = mPackageManager.queryIntentServicesAsUser( + transportServiceIntent, 0, UserHandle.USER_OWNER); + if (DEBUG) { + Slog.v(TAG, "Found transports: " + ((hosts == null) ? "null" : hosts.size())); + } + if (hosts != null) { + if (MORE_DEBUG) { + for (int i = 0; i < hosts.size(); i++) { + ServiceInfo info = hosts.get(i).serviceInfo; + Slog.v(TAG, " " + info.packageName + "/" + info.name); + } + } + for (int i = 0; i < hosts.size(); i++) { + try { + ServiceInfo info = hosts.get(i).serviceInfo; + PackageInfo packInfo = mPackageManager.getPackageInfo(info.packageName, 0); + if ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0) { + ComponentName svcName = new ComponentName(info.packageName, info.name); + if (DEBUG) { + Slog.i(TAG, "Binding to transport host " + svcName); + } + Intent intent = new Intent(transportServiceIntent); + intent.setComponent(svcName); + TransportConnection connection = new TransportConnection(); + mTransportConnections.add(connection); + context.bindServiceAsUser(intent, + connection, Context.BIND_AUTO_CREATE, + UserHandle.OWNER); + } else { + Slog.w(TAG, "Transport package not privileged: " + info.packageName); + } + } catch (Exception e) { + Slog.e(TAG, "Problem resolving transport service: " + e.getMessage()); + } + } + } + + // Now that we know about valid backup participants, parse any + // leftover journal files into the pending backup set + parseLeftoverJournals(); + + // Power management + mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"); + + // Start the backup passes going + setBackupEnabled(areEnabled); + } + + private class RunBackupReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (RUN_BACKUP_ACTION.equals(intent.getAction())) { + synchronized (mQueueLock) { + if (mPendingInits.size() > 0) { + // If there are pending init operations, we process those + // and then settle into the usual periodic backup schedule. + if (DEBUG) Slog.v(TAG, "Init pending at scheduled backup"); + try { + mAlarmManager.cancel(mRunInitIntent); + mRunInitIntent.send(); + } catch (PendingIntent.CanceledException ce) { + Slog.e(TAG, "Run init intent cancelled"); + // can't really do more than bail here + } + } else { + // Don't run backups now if we're disabled or not yet + // fully set up. + if (mEnabled && mProvisioned) { + if (!mBackupRunning) { + if (DEBUG) Slog.v(TAG, "Running a backup pass"); + + // Acquire the wakelock and pass it to the backup thread. it will + // be released once backup concludes. + mBackupRunning = true; + mWakelock.acquire(); + + Message msg = mBackupHandler.obtainMessage(MSG_RUN_BACKUP); + mBackupHandler.sendMessage(msg); + } else { + Slog.i(TAG, "Backup time but one already running"); + } + } else { + Slog.w(TAG, "Backup pass but e=" + mEnabled + " p=" + mProvisioned); + } + } + } + } + } + } + + private class RunInitializeReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (RUN_INITIALIZE_ACTION.equals(intent.getAction())) { + synchronized (mQueueLock) { + if (DEBUG) Slog.v(TAG, "Running a device init"); + + // Acquire the wakelock and pass it to the init thread. it will + // be released once init concludes. + mWakelock.acquire(); + + Message msg = mBackupHandler.obtainMessage(MSG_RUN_INITIALIZE); + mBackupHandler.sendMessage(msg); + } + } + } + } + + private void initPackageTracking() { + if (DEBUG) Slog.v(TAG, "Initializing package tracking"); + + // Remember our ancestral dataset + mTokenFile = new File(mBaseStateDir, "ancestral"); + try { + RandomAccessFile tf = new RandomAccessFile(mTokenFile, "r"); + int version = tf.readInt(); + if (version == CURRENT_ANCESTRAL_RECORD_VERSION) { + mAncestralToken = tf.readLong(); + mCurrentToken = tf.readLong(); + + int numPackages = tf.readInt(); + if (numPackages >= 0) { + mAncestralPackages = new HashSet(); + for (int i = 0; i < numPackages; i++) { + String pkgName = tf.readUTF(); + mAncestralPackages.add(pkgName); + } + } + } + tf.close(); + } catch (FileNotFoundException fnf) { + // Probably innocuous + Slog.v(TAG, "No ancestral data"); + } catch (IOException e) { + Slog.w(TAG, "Unable to read token file", e); + } + + // Keep a log of what apps we've ever backed up. Because we might have + // rebooted in the middle of an operation that was removing something from + // this log, we sanity-check its contents here and reconstruct it. + mEverStored = new File(mBaseStateDir, "processed"); + File tempProcessedFile = new File(mBaseStateDir, "processed.new"); + + // If we were in the middle of removing something from the ever-backed-up + // file, there might be a transient "processed.new" file still present. + // Ignore it -- we'll validate "processed" against the current package set. + if (tempProcessedFile.exists()) { + tempProcessedFile.delete(); + } + + // If there are previous contents, parse them out then start a new + // file to continue the recordkeeping. + if (mEverStored.exists()) { + RandomAccessFile temp = null; + RandomAccessFile in = null; + + try { + temp = new RandomAccessFile(tempProcessedFile, "rws"); + in = new RandomAccessFile(mEverStored, "r"); + + while (true) { + PackageInfo info; + String pkg = in.readUTF(); + try { + info = mPackageManager.getPackageInfo(pkg, 0); + mEverStoredApps.add(pkg); + temp.writeUTF(pkg); + if (MORE_DEBUG) Slog.v(TAG, " + " + pkg); + } catch (NameNotFoundException e) { + // nope, this package was uninstalled; don't include it + if (MORE_DEBUG) Slog.v(TAG, " - " + pkg); + } + } + } catch (EOFException e) { + // Once we've rewritten the backup history log, atomically replace the + // old one with the new one then reopen the file for continuing use. + if (!tempProcessedFile.renameTo(mEverStored)) { + Slog.e(TAG, "Error renaming " + tempProcessedFile + " to " + mEverStored); + } + } catch (IOException e) { + Slog.e(TAG, "Error in processed file", e); + } finally { + try { if (temp != null) temp.close(); } catch (IOException e) {} + try { if (in != null) in.close(); } catch (IOException e) {} + } + } + + // Register for broadcasts about package install, etc., so we can + // update the provider list. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiver(mBroadcastReceiver, filter); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mBroadcastReceiver, sdFilter); + } + + private void parseLeftoverJournals() { + for (File f : mJournalDir.listFiles()) { + if (mJournal == null || f.compareTo(mJournal) != 0) { + // This isn't the current journal, so it must be a leftover. Read + // out the package names mentioned there and schedule them for + // backup. + RandomAccessFile in = null; + try { + Slog.i(TAG, "Found stale backup journal, scheduling"); + in = new RandomAccessFile(f, "r"); + while (true) { + String packageName = in.readUTF(); + Slog.i(TAG, " " + packageName); + dataChangedImpl(packageName); + } + } catch (EOFException e) { + // no more data; we're done + } catch (Exception e) { + Slog.e(TAG, "Can't read " + f, e); + } finally { + // close/delete the file + try { if (in != null) in.close(); } catch (IOException e) {} + f.delete(); + } + } + } + } + + private SecretKey buildPasswordKey(String pw, byte[] salt, int rounds) { + return buildCharArrayKey(pw.toCharArray(), salt, rounds); + } + + private SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds) { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); + return keyFactory.generateSecret(ks); + } catch (InvalidKeySpecException e) { + Slog.e(TAG, "Invalid key spec for PBKDF2!"); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "PBKDF2 unavailable!"); + } + return null; + } + + private String buildPasswordHash(String pw, byte[] salt, int rounds) { + SecretKey key = buildPasswordKey(pw, salt, rounds); + if (key != null) { + return byteArrayToHex(key.getEncoded()); + } + return null; + } + + private String byteArrayToHex(byte[] data) { + StringBuilder buf = new StringBuilder(data.length * 2); + for (int i = 0; i < data.length; i++) { + buf.append(Byte.toHexString(data[i], true)); + } + return buf.toString(); + } + + private byte[] hexToByteArray(String digits) { + final int bytes = digits.length() / 2; + if (2*bytes != digits.length()) { + throw new IllegalArgumentException("Hex string must have an even number of digits"); + } + + byte[] result = new byte[bytes]; + for (int i = 0; i < digits.length(); i += 2) { + result[i/2] = (byte) Integer.parseInt(digits.substring(i, i+2), 16); + } + return result; + } + + private byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds) { + char[] mkAsChar = new char[pwBytes.length]; + for (int i = 0; i < pwBytes.length; i++) { + mkAsChar[i] = (char) pwBytes[i]; + } + + Key checksum = buildCharArrayKey(mkAsChar, salt, rounds); + return checksum.getEncoded(); + } + + // Used for generating random salts or passwords + private byte[] randomBytes(int bits) { + byte[] array = new byte[bits / 8]; + mRng.nextBytes(array); + return array; + } + + // Backup password management + boolean passwordMatchesSaved(String candidatePw, int rounds) { + // First, on an encrypted device we require matching the device pw + final boolean isEncrypted; + try { + isEncrypted = (mMountService.getEncryptionState() != + IMountService.ENCRYPTION_STATE_NONE); + if (isEncrypted) { + if (DEBUG) { + Slog.i(TAG, "Device encrypted; verifying against device data pw"); + } + // 0 means the password validated + // -2 means device not encrypted + // Any other result is either password failure or an error condition, + // so we refuse the match + final int result = mMountService.verifyEncryptionPassword(candidatePw); + if (result == 0) { + if (MORE_DEBUG) Slog.d(TAG, "Pw verifies"); + return true; + } else if (result != -2) { + if (MORE_DEBUG) Slog.d(TAG, "Pw mismatch"); + return false; + } else { + // ...else the device is supposedly not encrypted. HOWEVER, the + // query about the encryption state said that the device *is* + // encrypted, so ... we may have a problem. Log it and refuse + // the backup. + Slog.e(TAG, "verified encryption state mismatch against query; no match allowed"); + return false; + } + } + } catch (Exception e) { + // Something went wrong talking to the mount service. This is very bad; + // assume that we fail password validation. + return false; + } + + if (mPasswordHash == null) { + // no current password case -- require that 'currentPw' be null or empty + if (candidatePw == null || "".equals(candidatePw)) { + return true; + } // else the non-empty candidate does not match the empty stored pw + } else { + // hash the stated current pw and compare to the stored one + if (candidatePw != null && candidatePw.length() > 0) { + String currentPwHash = buildPasswordHash(candidatePw, mPasswordSalt, rounds); + if (mPasswordHash.equalsIgnoreCase(currentPwHash)) { + // candidate hash matches the stored hash -- the password matches + return true; + } + } // else the stored pw is nonempty but the candidate is empty; no match + } + return false; + } + + @Override + public boolean setBackupPassword(String currentPw, String newPw) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupPassword"); + + // If the supplied pw doesn't hash to the the saved one, fail + if (!passwordMatchesSaved(currentPw, PBKDF2_HASH_ROUNDS)) { + return false; + } + + // Clearing the password is okay + if (newPw == null || newPw.isEmpty()) { + if (mPasswordHashFile.exists()) { + if (!mPasswordHashFile.delete()) { + // Unable to delete the old pw file, so fail + Slog.e(TAG, "Unable to clear backup password"); + return false; + } + } + mPasswordHash = null; + mPasswordSalt = null; + return true; + } + + try { + // Okay, build the hash of the new backup password + byte[] salt = randomBytes(PBKDF2_SALT_SIZE); + String newPwHash = buildPasswordHash(newPw, salt, PBKDF2_HASH_ROUNDS); + + OutputStream pwf = null, buffer = null; + DataOutputStream out = null; + try { + pwf = new FileOutputStream(mPasswordHashFile); + buffer = new BufferedOutputStream(pwf); + out = new DataOutputStream(buffer); + // integer length of the salt array, followed by the salt, + // then the hex pw hash string + out.writeInt(salt.length); + out.write(salt); + out.writeUTF(newPwHash); + out.flush(); + mPasswordHash = newPwHash; + mPasswordSalt = salt; + return true; + } finally { + if (out != null) out.close(); + if (buffer != null) buffer.close(); + if (pwf != null) pwf.close(); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to set backup password"); + } + return false; + } + + @Override + public boolean hasBackupPassword() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "hasBackupPassword"); + + try { + return (mMountService.getEncryptionState() != IMountService.ENCRYPTION_STATE_NONE) + || (mPasswordHash != null && mPasswordHash.length() > 0); + } catch (Exception e) { + // If we can't talk to the mount service we have a serious problem; fail + // "secure" i.e. assuming that we require a password + return true; + } + } + + // Maintain persistent state around whether need to do an initialize operation. + // Must be called with the queue lock held. + void recordInitPendingLocked(boolean isPending, String transportName) { + if (DEBUG) Slog.i(TAG, "recordInitPendingLocked: " + isPending + + " on transport " + transportName); + mBackupHandler.removeMessages(MSG_RETRY_INIT); + + try { + IBackupTransport transport = getTransport(transportName); + if (transport != null) { + String transportDirName = transport.transportDirName(); + File stateDir = new File(mBaseStateDir, transportDirName); + File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME); + + if (isPending) { + // We need an init before we can proceed with sending backup data. + // Record that with an entry in our set of pending inits, as well as + // journaling it via creation of a sentinel file. + mPendingInits.add(transportName); + try { + (new FileOutputStream(initPendingFile)).close(); + } catch (IOException ioe) { + // Something is badly wrong with our permissions; just try to move on + } + } else { + // No more initialization needed; wipe the journal and reset our state. + initPendingFile.delete(); + mPendingInits.remove(transportName); + } + return; // done; don't fall through to the error case + } + } catch (RemoteException e) { + // transport threw when asked its name; fall through to the lookup-failed case + } + + // The named transport doesn't exist or threw. This operation is + // important, so we record the need for a an init and post a message + // to retry the init later. + if (isPending) { + mPendingInits.add(transportName); + mBackupHandler.sendMessageDelayed( + mBackupHandler.obtainMessage(MSG_RETRY_INIT, + (isPending ? 1 : 0), + 0, + transportName), + TRANSPORT_RETRY_INTERVAL); + } + } + + // Reset all of our bookkeeping, in response to having been told that + // the backend data has been wiped [due to idle expiry, for example], + // so we must re-upload all saved settings. + void resetBackupState(File stateFileDir) { + synchronized (mQueueLock) { + // Wipe the "what we've ever backed up" tracking + mEverStoredApps.clear(); + mEverStored.delete(); + + mCurrentToken = 0; + writeRestoreTokens(); + + // Remove all the state files + for (File sf : stateFileDir.listFiles()) { + // ... but don't touch the needs-init sentinel + if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) { + sf.delete(); + } + } + } + + // Enqueue a new backup of every participant + synchronized (mBackupParticipants) { + final int N = mBackupParticipants.size(); + for (int i=0; i participants = mBackupParticipants.valueAt(i); + if (participants != null) { + for (String packageName : participants) { + dataChangedImpl(packageName); + } + } + } + } + } + + // Add a transport to our set of available backends. If 'transport' is null, this + // is an unregistration, and the transport's entry is removed from our bookkeeping. + private void registerTransport(String name, String component, IBackupTransport transport) { + synchronized (mTransports) { + if (DEBUG) Slog.v(TAG, "Registering transport " + + component + "::" + name + " = " + transport); + if (transport != null) { + mTransports.put(name, transport); + mTransportNames.put(component, name); + } else { + mTransports.remove(mTransportNames.get(component)); + mTransportNames.remove(component); + // Nothing further to do in the unregistration case + return; + } + } + + // If the init sentinel file exists, we need to be sure to perform the init + // as soon as practical. We also create the state directory at registration + // time to ensure it's present from the outset. + try { + String transportName = transport.transportDirName(); + File stateDir = new File(mBaseStateDir, transportName); + stateDir.mkdirs(); + + File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME); + if (initSentinel.exists()) { + synchronized (mQueueLock) { + mPendingInits.add(transportName); + + // TODO: pick a better starting time than now + 1 minute + long delay = 1000 * 60; // one minute, in milliseconds + mAlarmManager.set(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + delay, mRunInitIntent); + } + } + } catch (RemoteException e) { + // the transport threw when asked its file naming prefs; declare it invalid + Slog.e(TAG, "Unable to register transport as " + name); + mTransportNames.remove(component); + mTransports.remove(name); + } + } + + // ----- Track installation/removal of packages ----- + BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (DEBUG) Slog.d(TAG, "Received broadcast " + intent); + + String action = intent.getAction(); + boolean replacing = false; + boolean added = false; + Bundle extras = intent.getExtras(); + String pkgList[] = null; + if (Intent.ACTION_PACKAGE_ADDED.equals(action) || + Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + Uri uri = intent.getData(); + if (uri == null) { + return; + } + String pkgName = uri.getSchemeSpecificPart(); + if (pkgName != null) { + pkgList = new String[] { pkgName }; + } + added = Intent.ACTION_PACKAGE_ADDED.equals(action); + replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + added = true; + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + added = false; + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + } + + if (pkgList == null || pkgList.length == 0) { + return; + } + + final int uid = extras.getInt(Intent.EXTRA_UID); + if (added) { + synchronized (mBackupParticipants) { + if (replacing) { + // This is the package-replaced case; we just remove the entry + // under the old uid and fall through to re-add. + removePackageParticipantsLocked(pkgList, uid); + } + addPackageParticipantsLocked(pkgList); + } + } else { + if (replacing) { + // The package is being updated. We'll receive a PACKAGE_ADDED shortly. + } else { + synchronized (mBackupParticipants) { + removePackageParticipantsLocked(pkgList, uid); + } + } + } + } + }; + + // ----- Track connection to transports service ----- + class TransportConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (DEBUG) Slog.v(TAG, "Connected to transport " + component); + try { + IBackupTransport transport = IBackupTransport.Stub.asInterface(service); + registerTransport(transport.name(), component.flattenToShortString(), transport); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to register transport " + component); + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (DEBUG) Slog.v(TAG, "Disconnected from transport " + component); + registerTransport(null, component.flattenToShortString(), null); + } + }; + + // Add the backup agents in the given packages to our set of known backup participants. + // If 'packageNames' is null, adds all backup agents in the whole system. + void addPackageParticipantsLocked(String[] packageNames) { + // Look for apps that define the android:backupAgent attribute + List targetApps = allAgentPackages(); + if (packageNames != null) { + if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: #" + packageNames.length); + for (String packageName : packageNames) { + addPackageParticipantsLockedInner(packageName, targetApps); + } + } else { + if (DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: all"); + addPackageParticipantsLockedInner(null, targetApps); + } + } + + private void addPackageParticipantsLockedInner(String packageName, + List targetPkgs) { + if (MORE_DEBUG) { + Slog.v(TAG, "Examining " + packageName + " for backup agent"); + } + + for (PackageInfo pkg : targetPkgs) { + if (packageName == null || pkg.packageName.equals(packageName)) { + int uid = pkg.applicationInfo.uid; + HashSet set = mBackupParticipants.get(uid); + if (set == null) { + set = new HashSet(); + mBackupParticipants.put(uid, set); + } + set.add(pkg.packageName); + if (MORE_DEBUG) Slog.v(TAG, "Agent found; added"); + + // Schedule a backup for it on general principles + if (DEBUG) Slog.i(TAG, "Scheduling backup for new app " + pkg.packageName); + dataChangedImpl(pkg.packageName); + } + } + } + + // Remove the given packages' entries from our known active set. + void removePackageParticipantsLocked(String[] packageNames, int oldUid) { + if (packageNames == null) { + Slog.w(TAG, "removePackageParticipants with null list"); + return; + } + + if (DEBUG) Slog.v(TAG, "removePackageParticipantsLocked: uid=" + oldUid + + " #" + packageNames.length); + for (String pkg : packageNames) { + // Known previous UID, so we know which package set to check + HashSet set = mBackupParticipants.get(oldUid); + if (set != null && set.contains(pkg)) { + removePackageFromSetLocked(set, pkg); + if (set.isEmpty()) { + if (MORE_DEBUG) Slog.v(TAG, " last one of this uid; purging set"); + mBackupParticipants.remove(oldUid); + } + } + } + } + + private void removePackageFromSetLocked(final HashSet set, + final String packageName) { + if (set.contains(packageName)) { + // Found it. Remove this one package from the bookkeeping, and + // if it's the last participating app under this uid we drop the + // (now-empty) set as well. + // Note that we deliberately leave it 'known' in the "ever backed up" + // bookkeeping so that its current-dataset data will be retrieved + // if the app is subsequently reinstalled + if (MORE_DEBUG) Slog.v(TAG, " removing participant " + packageName); + set.remove(packageName); + mPendingBackups.remove(packageName); + } + } + + // Returns the set of all applications that define an android:backupAgent attribute + List allAgentPackages() { + // !!! TODO: cache this and regenerate only when necessary + int flags = PackageManager.GET_SIGNATURES; + List packages = mPackageManager.getInstalledPackages(flags); + int N = packages.size(); + for (int a = N-1; a >= 0; a--) { + PackageInfo pkg = packages.get(a); + try { + ApplicationInfo app = pkg.applicationInfo; + if (((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) + || app.backupAgentName == null) { + packages.remove(a); + } + else { + // we will need the shared library path, so look that up and store it here + app = mPackageManager.getApplicationInfo(pkg.packageName, + PackageManager.GET_SHARED_LIBRARY_FILES); + pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles; + } + } catch (NameNotFoundException e) { + packages.remove(a); + } + } + return packages; + } + + // Called from the backup task: record that the given app has been successfully + // backed up at least once + void logBackupComplete(String packageName) { + if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return; + + synchronized (mEverStoredApps) { + if (!mEverStoredApps.add(packageName)) return; + + RandomAccessFile out = null; + try { + out = new RandomAccessFile(mEverStored, "rws"); + out.seek(out.length()); + out.writeUTF(packageName); + } catch (IOException e) { + Slog.e(TAG, "Can't log backup of " + packageName + " to " + mEverStored); + } finally { + try { if (out != null) out.close(); } catch (IOException e) {} + } + } + } + + // Remove our awareness of having ever backed up the given package + void removeEverBackedUp(String packageName) { + if (DEBUG) Slog.v(TAG, "Removing backed-up knowledge of " + packageName); + if (MORE_DEBUG) Slog.v(TAG, "New set:"); + + synchronized (mEverStoredApps) { + // Rewrite the file and rename to overwrite. If we reboot in the middle, + // we'll recognize on initialization time that the package no longer + // exists and fix it up then. + File tempKnownFile = new File(mBaseStateDir, "processed.new"); + RandomAccessFile known = null; + try { + known = new RandomAccessFile(tempKnownFile, "rws"); + mEverStoredApps.remove(packageName); + for (String s : mEverStoredApps) { + known.writeUTF(s); + if (MORE_DEBUG) Slog.v(TAG, " " + s); + } + known.close(); + known = null; + if (!tempKnownFile.renameTo(mEverStored)) { + throw new IOException("Can't rename " + tempKnownFile + " to " + mEverStored); + } + } catch (IOException e) { + // Bad: we couldn't create the new copy. For safety's sake we + // abandon the whole process and remove all what's-backed-up + // state entirely, meaning we'll force a backup pass for every + // participant on the next boot or [re]install. + Slog.w(TAG, "Error rewriting " + mEverStored, e); + mEverStoredApps.clear(); + tempKnownFile.delete(); + mEverStored.delete(); + } finally { + try { if (known != null) known.close(); } catch (IOException e) {} + } + } + } + + // Persistently record the current and ancestral backup tokens as well + // as the set of packages with data [supposedly] available in the + // ancestral dataset. + void writeRestoreTokens() { + try { + RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd"); + + // First, the version number of this record, for futureproofing + af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION); + + // Write the ancestral and current tokens + af.writeLong(mAncestralToken); + af.writeLong(mCurrentToken); + + // Now write the set of ancestral packages + if (mAncestralPackages == null) { + af.writeInt(-1); + } else { + af.writeInt(mAncestralPackages.size()); + if (DEBUG) Slog.v(TAG, "Ancestral packages: " + mAncestralPackages.size()); + for (String pkgName : mAncestralPackages) { + af.writeUTF(pkgName); + if (MORE_DEBUG) Slog.v(TAG, " " + pkgName); + } + } + af.close(); + } catch (IOException e) { + Slog.w(TAG, "Unable to write token file:", e); + } + } + + // Return the given transport + private IBackupTransport getTransport(String transportName) { + synchronized (mTransports) { + IBackupTransport transport = mTransports.get(transportName); + if (transport == null) { + Slog.w(TAG, "Requested unavailable transport: " + transportName); + } + return transport; + } + } + + // fire off a backup agent, blocking until it attaches or times out + IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) { + IBackupAgent agent = null; + synchronized(mAgentConnectLock) { + mConnecting = true; + mConnectedAgent = null; + try { + if (mActivityManager.bindBackupAgent(app, mode)) { + Slog.d(TAG, "awaiting agent for " + app); + + // success; wait for the agent to arrive + // only wait 10 seconds for the bind to happen + long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; + while (mConnecting && mConnectedAgent == null + && (System.currentTimeMillis() < timeoutMark)) { + try { + mAgentConnectLock.wait(5000); + } catch (InterruptedException e) { + // just bail + if (DEBUG) Slog.w(TAG, "Interrupted: " + e); + mActivityManager.clearPendingBackup(); + return null; + } + } + + // if we timed out with no connect, abort and move on + if (mConnecting == true) { + Slog.w(TAG, "Timeout waiting for agent " + app); + mActivityManager.clearPendingBackup(); + return null; + } + if (DEBUG) Slog.i(TAG, "got agent " + mConnectedAgent); + agent = mConnectedAgent; + } + } catch (RemoteException e) { + // can't happen - ActivityManager is local + } + } + return agent; + } + + // clear an application's data, blocking until the operation completes or times out + void clearApplicationDataSynchronous(String packageName) { + // Don't wipe packages marked allowClearUserData=false + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) == 0) { + if (MORE_DEBUG) Slog.i(TAG, "allowClearUserData=false so not wiping " + + packageName); + return; + } + } catch (NameNotFoundException e) { + Slog.w(TAG, "Tried to clear data for " + packageName + " but not found"); + return; + } + + ClearDataObserver observer = new ClearDataObserver(); + + synchronized(mClearDataLock) { + mClearingData = true; + try { + mActivityManager.clearApplicationUserData(packageName, observer, 0); + } catch (RemoteException e) { + // can't happen because the activity manager is in this process + } + + // only wait 10 seconds for the clear data to happen + long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL; + while (mClearingData && (System.currentTimeMillis() < timeoutMark)) { + try { + mClearDataLock.wait(5000); + } catch (InterruptedException e) { + // won't happen, but still. + mClearingData = false; + } + } + } + } + + class ClearDataObserver extends IPackageDataObserver.Stub { + public void onRemoveCompleted(String packageName, boolean succeeded) { + synchronized(mClearDataLock) { + mClearingData = false; + mClearDataLock.notifyAll(); + } + } + } + + // Get the restore-set token for the best-available restore set for this package: + // the active set if possible, else the ancestral one. Returns zero if none available. + long getAvailableRestoreToken(String packageName) { + long token = mAncestralToken; + synchronized (mQueueLock) { + if (mEverStoredApps.contains(packageName)) { + token = mCurrentToken; + } + } + return token; + } + + // ----- + // Interface and methods used by the asynchronous-with-timeout backup/restore operations + + interface BackupRestoreTask { + // Execute one tick of whatever state machine the task implements + void execute(); + + // An operation that wanted a callback has completed + void operationComplete(); + + // An operation that wanted a callback has timed out + void handleTimeout(); + } + + void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback) { + if (MORE_DEBUG) Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token) + + " interval=" + interval); + synchronized (mCurrentOpLock) { + mCurrentOperations.put(token, new Operation(OP_PENDING, callback)); + + Message msg = mBackupHandler.obtainMessage(MSG_TIMEOUT, token, 0, callback); + mBackupHandler.sendMessageDelayed(msg, interval); + } + } + + // synchronous waiter case + boolean waitUntilOperationComplete(int token) { + if (MORE_DEBUG) Slog.i(TAG, "Blocking until operation complete for " + + Integer.toHexString(token)); + int finalState = OP_PENDING; + Operation op = null; + synchronized (mCurrentOpLock) { + while (true) { + op = mCurrentOperations.get(token); + if (op == null) { + // mysterious disappearance: treat as success with no callback + break; + } else { + if (op.state == OP_PENDING) { + try { + mCurrentOpLock.wait(); + } catch (InterruptedException e) {} + // When the wait is notified we loop around and recheck the current state + } else { + // No longer pending; we're done + finalState = op.state; + break; + } + } + } + } + + mBackupHandler.removeMessages(MSG_TIMEOUT); + if (MORE_DEBUG) Slog.v(TAG, "operation " + Integer.toHexString(token) + + " complete: finalState=" + finalState); + return finalState == OP_ACKNOWLEDGED; + } + + void handleTimeout(int token, Object obj) { + // Notify any synchronous waiters + Operation op = null; + synchronized (mCurrentOpLock) { + op = mCurrentOperations.get(token); + if (MORE_DEBUG) { + if (op == null) Slog.w(TAG, "Timeout of token " + Integer.toHexString(token) + + " but no op found"); + } + int state = (op != null) ? op.state : OP_TIMEOUT; + if (state == OP_PENDING) { + if (DEBUG) Slog.v(TAG, "TIMEOUT: token=" + Integer.toHexString(token)); + op.state = OP_TIMEOUT; + mCurrentOperations.put(token, op); + } + mCurrentOpLock.notifyAll(); + } + + // If there's a TimeoutHandler for this event, call it + if (op != null && op.callback != null) { + op.callback.handleTimeout(); + } + } + + // ----- Back up a set of applications via a worker thread ----- + + enum BackupState { + INITIAL, + RUNNING_QUEUE, + FINAL + } + + class PerformBackupTask implements BackupRestoreTask { + private static final String TAG = "PerformBackupTask"; + + IBackupTransport mTransport; + ArrayList mQueue; + ArrayList mOriginalQueue; + File mStateDir; + File mJournal; + BackupState mCurrentState; + + // carried information about the current in-flight operation + PackageInfo mCurrentPackage; + File mSavedStateName; + File mBackupDataName; + File mNewStateName; + ParcelFileDescriptor mSavedState; + ParcelFileDescriptor mBackupData; + ParcelFileDescriptor mNewState; + int mStatus; + boolean mFinished; + + public PerformBackupTask(IBackupTransport transport, String dirName, + ArrayList queue, File journal) { + mTransport = transport; + mOriginalQueue = queue; + mJournal = journal; + + mStateDir = new File(mBaseStateDir, dirName); + + mCurrentState = BackupState.INITIAL; + mFinished = false; + + addBackupTrace("STATE => INITIAL"); + } + + // Main entry point: perform one chunk of work, updating the state as appropriate + // and reposting the next chunk to the primary backup handler thread. + @Override + public void execute() { + switch (mCurrentState) { + case INITIAL: + beginBackup(); + break; + + case RUNNING_QUEUE: + invokeNextAgent(); + break; + + case FINAL: + if (!mFinished) finalizeBackup(); + else { + Slog.e(TAG, "Duplicate finish"); + } + mFinished = true; + break; + } + } + + // We're starting a backup pass. Initialize the transport and send + // the PM metadata blob if we haven't already. + void beginBackup() { + if (DEBUG_BACKUP_TRACE) { + clearBackupTrace(); + StringBuilder b = new StringBuilder(256); + b.append("beginBackup: ["); + for (BackupRequest req : mOriginalQueue) { + b.append(' '); + b.append(req.packageName); + } + b.append(" ]"); + addBackupTrace(b.toString()); + } + + mStatus = BackupConstants.TRANSPORT_OK; + + // Sanity check: if the queue is empty we have no work to do. + if (mOriginalQueue.isEmpty()) { + Slog.w(TAG, "Backup begun with an empty queue - nothing to do."); + addBackupTrace("queue empty at begin"); + executeNextState(BackupState.FINAL); + return; + } + + // We need to retain the original queue contents in case of transport + // failure, but we want a working copy that we can manipulate along + // the way. + mQueue = (ArrayList) mOriginalQueue.clone(); + + if (DEBUG) Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); + + File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + try { + final String transportName = mTransport.transportDirName(); + EventLog.writeEvent(EventLogTags.BACKUP_START, transportName); + + // If we haven't stored package manager metadata yet, we must init the transport. + if (mStatus == BackupConstants.TRANSPORT_OK && pmState.length() <= 0) { + Slog.i(TAG, "Initializing (wiping) backup state and transport storage"); + addBackupTrace("initializing transport " + transportName); + resetBackupState(mStateDir); // Just to make sure. + mStatus = mTransport.initializeDevice(); + + addBackupTrace("transport.initializeDevice() == " + mStatus); + if (mStatus == BackupConstants.TRANSPORT_OK) { + EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); + } else { + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + Slog.e(TAG, "Transport error in initializeDevice()"); + } + } + + // The package manager doesn't have a proper etc, but since + // it's running here in the system process we can just set up its agent + // directly and use a synthetic BackupRequest. We always run this pass + // because it's cheap and this way we guarantee that we don't get out of + // step even if we're selecting among various transports at run time. + if (mStatus == BackupConstants.TRANSPORT_OK) { + PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent( + mPackageManager, allAgentPackages()); + mStatus = invokeAgentForBackup(PACKAGE_MANAGER_SENTINEL, + IBackupAgent.Stub.asInterface(pmAgent.onBind()), mTransport); + addBackupTrace("PMBA invoke: " + mStatus); + } + + if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { + // The backend reports that our dataset has been wiped. Note this in + // the event log; the no-success code below will reset the backup + // state as well. + EventLog.writeEvent(EventLogTags.BACKUP_RESET, mTransport.transportDirName()); + } + } catch (Exception e) { + Slog.e(TAG, "Error in backup thread", e); + addBackupTrace("Exception in backup thread: " + e); + mStatus = BackupConstants.TRANSPORT_ERROR; + } finally { + // If we've succeeded so far, invokeAgentForBackup() will have run the PM + // metadata and its completion/timeout callback will continue the state + // machine chain. If it failed that won't happen; we handle that now. + addBackupTrace("exiting prelim: " + mStatus); + if (mStatus != BackupConstants.TRANSPORT_OK) { + // if things went wrong at this point, we need to + // restage everything and try again later. + resetBackupState(mStateDir); // Just to make sure. + executeNextState(BackupState.FINAL); + } + } + } + + // Transport has been initialized and the PM metadata submitted successfully + // if that was warranted. Now we process the single next thing in the queue. + void invokeNextAgent() { + mStatus = BackupConstants.TRANSPORT_OK; + addBackupTrace("invoke q=" + mQueue.size()); + + // Sanity check that we have work to do. If not, skip to the end where + // we reestablish the wakelock invariants etc. + if (mQueue.isEmpty()) { + if (DEBUG) Slog.i(TAG, "queue now empty"); + executeNextState(BackupState.FINAL); + return; + } + + // pop the entry we're going to process on this step + BackupRequest request = mQueue.get(0); + mQueue.remove(0); + + Slog.d(TAG, "starting agent for backup of " + request); + addBackupTrace("launch agent for " + request.packageName); + + // Verify that the requested app exists; it might be something that + // requested a backup but was then uninstalled. The request was + // journalled and rather than tamper with the journal it's safer + // to sanity-check here. This also gives us the classname of the + // package's backup agent. + try { + mCurrentPackage = mPackageManager.getPackageInfo(request.packageName, + PackageManager.GET_SIGNATURES); + if (mCurrentPackage.applicationInfo.backupAgentName == null) { + // The manifest has changed but we had a stale backup request pending. + // This won't happen again because the app won't be requesting further + // backups. + Slog.i(TAG, "Package " + request.packageName + + " no longer supports backup; skipping"); + addBackupTrace("skipping - no agent, completion is noop"); + executeNextState(BackupState.RUNNING_QUEUE); + return; + } + + if ((mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { + // The app has been force-stopped or cleared or just installed, + // and not yet launched out of that state, so just as it won't + // receive broadcasts, we won't run it for backup. + addBackupTrace("skipping - stopped"); + executeNextState(BackupState.RUNNING_QUEUE); + return; + } + + IBackupAgent agent = null; + try { + mWakelock.setWorkSource(new WorkSource(mCurrentPackage.applicationInfo.uid)); + agent = bindToAgentSynchronous(mCurrentPackage.applicationInfo, + IApplicationThread.BACKUP_MODE_INCREMENTAL); + addBackupTrace("agent bound; a? = " + (agent != null)); + if (agent != null) { + mStatus = invokeAgentForBackup(request.packageName, agent, mTransport); + // at this point we'll either get a completion callback from the + // agent, or a timeout message on the main handler. either way, we're + // done here as long as we're successful so far. + } else { + // Timeout waiting for the agent + mStatus = BackupConstants.AGENT_ERROR; + } + } catch (SecurityException ex) { + // Try for the next one. + Slog.d(TAG, "error in bind/backup", ex); + mStatus = BackupConstants.AGENT_ERROR; + addBackupTrace("agent SE"); + } + } catch (NameNotFoundException e) { + Slog.d(TAG, "Package does not exist; skipping"); + addBackupTrace("no such package"); + mStatus = BackupConstants.AGENT_UNKNOWN; + } finally { + mWakelock.setWorkSource(null); + + // If there was an agent error, no timeout/completion handling will occur. + // That means we need to direct to the next state ourselves. + if (mStatus != BackupConstants.TRANSPORT_OK) { + BackupState nextState = BackupState.RUNNING_QUEUE; + + // An agent-level failure means we reenqueue this one agent for + // a later retry, but otherwise proceed normally. + if (mStatus == BackupConstants.AGENT_ERROR) { + if (MORE_DEBUG) Slog.i(TAG, "Agent failure for " + request.packageName + + " - restaging"); + dataChangedImpl(request.packageName); + mStatus = BackupConstants.TRANSPORT_OK; + if (mQueue.isEmpty()) nextState = BackupState.FINAL; + } else if (mStatus == BackupConstants.AGENT_UNKNOWN) { + // Failed lookup of the app, so we couldn't bring up an agent, but + // we're otherwise fine. Just drop it and go on to the next as usual. + mStatus = BackupConstants.TRANSPORT_OK; + } else { + // Transport-level failure means we reenqueue everything + revertAndEndBackup(); + nextState = BackupState.FINAL; + } + + executeNextState(nextState); + } else { + addBackupTrace("expecting completion/timeout callback"); + } + } + } + + void finalizeBackup() { + addBackupTrace("finishing"); + + // Either backup was successful, in which case we of course do not need + // this pass's journal any more; or it failed, in which case we just + // re-enqueued all of these packages in the current active journal. + // Either way, we no longer need this pass's journal. + if (mJournal != null && !mJournal.delete()) { + Slog.e(TAG, "Unable to remove backup journal file " + mJournal); + } + + // If everything actually went through and this is the first time we've + // done a backup, we can now record what the current backup dataset token + // is. + if ((mCurrentToken == 0) && (mStatus == BackupConstants.TRANSPORT_OK)) { + addBackupTrace("success; recording token"); + try { + mCurrentToken = mTransport.getCurrentRestoreSet(); + writeRestoreTokens(); + } catch (RemoteException e) { + // nothing for it at this point, unfortunately, but this will be + // recorded the next time we fully succeed. + addBackupTrace("transport threw returning token"); + } + } + + // Set up the next backup pass - at this point we can set mBackupRunning + // to false to allow another pass to fire, because we're done with the + // state machine sequence and the wakelock is refcounted. + synchronized (mQueueLock) { + mBackupRunning = false; + if (mStatus == BackupConstants.TRANSPORT_NOT_INITIALIZED) { + // Make sure we back up everything and perform the one-time init + clearMetadata(); + if (DEBUG) Slog.d(TAG, "Server requires init; rerunning"); + addBackupTrace("init required; rerunning"); + backupNow(); + } + } + + // Only once we're entirely finished do we release the wakelock + clearBackupTrace(); + Slog.i(TAG, "Backup pass finished."); + mWakelock.release(); + } + + // Remove the PM metadata state. This will generate an init on the next pass. + void clearMetadata() { + final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL); + if (pmState.exists()) pmState.delete(); + } + + // Invoke an agent's doBackup() and start a timeout message spinning on the main + // handler in case it doesn't get back to us. + int invokeAgentForBackup(String packageName, IBackupAgent agent, + IBackupTransport transport) { + if (DEBUG) Slog.d(TAG, "invokeAgentForBackup on " + packageName); + addBackupTrace("invoking " + packageName); + + mSavedStateName = new File(mStateDir, packageName); + mBackupDataName = new File(mDataDir, packageName + ".data"); + mNewStateName = new File(mStateDir, packageName + ".new"); + + mSavedState = null; + mBackupData = null; + mNewState = null; + + final int token = generateToken(); + try { + // Look up the package info & signatures. This is first so that if it + // throws an exception, there's no file setup yet that would need to + // be unraveled. + if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) { + // The metadata 'package' is synthetic; construct one and make + // sure our global state is pointed at it + mCurrentPackage = new PackageInfo(); + mCurrentPackage.packageName = packageName; + } + + // In a full backup, we pass a null ParcelFileDescriptor as + // the saved-state "file". This is by definition an incremental, + // so we build a saved state file to pass. + mSavedState = ParcelFileDescriptor.open(mSavedStateName, + ParcelFileDescriptor.MODE_READ_ONLY | + ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary + + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName); + } + + mNewState = ParcelFileDescriptor.open(mNewStateName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + // Initiate the target's backup pass + addBackupTrace("setting timeout"); + prepareOperationTimeout(token, TIMEOUT_BACKUP_INTERVAL, this); + addBackupTrace("calling agent doBackup()"); + agent.doBackup(mSavedState, mBackupData, mNewState, token, mBackupManagerBinder); + } catch (Exception e) { + Slog.e(TAG, "Error invoking for backup on " + packageName); + addBackupTrace("exception: " + e); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName, + e.toString()); + agentErrorCleanup(); + return BackupConstants.AGENT_ERROR; + } + + // At this point the agent is off and running. The next thing to happen will + // either be a callback from the agent, at which point we'll process its data + // for transport, or a timeout. Either way the next phase will happen in + // response to the TimeoutHandler interface callbacks. + addBackupTrace("invoke success"); + return BackupConstants.TRANSPORT_OK; + } + + @Override + public void operationComplete() { + // Okay, the agent successfully reported back to us. Spin the data off to the + // transport and proceed with the next stage. + if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for " + + mCurrentPackage.packageName); + mBackupHandler.removeMessages(MSG_TIMEOUT); + clearAgentState(); + addBackupTrace("operation complete"); + + ParcelFileDescriptor backupData = null; + mStatus = BackupConstants.TRANSPORT_OK; + try { + int size = (int) mBackupDataName.length(); + if (size > 0) { + if (mStatus == BackupConstants.TRANSPORT_OK) { + backupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_ONLY); + addBackupTrace("sending data to transport"); + mStatus = mTransport.performBackup(mCurrentPackage, backupData); + } + + // TODO - We call finishBackup() for each application backed up, because + // we need to know now whether it succeeded or failed. Instead, we should + // hold off on finishBackup() until the end, which implies holding off on + // renaming *all* the output state files (see below) until that happens. + + addBackupTrace("data delivered: " + mStatus); + if (mStatus == BackupConstants.TRANSPORT_OK) { + addBackupTrace("finishing op on transport"); + mStatus = mTransport.finishBackup(); + addBackupTrace("finished: " + mStatus); + } + } else { + if (DEBUG) Slog.i(TAG, "no backup data written; not calling transport"); + addBackupTrace("no data to send"); + } + + // After successful transport, delete the now-stale data + // and juggle the files so that next time we supply the agent + // with the new state file it just created. + if (mStatus == BackupConstants.TRANSPORT_OK) { + mBackupDataName.delete(); + mNewStateName.renameTo(mSavedStateName); + EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, + mCurrentPackage.packageName, size); + logBackupComplete(mCurrentPackage.packageName); + } else { + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, + mCurrentPackage.packageName); + } + } catch (Exception e) { + Slog.e(TAG, "Transport error backing up " + mCurrentPackage.packageName, e); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, + mCurrentPackage.packageName); + mStatus = BackupConstants.TRANSPORT_ERROR; + } finally { + try { if (backupData != null) backupData.close(); } catch (IOException e) {} + } + + // If we encountered an error here it's a transport-level failure. That + // means we need to halt everything and reschedule everything for next time. + final BackupState nextState; + if (mStatus != BackupConstants.TRANSPORT_OK) { + revertAndEndBackup(); + nextState = BackupState.FINAL; + } else { + // Success! Proceed with the next app if any, otherwise we're done. + nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE; + } + + executeNextState(nextState); + } + + @Override + public void handleTimeout() { + // Whoops, the current agent timed out running doBackup(). Tidy up and restage + // it for the next time we run a backup pass. + // !!! TODO: keep track of failure counts per agent, and blacklist those which + // fail repeatedly (i.e. have proved themselves to be buggy). + Slog.e(TAG, "Timeout backing up " + mCurrentPackage.packageName); + EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, mCurrentPackage.packageName, + "timeout"); + addBackupTrace("timeout of " + mCurrentPackage.packageName); + agentErrorCleanup(); + dataChangedImpl(mCurrentPackage.packageName); + } + + void revertAndEndBackup() { + if (MORE_DEBUG) Slog.i(TAG, "Reverting backup queue - restaging everything"); + addBackupTrace("transport error; reverting"); + for (BackupRequest request : mOriginalQueue) { + dataChangedImpl(request.packageName); + } + // We also want to reset the backup schedule based on whatever + // the transport suggests by way of retry/backoff time. + restartBackupAlarm(); + } + + void agentErrorCleanup() { + mBackupDataName.delete(); + mNewStateName.delete(); + clearAgentState(); + + executeNextState(mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE); + } + + // Cleanup common to both success and failure cases + void clearAgentState() { + try { if (mSavedState != null) mSavedState.close(); } catch (IOException e) {} + try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} + try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} + mSavedState = mBackupData = mNewState = null; + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + + // If this was a pseudopackage there's no associated Activity Manager state + if (mCurrentPackage.applicationInfo != null) { + addBackupTrace("unbinding " + mCurrentPackage.packageName); + try { // unbind even on timeout, just in case + mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); + } catch (RemoteException e) { /* can't happen; activity manager is local */ } + } + } + + void restartBackupAlarm() { + addBackupTrace("setting backup trigger"); + synchronized (mQueueLock) { + try { + startBackupAlarmsLocked(mTransport.requestBackupTime()); + } catch (RemoteException e) { /* cannot happen */ } + } + } + + void executeNextState(BackupState nextState) { + if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " + + this + " nextState=" + nextState); + addBackupTrace("executeNextState => " + nextState); + mCurrentState = nextState; + Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); + mBackupHandler.sendMessage(msg); + } + } + + + // ----- Full backup/restore to a file/socket ----- + + abstract class ObbServiceClient { + public IObbBackupService mObbService; + public void setObbBinder(IObbBackupService binder) { + mObbService = binder; + } + } + + class FullBackupObbConnection implements ServiceConnection { + volatile IObbBackupService mService; + + FullBackupObbConnection() { + mService = null; + } + + public void establish() { + if (DEBUG) Slog.i(TAG, "Initiating bind of OBB service on " + this); + Intent obbIntent = new Intent().setComponent(new ComponentName( + "com.android.sharedstoragebackup", + "com.android.sharedstoragebackup.ObbBackupService")); + BackupManagerService.this.mContext.bindService( + obbIntent, this, Context.BIND_AUTO_CREATE); + } + + public void tearDown() { + BackupManagerService.this.mContext.unbindService(this); + } + + public boolean backupObbs(PackageInfo pkg, OutputStream out) { + boolean success = false; + waitForConnection(); + + ParcelFileDescriptor[] pipes = null; + try { + pipes = ParcelFileDescriptor.createPipe(); + int token = generateToken(); + prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); + mService.backupObbs(pkg.packageName, pipes[1], token, mBackupManagerBinder); + routeSocketDataToOutput(pipes[0], out); + success = waitUntilOperationComplete(token); + } catch (Exception e) { + Slog.w(TAG, "Unable to back up OBBs for " + pkg, e); + } finally { + try { + out.flush(); + if (pipes != null) { + if (pipes[0] != null) pipes[0].close(); + if (pipes[1] != null) pipes[1].close(); + } + } catch (IOException e) { + Slog.w(TAG, "I/O error closing down OBB backup", e); + } + } + return success; + } + + public void restoreObbFile(String pkgName, ParcelFileDescriptor data, + long fileSize, int type, String path, long mode, long mtime, + int token, IBackupManager callbackBinder) { + waitForConnection(); + + try { + mService.restoreObbFile(pkgName, data, fileSize, type, path, mode, mtime, + token, callbackBinder); + } catch (Exception e) { + Slog.w(TAG, "Unable to restore OBBs for " + pkgName, e); + } + } + + private void waitForConnection() { + synchronized (this) { + while (mService == null) { + if (DEBUG) Slog.i(TAG, "...waiting for OBB service binding..."); + try { + this.wait(); + } catch (InterruptedException e) { /* never interrupted */ } + } + if (DEBUG) Slog.i(TAG, "Connected to OBB service; continuing"); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (this) { + mService = IObbBackupService.Stub.asInterface(service); + if (DEBUG) Slog.i(TAG, "OBB service connection " + mService + + " connected on " + this); + this.notifyAll(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (this) { + mService = null; + if (DEBUG) Slog.i(TAG, "OBB service connection disconnected on " + this); + this.notifyAll(); + } + } + + } + + private void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out) + throws IOException { + FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor()); + DataInputStream in = new DataInputStream(raw); + + byte[] buffer = new byte[32 * 1024]; + int chunkTotal; + while ((chunkTotal = in.readInt()) > 0) { + while (chunkTotal > 0) { + int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal; + int nRead = in.read(buffer, 0, toRead); + out.write(buffer, 0, nRead); + chunkTotal -= nRead; + } + } + } + + class PerformFullBackupTask extends ObbServiceClient implements Runnable { + ParcelFileDescriptor mOutputFile; + DeflaterOutputStream mDeflater; + IFullBackupRestoreObserver mObserver; + boolean mIncludeApks; + boolean mIncludeObbs; + boolean mIncludeShared; + boolean mAllApps; + final boolean mIncludeSystem; + String[] mPackages; + String mCurrentPassword; + String mEncryptPassword; + AtomicBoolean mLatchObject; + File mFilesDir; + File mManifestFile; + + + class FullBackupRunner implements Runnable { + PackageInfo mPackage; + IBackupAgent mAgent; + ParcelFileDescriptor mPipe; + int mToken; + boolean mSendApk; + boolean mWriteManifest; + + FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe, + int token, boolean sendApk, boolean writeManifest) throws IOException { + mPackage = pack; + mAgent = agent; + mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor()); + mToken = token; + mSendApk = sendApk; + mWriteManifest = writeManifest; + } + + @Override + public void run() { + try { + BackupDataOutput output = new BackupDataOutput( + mPipe.getFileDescriptor()); + + if (mWriteManifest) { + if (MORE_DEBUG) Slog.d(TAG, "Writing manifest for " + mPackage.packageName); + writeAppManifest(mPackage, mManifestFile, mSendApk); + FullBackup.backupToTar(mPackage.packageName, null, null, + mFilesDir.getAbsolutePath(), + mManifestFile.getAbsolutePath(), + output); + } + + if (mSendApk) { + writeApkToBackup(mPackage, output); + } + + if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName); + prepareOperationTimeout(mToken, TIMEOUT_FULL_BACKUP_INTERVAL, null); + mAgent.doFullBackup(mPipe, mToken, mBackupManagerBinder); + } catch (IOException e) { + Slog.e(TAG, "Error running full backup for " + mPackage.packageName); + } catch (RemoteException e) { + Slog.e(TAG, "Remote agent vanished during full backup of " + + mPackage.packageName); + } finally { + try { + mPipe.close(); + } catch (IOException e) {} + } + } + } + + PerformFullBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer, + boolean includeApks, boolean includeObbs, boolean includeShared, + String curPassword, String encryptPassword, boolean doAllApps, + boolean doSystem, String[] packages, AtomicBoolean latch) { + mOutputFile = fd; + mObserver = observer; + mIncludeApks = includeApks; + mIncludeObbs = includeObbs; + mIncludeShared = includeShared; + mAllApps = doAllApps; + mIncludeSystem = doSystem; + mPackages = packages; + mCurrentPassword = curPassword; + // when backing up, if there is a current backup password, we require that + // the user use a nonempty encryption password as well. if one is supplied + // in the UI we use that, but if the UI was left empty we fall back to the + // current backup password (which was supplied by the user as well). + if (encryptPassword == null || "".equals(encryptPassword)) { + mEncryptPassword = curPassword; + } else { + mEncryptPassword = encryptPassword; + } + mLatchObject = latch; + + mFilesDir = new File("/data/system"); + mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME); + } + + @Override + public void run() { + Slog.i(TAG, "--- Performing full-dataset backup ---"); + + List packagesToBackup = new ArrayList(); + FullBackupObbConnection obbConnection = new FullBackupObbConnection(); + obbConnection.establish(); // we'll want this later + + sendStartBackup(); + + // doAllApps supersedes the package set if any + if (mAllApps) { + packagesToBackup = mPackageManager.getInstalledPackages( + PackageManager.GET_SIGNATURES); + // Exclude system apps if we've been asked to do so + if (mIncludeSystem == false) { + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + packagesToBackup.remove(i); + } else { + i++; + } + } + } + } + + // Now process the command line argument packages, if any. Note that explicitly- + // named system-partition packages will be included even if includeSystem was + // set to false. + if (mPackages != null) { + for (String pkgName : mPackages) { + try { + packagesToBackup.add(mPackageManager.getPackageInfo(pkgName, + PackageManager.GET_SIGNATURES)); + } catch (NameNotFoundException e) { + Slog.w(TAG, "Unknown package " + pkgName + ", skipping"); + } + } + } + + // Cull any packages that have indicated that backups are not permitted, as well + // as any explicit mention of the 'special' shared-storage agent package (we + // handle that one at the end). + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0 + || pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE)) { + packagesToBackup.remove(i); + } else { + i++; + } + } + + // Cull any packages that run as system-domain uids but do not define their + // own backup agents + for (int i = 0; i < packagesToBackup.size(); ) { + PackageInfo pkg = packagesToBackup.get(i); + if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) + && (pkg.applicationInfo.backupAgentName == null)) { + if (MORE_DEBUG) { + Slog.i(TAG, "... ignoring non-agent system package " + pkg.packageName); + } + packagesToBackup.remove(i); + } else { + i++; + } + } + + FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor()); + OutputStream out = null; + + PackageInfo pkg = null; + try { + boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0); + boolean compressing = COMPRESS_FULL_BACKUPS; + OutputStream finalOutput = ofstream; + + // Verify that the given password matches the currently-active + // backup password, if any + if (hasBackupPassword()) { + if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; + } + } + + // Write the global file header. All strings are UTF-8 encoded; lines end + // with a '\n' byte. Actual backup data begins immediately following the + // final '\n'. + // + // line 1: "ANDROID BACKUP" + // line 2: backup file format version, currently "1" + // line 3: compressed? "0" if not compressed, "1" if compressed. + // line 4: name of encryption algorithm [currently only "none" or "AES-256"] + // + // When line 4 is not "none", then additional header data follows: + // + // line 5: user password salt [hex] + // line 6: master key checksum salt [hex] + // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal] + // line 8: IV of the user key [hex] + // line 9: master key blob [hex] + // IV of the master key, master key itself, master key checksum hash + // + // The master key checksum is the master key plus its checksum salt, run through + // 10k rounds of PBKDF2. This is used to verify that the user has supplied the + // correct password for decrypting the archive: the master key decrypted from + // the archive using the user-supplied password is also run through PBKDF2 in + // this way, and if the result does not match the checksum as stored in the + // archive, then we know that the user-supplied password does not match the + // archive's. + StringBuilder headerbuf = new StringBuilder(1024); + + headerbuf.append(BACKUP_FILE_HEADER_MAGIC); + headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n + headerbuf.append(compressing ? "\n1\n" : "\n0\n"); + + try { + // Set up the encryption stage if appropriate, and emit the correct header + if (encrypting) { + finalOutput = emitAesBackupHeader(headerbuf, finalOutput); + } else { + headerbuf.append("none\n"); + } + + byte[] header = headerbuf.toString().getBytes("UTF-8"); + ofstream.write(header); + + // Set up the compression stage feeding into the encryption stage (if any) + if (compressing) { + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + finalOutput = new DeflaterOutputStream(finalOutput, deflater, true); + } + + out = finalOutput; + } catch (Exception e) { + // Should never happen! + Slog.e(TAG, "Unable to emit archive header", e); + return; + } + + // Shared storage if requested + if (mIncludeShared) { + try { + pkg = mPackageManager.getPackageInfo(SHARED_BACKUP_AGENT_PACKAGE, 0); + packagesToBackup.add(pkg); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Unable to find shared-storage backup handler"); + } + } + + // Now back up the app data via the agent mechanism + int N = packagesToBackup.size(); + for (int i = 0; i < N; i++) { + pkg = packagesToBackup.get(i); + backupOnePackage(pkg, out); + + // after the app's agent runs to handle its private filesystem + // contents, back up any OBB content it has on its behalf. + if (mIncludeObbs) { + boolean obbOkay = obbConnection.backupObbs(pkg, out); + if (!obbOkay) { + throw new RuntimeException("Failure writing OBB stack for " + pkg); + } + } + } + + // Done! + finalizeBackup(out); + } catch (RemoteException e) { + Slog.e(TAG, "App died during full backup"); + } catch (Exception e) { + Slog.e(TAG, "Internal exception during full backup", e); + } finally { + tearDown(pkg); + try { + if (out != null) out.close(); + mOutputFile.close(); + } catch (IOException e) { + /* nothing we can do about this */ + } + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + synchronized (mLatchObject) { + mLatchObject.set(true); + mLatchObject.notifyAll(); + } + sendEndBackup(); + obbConnection.tearDown(); + if (DEBUG) Slog.d(TAG, "Full backup pass complete."); + mWakelock.release(); + } + } + + private OutputStream emitAesBackupHeader(StringBuilder headerbuf, + OutputStream ofstream) throws Exception { + // User key will be used to encrypt the master key. + byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE); + SecretKey userKey = buildPasswordKey(mEncryptPassword, newUserSalt, + PBKDF2_HASH_ROUNDS); + + // the master key is random for each backup + byte[] masterPw = new byte[256 / 8]; + mRng.nextBytes(masterPw); + byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE); + + // primary encryption of the datastream with the random key + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES"); + c.init(Cipher.ENCRYPT_MODE, masterKeySpec); + OutputStream finalOutput = new CipherOutputStream(ofstream, c); + + // line 4: name of encryption algorithm + headerbuf.append(ENCRYPTION_ALGORITHM_NAME); + headerbuf.append('\n'); + // line 5: user password salt [hex] + headerbuf.append(byteArrayToHex(newUserSalt)); + headerbuf.append('\n'); + // line 6: master key checksum salt [hex] + headerbuf.append(byteArrayToHex(checksumSalt)); + headerbuf.append('\n'); + // line 7: number of PBKDF2 rounds used [decimal] + headerbuf.append(PBKDF2_HASH_ROUNDS); + headerbuf.append('\n'); + + // line 8: IV of the user key [hex] + Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding"); + mkC.init(Cipher.ENCRYPT_MODE, userKey); + + byte[] IV = mkC.getIV(); + headerbuf.append(byteArrayToHex(IV)); + headerbuf.append('\n'); + + // line 9: master IV + key blob, encrypted by the user key [hex]. Blob format: + // [byte] IV length = Niv + // [array of Niv bytes] IV itself + // [byte] master key length = Nmk + // [array of Nmk bytes] master key itself + // [byte] MK checksum hash length = Nck + // [array of Nck bytes] master key checksum hash + // + // The checksum is the (master key + checksum salt), run through the + // stated number of PBKDF2 rounds + IV = c.getIV(); + byte[] mk = masterKeySpec.getEncoded(); + byte[] checksum = makeKeyChecksum(masterKeySpec.getEncoded(), + checksumSalt, PBKDF2_HASH_ROUNDS); + + ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length + + checksum.length + 3); + DataOutputStream mkOut = new DataOutputStream(blob); + mkOut.writeByte(IV.length); + mkOut.write(IV); + mkOut.writeByte(mk.length); + mkOut.write(mk); + mkOut.writeByte(checksum.length); + mkOut.write(checksum); + mkOut.flush(); + byte[] encryptedMk = mkC.doFinal(blob.toByteArray()); + headerbuf.append(byteArrayToHex(encryptedMk)); + headerbuf.append('\n'); + + return finalOutput; + } + + private void backupOnePackage(PackageInfo pkg, OutputStream out) + throws RemoteException { + Slog.d(TAG, "Binding to full backup agent : " + pkg.packageName); + + IBackupAgent agent = bindToAgentSynchronous(pkg.applicationInfo, + IApplicationThread.BACKUP_MODE_FULL); + if (agent != null) { + ParcelFileDescriptor[] pipes = null; + try { + pipes = ParcelFileDescriptor.createPipe(); + + ApplicationInfo app = pkg.applicationInfo; + final boolean isSharedStorage = pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE); + final boolean sendApk = mIncludeApks + && !isSharedStorage + && ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) == 0) + && ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 || + (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); + + sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName); + + final int token = generateToken(); + FullBackupRunner runner = new FullBackupRunner(pkg, agent, pipes[1], + token, sendApk, !isSharedStorage); + pipes[1].close(); // the runner has dup'd it + pipes[1] = null; + Thread t = new Thread(runner); + t.start(); + + // Now pull data from the app and stuff it into the compressor + try { + routeSocketDataToOutput(pipes[0], out); + } catch (IOException e) { + Slog.i(TAG, "Caught exception reading from agent", e); + } + + if (!waitUntilOperationComplete(token)) { + Slog.e(TAG, "Full backup failed on package " + pkg.packageName); + } else { + if (DEBUG) Slog.d(TAG, "Full package backup success: " + pkg.packageName); + } + + } catch (IOException e) { + Slog.e(TAG, "Error backing up " + pkg.packageName, e); + } finally { + try { + // flush after every package + out.flush(); + if (pipes != null) { + if (pipes[0] != null) pipes[0].close(); + if (pipes[1] != null) pipes[1].close(); + } + } catch (IOException e) { + Slog.w(TAG, "Error bringing down backup stack"); + } + } + } else { + Slog.w(TAG, "Unable to bind to full agent for " + pkg.packageName); + } + tearDown(pkg); + } + + private void writeApkToBackup(PackageInfo pkg, BackupDataOutput output) { + // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here + final String appSourceDir = pkg.applicationInfo.sourceDir; + final String apkDir = new File(appSourceDir).getParent(); + FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null, + apkDir, appSourceDir, output); + + // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM + // doesn't have access to external storage. + + // Save associated .obb content if it exists and we did save the apk + // check for .obb and save those too + final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_OWNER); + final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0]; + if (obbDir != null) { + if (MORE_DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); + File[] obbFiles = obbDir.listFiles(); + if (obbFiles != null) { + final String obbDirName = obbDir.getAbsolutePath(); + for (File obb : obbFiles) { + FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null, + obbDirName, obb.getAbsolutePath(), output); + } + } + } + } + + private void finalizeBackup(OutputStream out) { + try { + // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes. + byte[] eof = new byte[512 * 2]; // newly allocated == zero filled + out.write(eof); + } catch (IOException e) { + Slog.w(TAG, "Error attempting to finalize backup stream"); + } + } + + private void writeAppManifest(PackageInfo pkg, File manifestFile, boolean withApk) + throws IOException { + // Manifest format. All data are strings ending in LF: + // BACKUP_MANIFEST_VERSION, currently 1 + // + // Version 1: + // package name + // package's versionCode + // platform versionCode + // getInstallerPackageName() for this package (maybe empty) + // boolean: "1" if archive includes .apk; any other string means not + // number of signatures == N + // N*: signature byte array in ascii format per Signature.toCharsString() + StringBuilder builder = new StringBuilder(4096); + StringBuilderPrinter printer = new StringBuilderPrinter(builder); + + printer.println(Integer.toString(BACKUP_MANIFEST_VERSION)); + printer.println(pkg.packageName); + printer.println(Integer.toString(pkg.versionCode)); + printer.println(Integer.toString(Build.VERSION.SDK_INT)); + + String installerName = mPackageManager.getInstallerPackageName(pkg.packageName); + printer.println((installerName != null) ? installerName : ""); + + printer.println(withApk ? "1" : "0"); + if (pkg.signatures == null) { + printer.println("0"); + } else { + printer.println(Integer.toString(pkg.signatures.length)); + for (Signature sig : pkg.signatures) { + printer.println(sig.toCharsString()); + } + } + + FileOutputStream outstream = new FileOutputStream(manifestFile); + outstream.write(builder.toString().getBytes()); + outstream.close(); + } + + private void tearDown(PackageInfo pkg) { + if (pkg != null) { + final ApplicationInfo app = pkg.applicationInfo; + if (app != null) { + try { + // unbind and tidy up even on timeout or failure, just in case + mActivityManager.unbindBackupAgent(app); + + // The agent was running with a stub Application object, so shut it down. + if (app.uid != Process.SYSTEM_UID + && app.uid != Process.PHONE_UID) { + if (MORE_DEBUG) Slog.d(TAG, "Backup complete, killing host process"); + mActivityManager.killApplicationProcess(app.processName, app.uid); + } else { + if (MORE_DEBUG) Slog.d(TAG, "Not killing after restore: " + app.processName); + } + } catch (RemoteException e) { + Slog.d(TAG, "Lost app trying to shut down"); + } + } + } + } + + // wrappers for observer use + void sendStartBackup() { + if (mObserver != null) { + try { + mObserver.onStartBackup(); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: startBackup"); + mObserver = null; + } + } + } + + void sendOnBackupPackage(String name) { + if (mObserver != null) { + try { + // TODO: use a more user-friendly name string + mObserver.onBackupPackage(name); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: backupPackage"); + mObserver = null; + } + } + } + + void sendEndBackup() { + if (mObserver != null) { + try { + mObserver.onEndBackup(); + } catch (RemoteException e) { + Slog.w(TAG, "full backup observer went away: endBackup"); + mObserver = null; + } + } + } + } + + + // ----- Full restore from a file/socket ----- + + // Description of a file in the restore datastream + static class FileMetadata { + String packageName; // name of the owning app + String installerPackageName; // name of the market-type app that installed the owner + int type; // e.g. BackupAgent.TYPE_DIRECTORY + String domain; // e.g. FullBackup.DATABASE_TREE_TOKEN + String path; // subpath within the semantic domain + long mode; // e.g. 0666 (actually int) + long mtime; // last mod time, UTC time_t (actually int) + long size; // bytes of content + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FileMetadata{"); + sb.append(packageName); sb.append(','); + sb.append(type); sb.append(','); + sb.append(domain); sb.append(':'); sb.append(path); sb.append(','); + sb.append(size); + sb.append('}'); + return sb.toString(); + } + } + + enum RestorePolicy { + IGNORE, + ACCEPT, + ACCEPT_IF_APK + } + + class PerformFullRestoreTask extends ObbServiceClient implements Runnable { + ParcelFileDescriptor mInputFile; + String mCurrentPassword; + String mDecryptPassword; + IFullBackupRestoreObserver mObserver; + AtomicBoolean mLatchObject; + IBackupAgent mAgent; + String mAgentPackage; + ApplicationInfo mTargetApp; + FullBackupObbConnection mObbConnection = null; + ParcelFileDescriptor[] mPipes = null; + + long mBytes; + + // possible handling states for a given package in the restore dataset + final HashMap mPackagePolicies + = new HashMap(); + + // installer package names for each encountered app, derived from the manifests + final HashMap mPackageInstallers = new HashMap(); + + // Signatures for a given package found in its manifest file + final HashMap mManifestSignatures + = new HashMap(); + + // Packages we've already wiped data on when restoring their first file + final HashSet mClearedPackages = new HashSet(); + + PerformFullRestoreTask(ParcelFileDescriptor fd, String curPassword, String decryptPassword, + IFullBackupRestoreObserver observer, AtomicBoolean latch) { + mInputFile = fd; + mCurrentPassword = curPassword; + mDecryptPassword = decryptPassword; + mObserver = observer; + mLatchObject = latch; + mAgent = null; + mAgentPackage = null; + mTargetApp = null; + mObbConnection = new FullBackupObbConnection(); + + // Which packages we've already wiped data on. We prepopulate this + // with a whitelist of packages known to be unclearable. + mClearedPackages.add("android"); + mClearedPackages.add("com.android.providers.settings"); + + } + + class RestoreFileRunnable implements Runnable { + IBackupAgent mAgent; + FileMetadata mInfo; + ParcelFileDescriptor mSocket; + int mToken; + + RestoreFileRunnable(IBackupAgent agent, FileMetadata info, + ParcelFileDescriptor socket, int token) throws IOException { + mAgent = agent; + mInfo = info; + mToken = token; + + // This class is used strictly for process-local binder invocations. The + // semantics of ParcelFileDescriptor differ in this case; in particular, we + // do not automatically get a 'dup'ed descriptor that we can can continue + // to use asynchronously from the caller. So, we make sure to dup it ourselves + // before proceeding to do the restore. + mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor()); + } + + @Override + public void run() { + try { + mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type, + mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime, + mToken, mBackupManagerBinder); + } catch (RemoteException e) { + // never happens; this is used strictly for local binder calls + } + } + } + + @Override + public void run() { + Slog.i(TAG, "--- Performing full-dataset restore ---"); + mObbConnection.establish(); + sendStartRestore(); + + // Are we able to restore shared-storage data? + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + mPackagePolicies.put(SHARED_BACKUP_AGENT_PACKAGE, RestorePolicy.ACCEPT); + } + + FileInputStream rawInStream = null; + DataInputStream rawDataIn = null; + try { + if (hasBackupPassword()) { + if (!passwordMatchesSaved(mCurrentPassword, PBKDF2_HASH_ROUNDS)) { + if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting"); + return; + } + } + + mBytes = 0; + byte[] buffer = new byte[32 * 1024]; + rawInStream = new FileInputStream(mInputFile.getFileDescriptor()); + rawDataIn = new DataInputStream(rawInStream); + + // First, parse out the unencrypted/uncompressed header + boolean compressed = false; + InputStream preCompressStream = rawInStream; + final InputStream in; + + boolean okay = false; + final int headerLen = BACKUP_FILE_HEADER_MAGIC.length(); + byte[] streamHeader = new byte[headerLen]; + rawDataIn.readFully(streamHeader); + byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8"); + if (Arrays.equals(magicBytes, streamHeader)) { + // okay, header looks good. now parse out the rest of the fields. + String s = readHeaderLine(rawInStream); + if (Integer.parseInt(s) == BACKUP_FILE_VERSION) { + // okay, it's a version we recognize + s = readHeaderLine(rawInStream); + compressed = (Integer.parseInt(s) != 0); + s = readHeaderLine(rawInStream); + if (s.equals("none")) { + // no more header to parse; we're good to go + okay = true; + } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { + preCompressStream = decodeAesHeaderAndInitialize(s, rawInStream); + if (preCompressStream != null) { + okay = true; + } + } else Slog.w(TAG, "Archive is encrypted but no password given"); + } else Slog.w(TAG, "Wrong header version: " + s); + } else Slog.w(TAG, "Didn't read the right header magic"); + + if (!okay) { + Slog.w(TAG, "Invalid restore data; aborting."); + return; + } + + // okay, use the right stream layer based on compression + in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream; + + boolean didRestore; + do { + didRestore = restoreOneFile(in, buffer); + } while (didRestore); + + if (MORE_DEBUG) Slog.v(TAG, "Done consuming input tarfile, total bytes=" + mBytes); + } catch (IOException e) { + Slog.e(TAG, "Unable to read restore input"); + } finally { + tearDownPipes(); + tearDownAgent(mTargetApp); + + try { + if (rawDataIn != null) rawDataIn.close(); + if (rawInStream != null) rawInStream.close(); + mInputFile.close(); + } catch (IOException e) { + Slog.w(TAG, "Close of restore data pipe threw", e); + /* nothing we can do about this */ + } + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + synchronized (mLatchObject) { + mLatchObject.set(true); + mLatchObject.notifyAll(); + } + mObbConnection.tearDown(); + sendEndRestore(); + Slog.d(TAG, "Full restore pass complete."); + mWakelock.release(); + } + } + + String readHeaderLine(InputStream in) throws IOException { + int c; + StringBuilder buffer = new StringBuilder(80); + while ((c = in.read()) >= 0) { + if (c == '\n') break; // consume and discard the newlines + buffer.append((char)c); + } + return buffer.toString(); + } + + InputStream decodeAesHeaderAndInitialize(String encryptionName, InputStream rawInStream) { + InputStream result = null; + try { + if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) { + + String userSaltHex = readHeaderLine(rawInStream); // 5 + byte[] userSalt = hexToByteArray(userSaltHex); + + String ckSaltHex = readHeaderLine(rawInStream); // 6 + byte[] ckSalt = hexToByteArray(ckSaltHex); + + int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7 + String userIvHex = readHeaderLine(rawInStream); // 8 + + String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 + + // decrypt the master key blob + Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKey userKey = buildPasswordKey(mDecryptPassword, userSalt, + rounds); + byte[] IV = hexToByteArray(userIvHex); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(userKey.getEncoded(), "AES"), + ivSpec); + byte[] mkCipher = hexToByteArray(masterKeyBlobHex); + byte[] mkBlob = c.doFinal(mkCipher); + + // first, the master key IV + int offset = 0; + int len = mkBlob[offset++]; + IV = Arrays.copyOfRange(mkBlob, offset, offset + len); + offset += len; + // then the master key itself + len = mkBlob[offset++]; + byte[] mk = Arrays.copyOfRange(mkBlob, + offset, offset + len); + offset += len; + // and finally the master key checksum hash + len = mkBlob[offset++]; + byte[] mkChecksum = Arrays.copyOfRange(mkBlob, + offset, offset + len); + + // now validate the decrypted master key against the checksum + byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds); + if (Arrays.equals(calculatedCk, mkChecksum)) { + ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, + new SecretKeySpec(mk, "AES"), + ivSpec); + // Only if all of the above worked properly will 'result' be assigned + result = new CipherInputStream(rawInStream, c); + } else Slog.w(TAG, "Incorrect password"); + } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName); + } catch (InvalidAlgorithmParameterException e) { + Slog.e(TAG, "Needed parameter spec unavailable!", e); + } catch (BadPaddingException e) { + // This case frequently occurs when the wrong password is used to decrypt + // the master key. Use the identical "incorrect password" log text as is + // used in the checksum failure log in order to avoid providing additional + // information to an attacker. + Slog.w(TAG, "Incorrect password"); + } catch (IllegalBlockSizeException e) { + Slog.w(TAG, "Invalid block size in master key"); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "Needed decryption algorithm unavailable!"); + } catch (NoSuchPaddingException e) { + Slog.e(TAG, "Needed padding mechanism unavailable!"); + } catch (InvalidKeyException e) { + Slog.w(TAG, "Illegal password; aborting"); + } catch (NumberFormatException e) { + Slog.w(TAG, "Can't parse restore data header"); + } catch (IOException e) { + Slog.w(TAG, "Can't read input header"); + } + + return result; + } + + boolean restoreOneFile(InputStream instream, byte[] buffer) { + FileMetadata info; + try { + info = readTarHeaders(instream); + if (info != null) { + if (MORE_DEBUG) { + dumpFileMetadata(info); + } + + final String pkg = info.packageName; + if (!pkg.equals(mAgentPackage)) { + // okay, change in package; set up our various + // bookkeeping if we haven't seen it yet + if (!mPackagePolicies.containsKey(pkg)) { + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + + // Clean up the previous agent relationship if necessary, + // and let the observer know we're considering a new app. + if (mAgent != null) { + if (DEBUG) Slog.d(TAG, "Saw new package; tearing down old one"); + tearDownPipes(); + tearDownAgent(mTargetApp); + mTargetApp = null; + mAgentPackage = null; + } + } + + if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { + mPackagePolicies.put(pkg, readAppManifest(info, instream)); + mPackageInstallers.put(pkg, info.installerPackageName); + // We've read only the manifest content itself at this point, + // so consume the footer before looping around to the next + // input file + skipTarPadding(info.size, instream); + sendOnRestorePackage(pkg); + } else { + // Non-manifest, so it's actual file data. Is this a package + // we're ignoring? + boolean okay = true; + RestorePolicy policy = mPackagePolicies.get(pkg); + switch (policy) { + case IGNORE: + okay = false; + break; + + case ACCEPT_IF_APK: + // If we're in accept-if-apk state, then the first file we + // see MUST be the apk. + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "APK file; installing"); + // Try to install the app. + String installerName = mPackageInstallers.get(pkg); + okay = installApk(info, installerName, instream); + // good to go; promote to ACCEPT + mPackagePolicies.put(pkg, (okay) + ? RestorePolicy.ACCEPT + : RestorePolicy.IGNORE); + // At this point we've consumed this file entry + // ourselves, so just strip the tar footer and + // go on to the next file in the input stream + skipTarPadding(info.size, instream); + return true; + } else { + // File data before (or without) the apk. We can't + // handle it coherently in this case so ignore it. + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + okay = false; + } + break; + + case ACCEPT: + if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "apk present but ACCEPT"); + // we can take the data without the apk, so we + // *want* to do so. skip the apk by declaring this + // one file not-okay without changing the restore + // policy for the package. + okay = false; + } + break; + + default: + // Something has gone dreadfully wrong when determining + // the restore policy from the manifest. Ignore the + // rest of this package's data. + Slog.e(TAG, "Invalid policy from manifest"); + okay = false; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + break; + } + + // If the policy is satisfied, go ahead and set up to pipe the + // data to the agent. + if (DEBUG && okay && mAgent != null) { + Slog.i(TAG, "Reusing existing agent instance"); + } + if (okay && mAgent == null) { + if (DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg); + + try { + mTargetApp = mPackageManager.getApplicationInfo(pkg, 0); + + // If we haven't sent any data to this app yet, we probably + // need to clear it first. Check that. + if (!mClearedPackages.contains(pkg)) { + // apps with their own backup agents are + // responsible for coherently managing a full + // restore. + if (mTargetApp.backupAgentName == null) { + if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore"); + clearApplicationDataSynchronous(pkg); + } else { + if (DEBUG) Slog.d(TAG, "backup agent (" + + mTargetApp.backupAgentName + ") => no clear"); + } + mClearedPackages.add(pkg); + } else { + if (DEBUG) Slog.d(TAG, "We've initialized this app already; no clear required"); + } + + // All set; now set up the IPC and launch the agent + setUpPipes(); + mAgent = bindToAgentSynchronous(mTargetApp, + IApplicationThread.BACKUP_MODE_RESTORE_FULL); + mAgentPackage = pkg; + } catch (IOException e) { + // fall through to error handling + } catch (NameNotFoundException e) { + // fall through to error handling + } + + if (mAgent == null) { + if (DEBUG) Slog.d(TAG, "Unable to create agent for " + pkg); + okay = false; + tearDownPipes(); + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Sanity check: make sure we never give data to the wrong app. This + // should never happen but a little paranoia here won't go amiss. + if (okay && !pkg.equals(mAgentPackage)) { + Slog.e(TAG, "Restoring data for " + pkg + + " but agent is for " + mAgentPackage); + okay = false; + } + + // At this point we have an agent ready to handle the full + // restore data as well as a pipe for sending data to + // that agent. Tell the agent to start reading from the + // pipe. + if (okay) { + boolean agentSuccess = true; + long toCopy = info.size; + final int token = generateToken(); + try { + prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, null); + if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) { + if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg + + " : " + info.path); + mObbConnection.restoreObbFile(pkg, mPipes[0], + info.size, info.type, info.path, info.mode, + info.mtime, token, mBackupManagerBinder); + } else { + if (DEBUG) Slog.d(TAG, "Invoking agent to restore file " + + info.path); + // fire up the app's agent listening on the socket. If + // the agent is running in the system process we can't + // just invoke it asynchronously, so we provide a thread + // for it here. + if (mTargetApp.processName.equals("system")) { + Slog.d(TAG, "system process agent - spinning a thread"); + RestoreFileRunnable runner = new RestoreFileRunnable( + mAgent, info, mPipes[0], token); + new Thread(runner).start(); + } else { + mAgent.doRestoreFile(mPipes[0], info.size, info.type, + info.domain, info.path, info.mode, info.mtime, + token, mBackupManagerBinder); + } + } + } catch (IOException e) { + // couldn't dup the socket for a process-local restore + Slog.d(TAG, "Couldn't establish restore"); + agentSuccess = false; + okay = false; + } catch (RemoteException e) { + // whoops, remote entity went away. We'll eat the content + // ourselves, then, and not copy it over. + Slog.e(TAG, "Agent crashed during full restore"); + agentSuccess = false; + okay = false; + } + + // Copy over the data if the agent is still good + if (okay) { + boolean pipeOkay = true; + FileOutputStream pipe = new FileOutputStream( + mPipes[1].getFileDescriptor()); + while (toCopy > 0) { + int toRead = (toCopy > buffer.length) + ? buffer.length : (int)toCopy; + int nRead = instream.read(buffer, 0, toRead); + if (nRead >= 0) mBytes += nRead; + if (nRead <= 0) break; + toCopy -= nRead; + + // send it to the output pipe as long as things + // are still good + if (pipeOkay) { + try { + pipe.write(buffer, 0, nRead); + } catch (IOException e) { + Slog.e(TAG, "Failed to write to restore pipe", e); + pipeOkay = false; + } + } + } + + // done sending that file! Now we just need to consume + // the delta from info.size to the end of block. + skipTarPadding(info.size, instream); + + // and now that we've sent it all, wait for the remote + // side to acknowledge receipt + agentSuccess = waitUntilOperationComplete(token); + } + + // okay, if the remote end failed at any point, deal with + // it by ignoring the rest of the restore on it + if (!agentSuccess) { + mBackupHandler.removeMessages(MSG_TIMEOUT); + tearDownPipes(); + tearDownAgent(mTargetApp); + mAgent = null; + mPackagePolicies.put(pkg, RestorePolicy.IGNORE); + } + } + + // Problems setting up the agent communication, or an already- + // ignored package: skip to the next tar stream entry by + // reading and discarding this file. + if (!okay) { + if (DEBUG) Slog.d(TAG, "[discarding file content]"); + long bytesToConsume = (info.size + 511) & ~511; + while (bytesToConsume > 0) { + int toRead = (bytesToConsume > buffer.length) + ? buffer.length : (int)bytesToConsume; + long nRead = instream.read(buffer, 0, toRead); + if (nRead >= 0) mBytes += nRead; + if (nRead <= 0) break; + bytesToConsume -= nRead; + } + } + } + } + } catch (IOException e) { + if (DEBUG) Slog.w(TAG, "io exception on restore socket read", e); + // treat as EOF + info = null; + } + + return (info != null); + } + + void setUpPipes() throws IOException { + mPipes = ParcelFileDescriptor.createPipe(); + } + + void tearDownPipes() { + if (mPipes != null) { + try { + mPipes[0].close(); + mPipes[0] = null; + mPipes[1].close(); + mPipes[1] = null; + } catch (IOException e) { + Slog.w(TAG, "Couldn't close agent pipes", e); + } + mPipes = null; + } + } + + void tearDownAgent(ApplicationInfo app) { + if (mAgent != null) { + try { + // unbind and tidy up even on timeout or failure, just in case + mActivityManager.unbindBackupAgent(app); + + // The agent was running with a stub Application object, so shut it down. + // !!! We hardcode the confirmation UI's package name here rather than use a + // manifest flag! TODO something less direct. + if (app.uid != Process.SYSTEM_UID + && !app.packageName.equals("com.android.backupconfirm")) { + if (DEBUG) Slog.d(TAG, "Killing host process"); + mActivityManager.killApplicationProcess(app.processName, app.uid); + } else { + if (DEBUG) Slog.d(TAG, "Not killing after full restore"); + } + } catch (RemoteException e) { + Slog.d(TAG, "Lost app trying to shut down"); + } + mAgent = null; + } + } + + class RestoreInstallObserver extends IPackageInstallObserver.Stub { + final AtomicBoolean mDone = new AtomicBoolean(); + String mPackageName; + int mResult; + + public void reset() { + synchronized (mDone) { + mDone.set(false); + } + } + + public void waitForCompletion() { + synchronized (mDone) { + while (mDone.get() == false) { + try { + mDone.wait(); + } catch (InterruptedException e) { } + } + } + } + + int getResult() { + return mResult; + } + + @Override + public void packageInstalled(String packageName, int returnCode) + throws RemoteException { + synchronized (mDone) { + mResult = returnCode; + mPackageName = packageName; + mDone.set(true); + mDone.notifyAll(); + } + } + } + + class RestoreDeleteObserver extends IPackageDeleteObserver.Stub { + final AtomicBoolean mDone = new AtomicBoolean(); + int mResult; + + public void reset() { + synchronized (mDone) { + mDone.set(false); + } + } + + public void waitForCompletion() { + synchronized (mDone) { + while (mDone.get() == false) { + try { + mDone.wait(); + } catch (InterruptedException e) { } + } + } + } + + @Override + public void packageDeleted(String packageName, int returnCode) throws RemoteException { + synchronized (mDone) { + mResult = returnCode; + mDone.set(true); + mDone.notifyAll(); + } + } + } + + final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); + final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); + + boolean installApk(FileMetadata info, String installerPackage, InputStream instream) { + boolean okay = true; + + if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName); + + // The file content is an .apk file. Copy it out to a staging location and + // attempt to install it. + File apkFile = new File(mDataDir, info.packageName); + try { + FileOutputStream apkStream = new FileOutputStream(apkFile); + byte[] buffer = new byte[32 * 1024]; + long size = info.size; + while (size > 0) { + long toRead = (buffer.length < size) ? buffer.length : size; + int didRead = instream.read(buffer, 0, (int)toRead); + if (didRead >= 0) mBytes += didRead; + apkStream.write(buffer, 0, didRead); + size -= didRead; + } + apkStream.close(); + + // make sure the installer can read it + apkFile.setReadable(true, false); + + // Now install it + Uri packageUri = Uri.fromFile(apkFile); + mInstallObserver.reset(); + mPackageManager.installPackage(packageUri, mInstallObserver, + PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB, + installerPackage); + mInstallObserver.waitForCompletion(); + + if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) { + // The only time we continue to accept install of data even if the + // apk install failed is if we had already determined that we could + // accept the data regardless. + if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) { + okay = false; + } + } else { + // Okay, the install succeeded. Make sure it was the right app. + boolean uninstall = false; + if (!mInstallObserver.mPackageName.equals(info.packageName)) { + Slog.w(TAG, "Restore stream claimed to include apk for " + + info.packageName + " but apk was really " + + mInstallObserver.mPackageName); + // delete the package we just put in place; it might be fraudulent + okay = false; + uninstall = true; + } else { + try { + PackageInfo pkg = mPackageManager.getPackageInfo(info.packageName, + PackageManager.GET_SIGNATURES); + if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) { + Slog.w(TAG, "Restore stream contains apk of package " + + info.packageName + " but it disallows backup/restore"); + okay = false; + } else { + // So far so good -- do the signatures match the manifest? + Signature[] sigs = mManifestSignatures.get(info.packageName); + if (signaturesMatch(sigs, pkg)) { + // If this is a system-uid app without a declared backup agent, + // don't restore any of the file data. + if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID) + && (pkg.applicationInfo.backupAgentName == null)) { + Slog.w(TAG, "Installed app " + info.packageName + + " has restricted uid and no agent"); + okay = false; + } + } else { + Slog.w(TAG, "Installed app " + info.packageName + + " signatures do not match restore manifest"); + okay = false; + uninstall = true; + } + } + } catch (NameNotFoundException e) { + Slog.w(TAG, "Install of package " + info.packageName + + " succeeded but now not found"); + okay = false; + } + } + + // If we're not okay at this point, we need to delete the package + // that we just installed. + if (uninstall) { + mDeleteObserver.reset(); + mPackageManager.deletePackage(mInstallObserver.mPackageName, + mDeleteObserver, 0); + mDeleteObserver.waitForCompletion(); + } + } + } catch (IOException e) { + Slog.e(TAG, "Unable to transcribe restored apk for install"); + okay = false; + } finally { + apkFile.delete(); + } + + return okay; + } + + // Given an actual file content size, consume the post-content padding mandated + // by the tar format. + void skipTarPadding(long size, InputStream instream) throws IOException { + long partial = (size + 512) % 512; + if (partial > 0) { + final int needed = 512 - (int)partial; + byte[] buffer = new byte[needed]; + if (readExactly(instream, buffer, 0, needed) == needed) { + mBytes += needed; + } else throw new IOException("Unexpected EOF in padding"); + } + } + + // Returns a policy constant; takes a buffer arg to reduce memory churn + RestorePolicy readAppManifest(FileMetadata info, InputStream instream) + throws IOException { + // Fail on suspiciously large manifest files + if (info.size > 64 * 1024) { + throw new IOException("Restore manifest too big; corrupt? size=" + info.size); + } + + byte[] buffer = new byte[(int) info.size]; + if (readExactly(instream, buffer, 0, (int)info.size) == info.size) { + mBytes += info.size; + } else throw new IOException("Unexpected EOF in manifest"); + + RestorePolicy policy = RestorePolicy.IGNORE; + String[] str = new String[1]; + int offset = 0; + + try { + offset = extractLine(buffer, offset, str); + int version = Integer.parseInt(str[0]); + if (version == BACKUP_MANIFEST_VERSION) { + offset = extractLine(buffer, offset, str); + String manifestPackage = str[0]; + // TODO: handle + if (manifestPackage.equals(info.packageName)) { + offset = extractLine(buffer, offset, str); + version = Integer.parseInt(str[0]); // app version + offset = extractLine(buffer, offset, str); + int platformVersion = Integer.parseInt(str[0]); + offset = extractLine(buffer, offset, str); + info.installerPackageName = (str[0].length() > 0) ? str[0] : null; + offset = extractLine(buffer, offset, str); + boolean hasApk = str[0].equals("1"); + offset = extractLine(buffer, offset, str); + int numSigs = Integer.parseInt(str[0]); + if (numSigs > 0) { + Signature[] sigs = new Signature[numSigs]; + for (int i = 0; i < numSigs; i++) { + offset = extractLine(buffer, offset, str); + sigs[i] = new Signature(str[0]); + } + mManifestSignatures.put(info.packageName, sigs); + + // Okay, got the manifest info we need... + try { + PackageInfo pkgInfo = mPackageManager.getPackageInfo( + info.packageName, PackageManager.GET_SIGNATURES); + // Fall through to IGNORE if the app explicitly disallows backup + final int flags = pkgInfo.applicationInfo.flags; + if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) { + // Restore system-uid-space packages only if they have + // defined a custom backup agent + if ((pkgInfo.applicationInfo.uid >= Process.FIRST_APPLICATION_UID) + || (pkgInfo.applicationInfo.backupAgentName != null)) { + // Verify signatures against any installed version; if they + // don't match, then we fall though and ignore the data. The + // signatureMatch() method explicitly ignores the signature + // check for packages installed on the system partition, because + // such packages are signed with the platform cert instead of + // the app developer's cert, so they're different on every + // device. + if (signaturesMatch(sigs, pkgInfo)) { + if (pkgInfo.versionCode >= version) { + Slog.i(TAG, "Sig + version match; taking data"); + policy = RestorePolicy.ACCEPT; + } else { + // The data is from a newer version of the app than + // is presently installed. That means we can only + // use it if the matching apk is also supplied. + Slog.d(TAG, "Data version " + version + + " is newer than installed version " + + pkgInfo.versionCode + " - requiring apk"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + } else { + Slog.w(TAG, "Restore manifest signatures do not match " + + "installed application for " + info.packageName); + } + } else { + Slog.w(TAG, "Package " + info.packageName + + " is system level with no agent"); + } + } else { + if (DEBUG) Slog.i(TAG, "Restore manifest from " + + info.packageName + " but allowBackup=false"); + } + } catch (NameNotFoundException e) { + // Okay, the target app isn't installed. We can process + // the restore properly only if the dataset provides the + // apk file and we can successfully install it. + if (DEBUG) Slog.i(TAG, "Package " + info.packageName + + " not installed; requiring apk in dataset"); + policy = RestorePolicy.ACCEPT_IF_APK; + } + + if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) { + Slog.i(TAG, "Cannot restore package " + info.packageName + + " without the matching .apk"); + } + } else { + Slog.i(TAG, "Missing signature on backed-up package " + + info.packageName); + } + } else { + Slog.i(TAG, "Expected package " + info.packageName + + " but restore manifest claims " + manifestPackage); + } + } else { + Slog.i(TAG, "Unknown restore manifest version " + version + + " for package " + info.packageName); + } + } catch (NumberFormatException e) { + Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); + } catch (IllegalArgumentException e) { + Slog.w(TAG, e.getMessage()); + } + + return policy; + } + + // Builds a line from a byte buffer starting at 'offset', and returns + // the index of the next unconsumed data in the buffer. + int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException { + final int end = buffer.length; + if (offset >= end) throw new IOException("Incomplete data"); + + int pos; + for (pos = offset; pos < end; pos++) { + byte c = buffer[pos]; + // at LF we declare end of line, and return the next char as the + // starting point for the next time through + if (c == '\n') { + break; + } + } + outStr[0] = new String(buffer, offset, pos - offset); + pos++; // may be pointing an extra byte past the end but that's okay + return pos; + } + + void dumpFileMetadata(FileMetadata info) { + if (DEBUG) { + StringBuilder b = new StringBuilder(128); + + // mode string + b.append((info.type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-'); + b.append(((info.mode & 0400) != 0) ? 'r' : '-'); + b.append(((info.mode & 0200) != 0) ? 'w' : '-'); + b.append(((info.mode & 0100) != 0) ? 'x' : '-'); + b.append(((info.mode & 0040) != 0) ? 'r' : '-'); + b.append(((info.mode & 0020) != 0) ? 'w' : '-'); + b.append(((info.mode & 0010) != 0) ? 'x' : '-'); + b.append(((info.mode & 0004) != 0) ? 'r' : '-'); + b.append(((info.mode & 0002) != 0) ? 'w' : '-'); + b.append(((info.mode & 0001) != 0) ? 'x' : '-'); + b.append(String.format(" %9d ", info.size)); + + Date stamp = new Date(info.mtime); + b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp)); + + b.append(info.packageName); + b.append(" :: "); + b.append(info.domain); + b.append(" :: "); + b.append(info.path); + + Slog.i(TAG, b.toString()); + } + } + // Consume a tar file header block [sequence] and accumulate the relevant metadata + FileMetadata readTarHeaders(InputStream instream) throws IOException { + byte[] block = new byte[512]; + FileMetadata info = null; + + boolean gotHeader = readTarHeader(instream, block); + if (gotHeader) { + try { + // okay, presume we're okay, and extract the various metadata + info = new FileMetadata(); + info.size = extractRadix(block, 124, 12, 8); + info.mtime = extractRadix(block, 136, 12, 8); + info.mode = extractRadix(block, 100, 8, 8); + + info.path = extractString(block, 345, 155); // prefix + String path = extractString(block, 0, 100); + if (path.length() > 0) { + if (info.path.length() > 0) info.path += '/'; + info.path += path; + } + + // tar link indicator field: 1 byte at offset 156 in the header. + int typeChar = block[156]; + if (typeChar == 'x') { + // pax extended header, so we need to read that + gotHeader = readPaxExtendedHeader(instream, info); + if (gotHeader) { + // and after a pax extended header comes another real header -- read + // that to find the real file type + gotHeader = readTarHeader(instream, block); + } + if (!gotHeader) throw new IOException("Bad or missing pax header"); + + typeChar = block[156]; + } + + switch (typeChar) { + case '0': info.type = BackupAgent.TYPE_FILE; break; + case '5': { + info.type = BackupAgent.TYPE_DIRECTORY; + if (info.size != 0) { + Slog.w(TAG, "Directory entry with nonzero size in header"); + info.size = 0; + } + break; + } + case 0: { + // presume EOF + if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info); + return null; + } + default: { + Slog.e(TAG, "Unknown tar entity type: " + typeChar); + throw new IOException("Unknown entity type " + typeChar); + } + } + + // Parse out the path + // + // first: apps/shared/unrecognized + if (FullBackup.SHARED_PREFIX.regionMatches(0, + info.path, 0, FullBackup.SHARED_PREFIX.length())) { + // File in shared storage. !!! TODO: implement this. + info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); + info.packageName = SHARED_BACKUP_AGENT_PACKAGE; + info.domain = FullBackup.SHARED_STORAGE_TOKEN; + if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path); + } else if (FullBackup.APPS_PREFIX.regionMatches(0, + info.path, 0, FullBackup.APPS_PREFIX.length())) { + // App content! Parse out the package name and domain + + // strip the apps/ prefix + info.path = info.path.substring(FullBackup.APPS_PREFIX.length()); + + // extract the package name + int slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in " + info.path); + info.packageName = info.path.substring(0, slash); + info.path = info.path.substring(slash+1); + + // if it's a manifest we're done, otherwise parse out the domains + if (!info.path.equals(BACKUP_MANIFEST_FILENAME)) { + slash = info.path.indexOf('/'); + if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path); + info.domain = info.path.substring(0, slash); + info.path = info.path.substring(slash + 1); + } + } + } catch (IOException e) { + if (DEBUG) { + Slog.e(TAG, "Parse error in header: " + e.getMessage()); + HEXLOG(block); + } + throw e; + } + } + return info; + } + + private void HEXLOG(byte[] block) { + int offset = 0; + int todo = block.length; + StringBuilder buf = new StringBuilder(64); + while (todo > 0) { + buf.append(String.format("%04x ", offset)); + int numThisLine = (todo > 16) ? 16 : todo; + for (int i = 0; i < numThisLine; i++) { + buf.append(String.format("%02x ", block[offset+i])); + } + Slog.i("hexdump", buf.toString()); + buf.setLength(0); + todo -= numThisLine; + offset += numThisLine; + } + } + + // Read exactly the given number of bytes into a buffer at the stated offset. + // Returns false if EOF is encountered before the requested number of bytes + // could be read. + int readExactly(InputStream in, byte[] buffer, int offset, int size) + throws IOException { + if (size <= 0) throw new IllegalArgumentException("size must be > 0"); + + int soFar = 0; + while (soFar < size) { + int nRead = in.read(buffer, offset + soFar, size - soFar); + if (nRead <= 0) { + if (MORE_DEBUG) Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar); + break; + } + soFar += nRead; + } + return soFar; + } + + boolean readTarHeader(InputStream instream, byte[] block) throws IOException { + final int got = readExactly(instream, block, 0, 512); + if (got == 0) return false; // Clean EOF + if (got < 512) throw new IOException("Unable to read full block header"); + mBytes += 512; + return true; + } + + // overwrites 'info' fields based on the pax extended header + boolean readPaxExtendedHeader(InputStream instream, FileMetadata info) + throws IOException { + // We should never see a pax extended header larger than this + if (info.size > 32*1024) { + Slog.w(TAG, "Suspiciously large pax header size " + info.size + + " - aborting"); + throw new IOException("Sanity failure: pax header size " + info.size); + } + + // read whole blocks, not just the content size + int numBlocks = (int)((info.size + 511) >> 9); + byte[] data = new byte[numBlocks * 512]; + if (readExactly(instream, data, 0, data.length) < data.length) { + throw new IOException("Unable to read full pax header"); + } + mBytes += data.length; + + final int contentSize = (int) info.size; + int offset = 0; + do { + // extract the line at 'offset' + int eol = offset+1; + while (eol < contentSize && data[eol] != ' ') eol++; + if (eol >= contentSize) { + // error: we just hit EOD looking for the end of the size field + throw new IOException("Invalid pax data"); + } + // eol points to the space between the count and the key + int linelen = (int) extractRadix(data, offset, eol - offset, 10); + int key = eol + 1; // start of key=value + eol = offset + linelen - 1; // trailing LF + int value; + for (value = key+1; data[value] != '=' && value <= eol; value++); + if (value > eol) { + throw new IOException("Invalid pax declaration"); + } + + // pax requires that key/value strings be in UTF-8 + String keyStr = new String(data, key, value-key, "UTF-8"); + // -1 to strip the trailing LF + String valStr = new String(data, value+1, eol-value-1, "UTF-8"); + + if ("path".equals(keyStr)) { + info.path = valStr; + } else if ("size".equals(keyStr)) { + info.size = Long.parseLong(valStr); + } else { + if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key); + } + + offset += linelen; + } while (offset < contentSize); + + return true; + } + + long extractRadix(byte[] data, int offset, int maxChars, int radix) + throws IOException { + long value = 0; + final int end = offset + maxChars; + for (int i = offset; i < end; i++) { + final byte b = data[i]; + // Numeric fields in tar can terminate with either NUL or SPC + if (b == 0 || b == ' ') break; + if (b < '0' || b > ('0' + radix - 1)) { + throw new IOException("Invalid number in header: '" + (char)b + "' for radix " + radix); + } + value = radix * value + (b - '0'); + } + return value; + } + + String extractString(byte[] data, int offset, int maxChars) throws IOException { + final int end = offset + maxChars; + int eos = offset; + // tar string fields terminate early with a NUL + while (eos < end && data[eos] != 0) eos++; + return new String(data, offset, eos-offset, "US-ASCII"); + } + + void sendStartRestore() { + if (mObserver != null) { + try { + mObserver.onStartRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: startRestore"); + mObserver = null; + } + } + } + + void sendOnRestorePackage(String name) { + if (mObserver != null) { + try { + // TODO: use a more user-friendly name string + mObserver.onRestorePackage(name); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: restorePackage"); + mObserver = null; + } + } + } + + void sendEndRestore() { + if (mObserver != null) { + try { + mObserver.onEndRestore(); + } catch (RemoteException e) { + Slog.w(TAG, "full restore observer went away: endRestore"); + mObserver = null; + } + } + } + } + + // ----- Restore handling ----- + + private boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) { + // If the target resides on the system partition, we allow it to restore + // data from the like-named package in a restore set even if the signatures + // do not match. (Unlike general applications, those flashed to the system + // partition will be signed with the device's platform certificate, so on + // different phones the same system app will have different signatures.) + if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + if (DEBUG) Slog.v(TAG, "System app " + target.packageName + " - skipping sig check"); + return true; + } + + // Allow unsigned apps, but not signed on one device and unsigned on the other + // !!! TODO: is this the right policy? + Signature[] deviceSigs = target.signatures; + if (MORE_DEBUG) Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + + " device=" + deviceSigs); + if ((storedSigs == null || storedSigs.length == 0) + && (deviceSigs == null || deviceSigs.length == 0)) { + return true; + } + if (storedSigs == null || deviceSigs == null) { + return false; + } + + // !!! TODO: this demands that every stored signature match one + // that is present on device, and does not demand the converse. + // Is this this right policy? + int nStored = storedSigs.length; + int nDevice = deviceSigs.length; + + for (int i=0; i < nStored; i++) { + boolean match = false; + for (int j=0; j < nDevice; j++) { + if (storedSigs[i].equals(deviceSigs[j])) { + match = true; + break; + } + } + if (!match) { + return false; + } + } + return true; + } + + enum RestoreState { + INITIAL, + DOWNLOAD_DATA, + PM_METADATA, + RUNNING_QUEUE, + FINAL + } + + class PerformRestoreTask implements BackupRestoreTask { + private IBackupTransport mTransport; + private IRestoreObserver mObserver; + private long mToken; + private PackageInfo mTargetPackage; + private File mStateDir; + private int mPmToken; + private boolean mNeedFullBackup; + private HashSet mFilterSet; + private long mStartRealtime; + private PackageManagerBackupAgent mPmAgent; + private List mAgentPackages; + private ArrayList mRestorePackages; + private RestoreState mCurrentState; + private int mCount; + private boolean mFinished; + private int mStatus; + private File mBackupDataName; + private File mNewStateName; + private File mSavedStateName; + private ParcelFileDescriptor mBackupData; + private ParcelFileDescriptor mNewState; + private PackageInfo mCurrentPackage; + + + class RestoreRequest { + public PackageInfo app; + public int storedAppVersion; + + RestoreRequest(PackageInfo _app, int _version) { + app = _app; + storedAppVersion = _version; + } + } + + PerformRestoreTask(IBackupTransport transport, String dirName, IRestoreObserver observer, + long restoreSetToken, PackageInfo targetPackage, int pmToken, + boolean needFullBackup, String[] filterSet) { + mCurrentState = RestoreState.INITIAL; + mFinished = false; + mPmAgent = null; + + mTransport = transport; + mObserver = observer; + mToken = restoreSetToken; + mTargetPackage = targetPackage; + mPmToken = pmToken; + mNeedFullBackup = needFullBackup; + + if (filterSet != null) { + mFilterSet = new HashSet(); + for (String pkg : filterSet) { + mFilterSet.add(pkg); + } + } else { + mFilterSet = null; + } + + mStateDir = new File(mBaseStateDir, dirName); + } + + // Execute one tick of whatever state machine the task implements + @Override + public void execute() { + if (MORE_DEBUG) Slog.v(TAG, "*** Executing restore step: " + mCurrentState); + switch (mCurrentState) { + case INITIAL: + beginRestore(); + break; + + case DOWNLOAD_DATA: + downloadRestoreData(); + break; + + case PM_METADATA: + restorePmMetadata(); + break; + + case RUNNING_QUEUE: + restoreNextAgent(); + break; + + case FINAL: + if (!mFinished) finalizeRestore(); + else { + Slog.e(TAG, "Duplicate finish"); + } + mFinished = true; + break; + } + } + + // Initialize and set up for the PM metadata restore, which comes first + void beginRestore() { + // Don't account time doing the restore as inactivity of the app + // that has opened a restore session. + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + + // Assume error until we successfully init everything + mStatus = BackupConstants.TRANSPORT_ERROR; + + try { + // TODO: Log this before getAvailableRestoreSets, somehow + EventLog.writeEvent(EventLogTags.RESTORE_START, mTransport.transportDirName(), mToken); + + // Get the list of all packages which have backup enabled. + // (Include the Package Manager metadata pseudo-package first.) + mRestorePackages = new ArrayList(); + PackageInfo omPackage = new PackageInfo(); + omPackage.packageName = PACKAGE_MANAGER_SENTINEL; + mRestorePackages.add(omPackage); + + mAgentPackages = allAgentPackages(); + if (mTargetPackage == null) { + // if there's a filter set, strip out anything that isn't + // present before proceeding + if (mFilterSet != null) { + for (int i = mAgentPackages.size() - 1; i >= 0; i--) { + final PackageInfo pkg = mAgentPackages.get(i); + if (! mFilterSet.contains(pkg.packageName)) { + mAgentPackages.remove(i); + } + } + if (MORE_DEBUG) { + Slog.i(TAG, "Post-filter package set for restore:"); + for (PackageInfo p : mAgentPackages) { + Slog.i(TAG, " " + p); + } + } + } + mRestorePackages.addAll(mAgentPackages); + } else { + // Just one package to attempt restore of + mRestorePackages.add(mTargetPackage); + } + + // let the observer know that we're running + if (mObserver != null) { + try { + // !!! TODO: get an actual count from the transport after + // its startRestore() runs? + mObserver.restoreStarting(mRestorePackages.size()); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died at restoreStarting"); + mObserver = null; + } + } + } catch (RemoteException e) { + // Something has gone catastrophically wrong with the transport + Slog.e(TAG, "Error communicating with transport for restore"); + executeNextState(RestoreState.FINAL); + return; + } + + mStatus = BackupConstants.TRANSPORT_OK; + executeNextState(RestoreState.DOWNLOAD_DATA); + } + + void downloadRestoreData() { + // Note that the download phase can be very time consuming, but we're executing + // it inline here on the looper. This is "okay" because it is not calling out to + // third party code; the transport is "trusted," and so we assume it is being a + // good citizen and timing out etc when appropriate. + // + // TODO: when appropriate, move the download off the looper and rearrange the + // error handling around that. + try { + mStatus = mTransport.startRestore(mToken, + mRestorePackages.toArray(new PackageInfo[0])); + if (mStatus != BackupConstants.TRANSPORT_OK) { + Slog.e(TAG, "Error starting restore operation"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + executeNextState(RestoreState.FINAL); + return; + } + } catch (RemoteException e) { + Slog.e(TAG, "Error communicating with transport for restore"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + return; + } + + // Successful download of the data to be parceled out to the apps, so off we go. + executeNextState(RestoreState.PM_METADATA); + } + + void restorePmMetadata() { + try { + String packageName = mTransport.nextRestorePackage(); + if (packageName == null) { + Slog.e(TAG, "Error getting first restore package"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + return; + } else if (packageName.equals("")) { + Slog.i(TAG, "No restore data available"); + int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); + EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, 0, millis); + mStatus = BackupConstants.TRANSPORT_OK; + executeNextState(RestoreState.FINAL); + return; + } else if (!packageName.equals(PACKAGE_MANAGER_SENTINEL)) { + Slog.e(TAG, "Expected restore data for \"" + PACKAGE_MANAGER_SENTINEL + + "\", found only \"" + packageName + "\""); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, + "Package manager data missing"); + executeNextState(RestoreState.FINAL); + return; + } + + // Pull the Package Manager metadata from the restore set first + PackageInfo omPackage = new PackageInfo(); + omPackage.packageName = PACKAGE_MANAGER_SENTINEL; + mPmAgent = new PackageManagerBackupAgent( + mPackageManager, mAgentPackages); + initiateOneRestore(omPackage, 0, IBackupAgent.Stub.asInterface(mPmAgent.onBind()), + mNeedFullBackup); + // The PM agent called operationComplete() already, because our invocation + // of it is process-local and therefore synchronous. That means that a + // RUNNING_QUEUE message is already enqueued. Only if we're unable to + // proceed with running the queue do we remove that pending message and + // jump straight to the FINAL state. + + // Verify that the backup set includes metadata. If not, we can't do + // signature/version verification etc, so we simply do not proceed with + // the restore operation. + if (!mPmAgent.hasMetadata()) { + Slog.e(TAG, "No restore metadata available, so not restoring settings"); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, PACKAGE_MANAGER_SENTINEL, + "Package manager restore metadata missing"); + mStatus = BackupConstants.TRANSPORT_ERROR; + mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); + executeNextState(RestoreState.FINAL); + return; + } + } catch (RemoteException e) { + Slog.e(TAG, "Error communicating with transport for restore"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupConstants.TRANSPORT_ERROR; + mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this); + executeNextState(RestoreState.FINAL); + return; + } + + // Metadata is intact, so we can now run the restore queue. If we get here, + // we have already enqueued the necessary next-step message on the looper. + } + + void restoreNextAgent() { + try { + String packageName = mTransport.nextRestorePackage(); + + if (packageName == null) { + Slog.e(TAG, "Error getting next restore package"); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + executeNextState(RestoreState.FINAL); + return; + } else if (packageName.equals("")) { + if (DEBUG) Slog.v(TAG, "No next package, finishing restore"); + int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); + EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis); + executeNextState(RestoreState.FINAL); + return; + } + + if (mObserver != null) { + try { + mObserver.onUpdate(mCount, packageName); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died in onUpdate"); + mObserver = null; + } + } + + Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName); + if (metaInfo == null) { + Slog.e(TAG, "Missing metadata for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package metadata missing"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + PackageInfo packageInfo; + try { + int flags = PackageManager.GET_SIGNATURES; + packageInfo = mPackageManager.getPackageInfo(packageName, flags); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Invalid package restoring data", e); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package missing on device"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (packageInfo.applicationInfo.backupAgentName == null + || "".equals(packageInfo.applicationInfo.backupAgentName)) { + if (DEBUG) { + Slog.i(TAG, "Data exists for package " + packageName + + " but app has no agent; skipping"); + } + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Package has no agent"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (metaInfo.versionCode > packageInfo.versionCode) { + // Data is from a "newer" version of the app than we have currently + // installed. If the app has not declared that it is prepared to + // handle this case, we do not attempt the restore. + if ((packageInfo.applicationInfo.flags + & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) { + String message = "Version " + metaInfo.versionCode + + " > installed version " + packageInfo.versionCode; + Slog.w(TAG, "Package " + packageName + ": " + message); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, + packageName, message); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } else { + if (DEBUG) Slog.v(TAG, "Version " + metaInfo.versionCode + + " > installed " + packageInfo.versionCode + + " but restoreAnyVersion"); + } + } + + if (!signaturesMatch(metaInfo.signatures, packageInfo)) { + Slog.w(TAG, "Signature mismatch restoring " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Signature mismatch"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + if (DEBUG) Slog.v(TAG, "Package " + packageName + + " restore version [" + metaInfo.versionCode + + "] is compatible with installed version [" + + packageInfo.versionCode + "]"); + + // Then set up and bind the agent + IBackupAgent agent = bindToAgentSynchronous( + packageInfo.applicationInfo, + IApplicationThread.BACKUP_MODE_INCREMENTAL); + if (agent == null) { + Slog.w(TAG, "Can't find backup agent for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, + "Restore agent missing"); + executeNextState(RestoreState.RUNNING_QUEUE); + return; + } + + // And then finally start the restore on this agent + try { + initiateOneRestore(packageInfo, metaInfo.versionCode, agent, mNeedFullBackup); + ++mCount; + } catch (Exception e) { + Slog.e(TAG, "Error when attempting restore: " + e.toString()); + agentErrorCleanup(); + executeNextState(RestoreState.RUNNING_QUEUE); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to fetch restore data from transport"); + mStatus = BackupConstants.TRANSPORT_ERROR; + executeNextState(RestoreState.FINAL); + } + } + + void finalizeRestore() { + if (MORE_DEBUG) Slog.d(TAG, "finishing restore mObserver=" + mObserver); + + try { + mTransport.finishRestore(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing restore", e); + } + + if (mObserver != null) { + try { + mObserver.restoreFinished(mStatus); + } catch (RemoteException e) { + Slog.d(TAG, "Restore observer died at restoreFinished"); + } + } + + // If this was a restoreAll operation, record that this was our + // ancestral dataset, as well as the set of apps that are possibly + // restoreable from the dataset + if (mTargetPackage == null && mPmAgent != null) { + mAncestralPackages = mPmAgent.getRestoredPackages(); + mAncestralToken = mToken; + writeRestoreTokens(); + } + + // We must under all circumstances tell the Package Manager to + // proceed with install notifications if it's waiting for us. + if (mPmToken > 0) { + if (MORE_DEBUG) Slog.v(TAG, "finishing PM token " + mPmToken); + try { + mPackageManagerBinder.finishPackageInstall(mPmToken); + } catch (RemoteException e) { /* can't happen */ } + } + + // Furthermore we need to reset the session timeout clock + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, + TIMEOUT_RESTORE_INTERVAL); + + // done; we can finally release the wakelock + Slog.i(TAG, "Restore complete."); + mWakelock.release(); + } + + // Call asynchronously into the app, passing it the restore data. The next step + // after this is always a callback, either operationComplete() or handleTimeout(). + void initiateOneRestore(PackageInfo app, int appVersionCode, IBackupAgent agent, + boolean needFullBackup) { + mCurrentPackage = app; + final String packageName = app.packageName; + + if (DEBUG) Slog.d(TAG, "initiateOneRestore packageName=" + packageName); + + // !!! TODO: get the dirs from the transport + mBackupDataName = new File(mDataDir, packageName + ".restore"); + mNewStateName = new File(mStateDir, packageName + ".new"); + mSavedStateName = new File(mStateDir, packageName); + + final int token = generateToken(); + try { + // Run the transport's restore pass + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + if (!SELinux.restorecon(mBackupDataName)) { + Slog.e(TAG, "SElinux restorecon failed for " + mBackupDataName); + } + + if (mTransport.getRestoreData(mBackupData) != BackupConstants.TRANSPORT_OK) { + // Transport-level failure, so we wind everything up and + // terminate the restore operation. + Slog.e(TAG, "Error getting restore data for " + packageName); + EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mBackupData.close(); + mBackupDataName.delete(); + executeNextState(RestoreState.FINAL); + return; + } + + // Okay, we have the data. Now have the agent do the restore. + mBackupData.close(); + mBackupData = ParcelFileDescriptor.open(mBackupDataName, + ParcelFileDescriptor.MODE_READ_ONLY); + + mNewState = ParcelFileDescriptor.open(mNewStateName, + ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + // Kick off the restore, checking for hung agents + prepareOperationTimeout(token, TIMEOUT_RESTORE_INTERVAL, this); + agent.doRestore(mBackupData, appVersionCode, mNewState, token, mBackupManagerBinder); + } catch (Exception e) { + Slog.e(TAG, "Unable to call app for restore: " + packageName, e); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName, e.toString()); + agentErrorCleanup(); // clears any pending timeout messages as well + + // After a restore failure we go back to running the queue. If there + // are no more packages to be restored that will be handled by the + // next step. + executeNextState(RestoreState.RUNNING_QUEUE); + } + } + + void agentErrorCleanup() { + // If the agent fails restore, it might have put the app's data + // into an incoherent state. For consistency we wipe its data + // again in this case before continuing with normal teardown + clearApplicationDataSynchronous(mCurrentPackage.packageName); + agentCleanup(); + } + + void agentCleanup() { + mBackupDataName.delete(); + try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {} + try { if (mNewState != null) mNewState.close(); } catch (IOException e) {} + mBackupData = mNewState = null; + + // if everything went okay, remember the recorded state now + // + // !!! TODO: the restored data should be migrated on the server + // side into the current dataset. In that case the new state file + // we just created would reflect the data already extant in the + // backend, so there'd be nothing more to do. Until that happens, + // however, we need to make sure that we record the data to the + // current backend dataset. (Yes, this means shipping the data over + // the wire in both directions. That's bad, but consistency comes + // first, then efficiency.) Once we introduce server-side data + // migration to the newly-restored device's dataset, we will change + // the following from a discard of the newly-written state to the + // "correct" operation of renaming into the canonical state blob. + mNewStateName.delete(); // TODO: remove; see above comment + //mNewStateName.renameTo(mSavedStateName); // TODO: replace with this + + // If this wasn't the PM pseudopackage, tear down the agent side + if (mCurrentPackage.applicationInfo != null) { + // unbind and tidy up even on timeout or failure + try { + mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo); + + // The agent was probably running with a stub Application object, + // which isn't a valid run mode for the main app logic. Shut + // down the app so that next time it's launched, it gets the + // usual full initialization. Note that this is only done for + // full-system restores: when a single app has requested a restore, + // it is explicitly not killed following that operation. + if (mTargetPackage == null && (mCurrentPackage.applicationInfo.flags + & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0) { + if (DEBUG) Slog.d(TAG, "Restore complete, killing host process of " + + mCurrentPackage.applicationInfo.processName); + mActivityManager.killApplicationProcess( + mCurrentPackage.applicationInfo.processName, + mCurrentPackage.applicationInfo.uid); + } + } catch (RemoteException e) { + // can't happen; we run in the same process as the activity manager + } + } + + // The caller is responsible for reestablishing the state machine; our + // responsibility here is to clear the decks for whatever comes next. + mBackupHandler.removeMessages(MSG_TIMEOUT, this); + synchronized (mCurrentOpLock) { + mCurrentOperations.clear(); + } + } + + // A call to agent.doRestore() has been positively acknowledged as complete + @Override + public void operationComplete() { + int size = (int) mBackupDataName.length(); + EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE, mCurrentPackage.packageName, size); + // Just go back to running the restore queue + agentCleanup(); + + executeNextState(RestoreState.RUNNING_QUEUE); + } + + // A call to agent.doRestore() has timed out + @Override + public void handleTimeout() { + Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); + EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, + mCurrentPackage.packageName, "restore timeout"); + // Handle like an agent that threw on invocation: wipe it and go on to the next + agentErrorCleanup(); + executeNextState(RestoreState.RUNNING_QUEUE); + } + + void executeNextState(RestoreState nextState) { + if (MORE_DEBUG) Slog.i(TAG, " => executing next step on " + + this + " nextState=" + nextState); + mCurrentState = nextState; + Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this); + mBackupHandler.sendMessage(msg); + } + } + + class PerformClearTask implements Runnable { + IBackupTransport mTransport; + PackageInfo mPackage; + + PerformClearTask(IBackupTransport transport, PackageInfo packageInfo) { + mTransport = transport; + mPackage = packageInfo; + } + + public void run() { + try { + // Clear the on-device backup state to ensure a full backup next time + File stateDir = new File(mBaseStateDir, mTransport.transportDirName()); + File stateFile = new File(stateDir, mPackage.packageName); + stateFile.delete(); + + // Tell the transport to remove all the persistent storage for the app + // TODO - need to handle failures + mTransport.clearBackupData(mPackage); + } catch (RemoteException e) { + // can't happen; the transport is local + } catch (Exception e) { + Slog.e(TAG, "Transport threw attempting to clear data for " + mPackage); + } finally { + try { + // TODO - need to handle failures + mTransport.finishBackup(); + } catch (RemoteException e) { + // can't happen; the transport is local + } + + // Last but not least, release the cpu + mWakelock.release(); + } + } + } + + class PerformInitializeTask implements Runnable { + HashSet mQueue; + + PerformInitializeTask(HashSet transportNames) { + mQueue = transportNames; + } + + public void run() { + try { + for (String transportName : mQueue) { + IBackupTransport transport = getTransport(transportName); + if (transport == null) { + Slog.e(TAG, "Requested init for " + transportName + " but not found"); + continue; + } + + Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName); + EventLog.writeEvent(EventLogTags.BACKUP_START, transport.transportDirName()); + long startRealtime = SystemClock.elapsedRealtime(); + int status = transport.initializeDevice(); + + if (status == BackupConstants.TRANSPORT_OK) { + status = transport.finishBackup(); + } + + // Okay, the wipe really happened. Clean up our local bookkeeping. + if (status == BackupConstants.TRANSPORT_OK) { + Slog.i(TAG, "Device init successful"); + int millis = (int) (SystemClock.elapsedRealtime() - startRealtime); + EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE); + resetBackupState(new File(mBaseStateDir, transport.transportDirName())); + EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis); + synchronized (mQueueLock) { + recordInitPendingLocked(false, transportName); + } + } else { + // If this didn't work, requeue this one and try again + // after a suitable interval + Slog.e(TAG, "Transport error in initializeDevice()"); + EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)"); + synchronized (mQueueLock) { + recordInitPendingLocked(true, transportName); + } + // do this via another alarm to make sure of the wakelock states + long delay = transport.requestBackupTime(); + if (DEBUG) Slog.w(TAG, "init failed on " + + transportName + " resched in " + delay); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + delay, mRunInitIntent); + } + } + } catch (RemoteException e) { + // can't happen; the transports are local + } catch (Exception e) { + Slog.e(TAG, "Unexpected error performing init", e); + } finally { + // Done; release the wakelock + mWakelock.release(); + } + } + } + + private void dataChangedImpl(String packageName) { + HashSet targets = dataChangedTargets(packageName); + dataChangedImpl(packageName, targets); + } + + private void dataChangedImpl(String packageName, HashSet targets) { + // Record that we need a backup pass for the caller. Since multiple callers + // may share a uid, we need to note all candidates within that uid and schedule + // a backup pass for each of them. + EventLog.writeEvent(EventLogTags.BACKUP_DATA_CHANGED, packageName); + + if (targets == null) { + Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" + + " uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mQueueLock) { + // Note that this client has made data changes that need to be backed up + if (targets.contains(packageName)) { + // Add the caller to the set of pending backups. If there is + // one already there, then overwrite it, but no harm done. + BackupRequest req = new BackupRequest(packageName); + if (mPendingBackups.put(packageName, req) == null) { + if (DEBUG) Slog.d(TAG, "Now staging backup of " + packageName); + + // Journal this request in case of crash. The put() + // operation returned null when this package was not already + // in the set; we want to avoid touching the disk redundantly. + writeToJournalLocked(packageName); + + if (MORE_DEBUG) { + int numKeys = mPendingBackups.size(); + Slog.d(TAG, "Now awaiting backup for " + numKeys + " participants:"); + for (BackupRequest b : mPendingBackups.values()) { + Slog.d(TAG, " + " + b); + } + } + } + } + } + } + + // Note: packageName is currently unused, but may be in the future + private HashSet dataChangedTargets(String packageName) { + // If the caller does not hold the BACKUP permission, it can only request a + // backup of its own data. + if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), + Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { + synchronized (mBackupParticipants) { + return mBackupParticipants.get(Binder.getCallingUid()); + } + } + + // a caller with full permission can ask to back up any participating app + // !!! TODO: allow backup of ANY app? + HashSet targets = new HashSet(); + synchronized (mBackupParticipants) { + int N = mBackupParticipants.size(); + for (int i = 0; i < N; i++) { + HashSet s = mBackupParticipants.valueAt(i); + if (s != null) { + targets.addAll(s); + } + } + } + return targets; + } + + private void writeToJournalLocked(String str) { + RandomAccessFile out = null; + try { + if (mJournal == null) mJournal = File.createTempFile("journal", null, mJournalDir); + out = new RandomAccessFile(mJournal, "rws"); + out.seek(out.length()); + out.writeUTF(str); + } catch (IOException e) { + Slog.e(TAG, "Can't write " + str + " to backup journal", e); + mJournal = null; + } finally { + try { if (out != null) out.close(); } catch (IOException e) {} + } + } + + // ----- IBackupManager binder interface ----- + + public void dataChanged(final String packageName) { + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + // App is running under a non-owner user profile. For now, we do not back + // up data from secondary user profiles. + // TODO: backups for all user profiles. + if (MORE_DEBUG) { + Slog.v(TAG, "dataChanged(" + packageName + ") ignored because it's user " + + callingUserHandle); + } + return; + } + + final HashSet targets = dataChangedTargets(packageName); + if (targets == null) { + Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'" + + " uid=" + Binder.getCallingUid()); + return; + } + + mBackupHandler.post(new Runnable() { + public void run() { + dataChangedImpl(packageName, targets); + } + }); + } + + // Clear the given package's backup data from the current transport + public void clearBackupData(String transportName, String packageName) { + if (DEBUG) Slog.v(TAG, "clearBackupData() of " + packageName + " on " + transportName); + PackageInfo info; + try { + info = mPackageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Slog.d(TAG, "No such package '" + packageName + "' - not clearing backup data"); + return; + } + + // If the caller does not hold the BACKUP permission, it can only request a + // wipe of its own backed-up data. + HashSet apps; + if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(), + Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) { + apps = mBackupParticipants.get(Binder.getCallingUid()); + } else { + // a caller with full permission can ask to back up any participating app + // !!! TODO: allow data-clear of ANY app? + if (DEBUG) Slog.v(TAG, "Privileged caller, allowing clear of other apps"); + apps = new HashSet(); + int N = mBackupParticipants.size(); + for (int i = 0; i < N; i++) { + HashSet s = mBackupParticipants.valueAt(i); + if (s != null) { + apps.addAll(s); + } + } + } + + // Is the given app an available participant? + if (apps.contains(packageName)) { + // found it; fire off the clear request + if (DEBUG) Slog.v(TAG, "Found the app - running clear process"); + mBackupHandler.removeMessages(MSG_RETRY_CLEAR); + synchronized (mQueueLock) { + final IBackupTransport transport = getTransport(transportName); + if (transport == null) { + // transport is currently unavailable -- make sure to retry + Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR, + new ClearRetryParams(transportName, packageName)); + mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL); + return; + } + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_CLEAR, + new ClearParams(transport, info)); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + } + } + } + + // Run a backup pass immediately for any applications that have declared + // that they have pending updates. + public void backupNow() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow"); + + if (DEBUG) Slog.v(TAG, "Scheduling immediate backup pass"); + synchronized (mQueueLock) { + // Because the alarms we are using can jitter, and we want an *immediate* + // backup pass to happen, we restart the timer beginning with "next time," + // then manually fire the backup trigger intent ourselves. + startBackupAlarmsLocked(BACKUP_INTERVAL); + try { + mRunBackupIntent.send(); + } catch (PendingIntent.CanceledException e) { + // should never happen + Slog.e(TAG, "run-backup intent cancelled!"); + } + } + } + + boolean deviceIsProvisioned() { + final ContentResolver resolver = mContext.getContentResolver(); + return (Settings.Global.getInt(resolver, Settings.Global.DEVICE_PROVISIONED, 0) != 0); + } + + // Run a *full* backup pass for the given package, writing the resulting data stream + // to the supplied file descriptor. This method is synchronous and does not return + // to the caller until the backup has been completed. + public void fullBackup(ParcelFileDescriptor fd, boolean includeApks, + boolean includeObbs, boolean includeShared, + boolean doAllApps, boolean includeSystem, String[] pkgList) { + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullBackup"); + + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + throw new IllegalStateException("Backup supported only for the device owner"); + } + + // Validate + if (!doAllApps) { + if (!includeShared) { + // If we're backing up shared data (sdcard or equivalent), then we can run + // without any supplied app names. Otherwise, we'd be doing no work, so + // report the error. + if (pkgList == null || pkgList.length == 0) { + throw new IllegalArgumentException( + "Backup requested but neither shared nor any apps named"); + } + } + } + + long oldId = Binder.clearCallingIdentity(); + try { + // Doesn't make sense to do a full backup prior to setup + if (!deviceIsProvisioned()) { + Slog.i(TAG, "Full backup not supported before setup"); + return; + } + + if (DEBUG) Slog.v(TAG, "Requesting full backup: apks=" + includeApks + + " obb=" + includeObbs + " shared=" + includeShared + " all=" + doAllApps + + " pkgs=" + pkgList); + Slog.i(TAG, "Beginning full backup..."); + + FullBackupParams params = new FullBackupParams(fd, includeApks, includeObbs, + includeShared, doAllApps, includeSystem, pkgList); + final int token = generateToken(); + synchronized (mFullConfirmations) { + mFullConfirmations.put(token, params); + } + + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full backup confirmation"); + mFullConfirmations.delete(token); + return; + } + + // make sure the screen is lit for the user interaction + mPowerManager.userActivity(SystemClock.uptimeMillis(), false); + + // start the confirmation countdown + startConfirmationTimeout(token, params); + + // wait for the backup to be performed + if (DEBUG) Slog.d(TAG, "Waiting for full backup completion..."); + waitForCompletion(params); + } finally { + try { + fd.close(); + } catch (IOException e) { + // just eat it + } + Binder.restoreCallingIdentity(oldId); + Slog.d(TAG, "Full backup processing complete."); + } + } + + public void fullRestore(ParcelFileDescriptor fd) { + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "fullRestore"); + + final int callingUserHandle = UserHandle.getCallingUserId(); + if (callingUserHandle != UserHandle.USER_OWNER) { + throw new IllegalStateException("Restore supported only for the device owner"); + } + + long oldId = Binder.clearCallingIdentity(); + + try { + // Check whether the device has been provisioned -- we don't handle + // full restores prior to completing the setup process. + if (!deviceIsProvisioned()) { + Slog.i(TAG, "Full restore not permitted before setup"); + return; + } + + Slog.i(TAG, "Beginning full restore..."); + + FullRestoreParams params = new FullRestoreParams(fd); + final int token = generateToken(); + synchronized (mFullConfirmations) { + mFullConfirmations.put(token, params); + } + + // start up the confirmation UI + if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token); + if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { + Slog.e(TAG, "Unable to launch full restore confirmation"); + mFullConfirmations.delete(token); + return; + } + + // make sure the screen is lit for the user interaction + mPowerManager.userActivity(SystemClock.uptimeMillis(), false); + + // start the confirmation countdown + startConfirmationTimeout(token, params); + + // wait for the restore to be performed + if (DEBUG) Slog.d(TAG, "Waiting for full restore completion..."); + waitForCompletion(params); + } finally { + try { + fd.close(); + } catch (IOException e) { + Slog.w(TAG, "Error trying to close fd after full restore: " + e); + } + Binder.restoreCallingIdentity(oldId); + Slog.i(TAG, "Full restore processing complete."); + } + } + + boolean startConfirmationUi(int token, String action) { + try { + Intent confIntent = new Intent(action); + confIntent.setClassName("com.android.backupconfirm", + "com.android.backupconfirm.BackupRestoreConfirmation"); + confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token); + confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(confIntent); + } catch (ActivityNotFoundException e) { + return false; + } + return true; + } + + void startConfirmationTimeout(int token, FullParams params) { + if (MORE_DEBUG) Slog.d(TAG, "Posting conf timeout msg after " + + TIMEOUT_FULL_CONFIRMATION + " millis"); + Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT, + token, 0, params); + mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION); + } + + void waitForCompletion(FullParams params) { + synchronized (params.latch) { + while (params.latch.get() == false) { + try { + params.latch.wait(); + } catch (InterruptedException e) { /* never interrupted */ } + } + } + } + + void signalFullBackupRestoreCompletion(FullParams params) { + synchronized (params.latch) { + params.latch.set(true); + params.latch.notifyAll(); + } + } + + // Confirm that the previously-requested full backup/restore operation can proceed. This + // is used to require a user-facing disclosure about the operation. + @Override + public void acknowledgeFullBackupOrRestore(int token, boolean allow, + String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { + if (DEBUG) Slog.d(TAG, "acknowledgeFullBackupOrRestore : token=" + token + + " allow=" + allow); + + // TODO: possibly require not just this signature-only permission, but even + // require that the specific designated confirmation-UI app uid is the caller? + mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "acknowledgeFullBackupOrRestore"); + + long oldId = Binder.clearCallingIdentity(); + try { + + FullParams params; + synchronized (mFullConfirmations) { + params = mFullConfirmations.get(token); + if (params != null) { + mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params); + mFullConfirmations.delete(token); + + if (allow) { + final int verb = params instanceof FullBackupParams + ? MSG_RUN_FULL_BACKUP + : MSG_RUN_FULL_RESTORE; + + params.observer = observer; + params.curPassword = curPassword; + + boolean isEncrypted; + try { + isEncrypted = (mMountService.getEncryptionState() != + IMountService.ENCRYPTION_STATE_NONE); + if (isEncrypted) Slog.w(TAG, "Device is encrypted; forcing enc password"); + } catch (RemoteException e) { + // couldn't contact the mount service; fail "safe" and assume encryption + Slog.e(TAG, "Unable to contact mount service!"); + isEncrypted = true; + } + params.encryptPassword = (isEncrypted) ? curPassword : encPpassword; + + if (DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(verb, params); + mBackupHandler.sendMessage(msg); + } else { + Slog.w(TAG, "User rejected full backup/restore operation"); + // indicate completion without having actually transferred any data + signalFullBackupRestoreCompletion(params); + } + } else { + Slog.w(TAG, "Attempted to ack full backup/restore with invalid token"); + } + } + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + // Enable/disable the backup service + @Override + public void setBackupEnabled(boolean enable) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupEnabled"); + + Slog.i(TAG, "Backup enabled => " + enable); + + long oldId = Binder.clearCallingIdentity(); + try { + boolean wasEnabled = mEnabled; + synchronized (this) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.BACKUP_ENABLED, enable ? 1 : 0); + mEnabled = enable; + } + + synchronized (mQueueLock) { + if (enable && !wasEnabled && mProvisioned) { + // if we've just been enabled, start scheduling backup passes + startBackupAlarmsLocked(BACKUP_INTERVAL); + } else if (!enable) { + // No longer enabled, so stop running backups + if (DEBUG) Slog.i(TAG, "Opting out of backup"); + + mAlarmManager.cancel(mRunBackupIntent); + + // This also constitutes an opt-out, so we wipe any data for + // this device from the backend. We start that process with + // an alarm in order to guarantee wakelock states. + if (wasEnabled && mProvisioned) { + // NOTE: we currently flush every registered transport, not just + // the currently-active one. + HashSet allTransports; + synchronized (mTransports) { + allTransports = new HashSet(mTransports.keySet()); + } + // build the set of transports for which we are posting an init + for (String transport : allTransports) { + recordInitPendingLocked(true, transport); + } + mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), + mRunInitIntent); + } + } + } + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + // Enable/disable automatic restore of app data at install time + public void setAutoRestore(boolean doAutoRestore) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setAutoRestore"); + + Slog.i(TAG, "Auto restore => " + doAutoRestore); + + synchronized (this) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0); + mAutoRestore = doAutoRestore; + } + } + + // Mark the backup service as having been provisioned + public void setBackupProvisioned(boolean available) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "setBackupProvisioned"); + /* + * This is now a no-op; provisioning is simply the device's own setup state. + */ + } + + private void startBackupAlarmsLocked(long delayBeforeFirstBackup) { + // We used to use setInexactRepeating(), but that may be linked to + // backups running at :00 more often than not, creating load spikes. + // Schedule at an exact time for now, and also add a bit of "fuzz". + + Random random = new Random(); + long when = System.currentTimeMillis() + delayBeforeFirstBackup + + random.nextInt(FUZZ_MILLIS); + mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, when, + BACKUP_INTERVAL + random.nextInt(FUZZ_MILLIS), mRunBackupIntent); + mNextBackupPass = when; + } + + // Report whether the backup mechanism is currently enabled + public boolean isBackupEnabled() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "isBackupEnabled"); + return mEnabled; // no need to synchronize just to read it + } + + // Report the name of the currently active transport + public String getCurrentTransport() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getCurrentTransport"); + if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + mCurrentTransport); + return mCurrentTransport; + } + + // Report all known, available backup transports + public String[] listAllTransports() { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "listAllTransports"); + + String[] list = null; + ArrayList known = new ArrayList(); + for (Map.Entry entry : mTransports.entrySet()) { + if (entry.getValue() != null) { + known.add(entry.getKey()); + } + } + + if (known.size() > 0) { + list = new String[known.size()]; + known.toArray(list); + } + return list; + } + + // Select which transport to use for the next backup operation. If the given + // name is not one of the available transports, no action is taken and the method + // returns null. + public String selectBackupTransport(String transport) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "selectBackupTransport"); + + synchronized (mTransports) { + String prevTransport = null; + if (mTransports.get(transport) != null) { + prevTransport = mCurrentTransport; + mCurrentTransport = transport; + Settings.Secure.putString(mContext.getContentResolver(), + Settings.Secure.BACKUP_TRANSPORT, transport); + Slog.v(TAG, "selectBackupTransport() set " + mCurrentTransport + + " returning " + prevTransport); + } else { + Slog.w(TAG, "Attempt to select unavailable transport " + transport); + } + return prevTransport; + } + } + + // Supply the configuration Intent for the given transport. If the name is not one + // of the available transports, or if the transport does not supply any configuration + // UI, the method returns null. + public Intent getConfigurationIntent(String transportName) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getConfigurationIntent"); + + synchronized (mTransports) { + final IBackupTransport transport = mTransports.get(transportName); + if (transport != null) { + try { + final Intent intent = transport.configurationIntent(); + if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent " + + intent); + return intent; + } catch (RemoteException e) { + /* fall through to return null */ + } + } + } + + return null; + } + + // Supply the configuration summary string for the given transport. If the name is + // not one of the available transports, or if the transport does not supply any + // summary / destination string, the method can return null. + // + // This string is used VERBATIM as the summary text of the relevant Settings item! + public String getDestinationString(String transportName) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getDestinationString"); + + synchronized (mTransports) { + final IBackupTransport transport = mTransports.get(transportName); + if (transport != null) { + try { + final String text = transport.currentDestinationString(); + if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text); + return text; + } catch (RemoteException e) { + /* fall through to return null */ + } + } + } + + return null; + } + + // Callback: a requested backup agent has been instantiated. This should only + // be called from the Activity Manager. + public void agentConnected(String packageName, IBinder agentBinder) { + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + Slog.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); + IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder); + mConnectedAgent = agent; + mConnecting = false; + } else { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent connected"); + } + mAgentConnectLock.notifyAll(); + } + } + + // Callback: a backup agent has failed to come up, or has unexpectedly quit. + // If the agent failed to come up in the first place, the agentBinder argument + // will be null. This should only be called from the Activity Manager. + public void agentDisconnected(String packageName) { + // TODO: handle backup being interrupted + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + mConnectedAgent = null; + mConnecting = false; + } else { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent disconnected"); + } + mAgentConnectLock.notifyAll(); + } + } + + // An application being installed will need a restore pass, then the Package Manager + // will need to be told when the restore is finished. + public void restoreAtInstall(String packageName, int token) { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " attemping install-time restore"); + return; + } + + long restoreSet = getAvailableRestoreToken(packageName); + if (DEBUG) Slog.v(TAG, "restoreAtInstall pkg=" + packageName + + " token=" + Integer.toHexString(token) + + " restoreSet=" + Long.toHexString(restoreSet)); + + if (mAutoRestore && mProvisioned && restoreSet != 0) { + // Do we have a transport to fetch data for us? + IBackupTransport transport = getTransport(mCurrentTransport); + if (transport == null) { + if (DEBUG) Slog.w(TAG, "No transport for install-time restore"); + return; + } + + try { + // okay, we're going to attempt a restore of this package from this restore set. + // The eventual message back into the Package Manager to run the post-install + // steps for 'token' will be issued from the restore handling code. + + // This can throw and so *must* happen before the wakelock is acquired + String dirName = transport.transportDirName(); + + // We can use a synthetic PackageInfo here because: + // 1. We know it's valid, since the Package Manager supplied the name + // 2. Only the packageName field will be used by the restore code + PackageInfo pkg = new PackageInfo(); + pkg.packageName = packageName; + + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(transport, dirName, null, + restoreSet, pkg, token, true); + mBackupHandler.sendMessage(msg); + } catch (RemoteException e) { + // Binding to the transport broke; back off and proceed with the installation. + Slog.e(TAG, "Unable to contact transport for install-time restore"); + } + } else { + // Auto-restore disabled or no way to attempt a restore; just tell the Package + // Manager to proceed with the post-install handling for this package. + if (DEBUG) Slog.v(TAG, "No restore set -- skipping restore"); + try { + mPackageManagerBinder.finishPackageInstall(token); + } catch (RemoteException e) { /* can't happen */ } + } + } + + // Hand off a restore session + public IRestoreSession beginRestoreSession(String packageName, String transport) { + if (DEBUG) Slog.v(TAG, "beginRestoreSession: pkg=" + packageName + + " transport=" + transport); + + boolean needPermission = true; + if (transport == null) { + transport = mCurrentTransport; + + if (packageName != null) { + PackageInfo app = null; + try { + app = mPackageManager.getPackageInfo(packageName, 0); + } catch (NameNotFoundException nnf) { + Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); + throw new IllegalArgumentException("Package " + packageName + " not found"); + } + + if (app.applicationInfo.uid == Binder.getCallingUid()) { + // So: using the current active transport, and the caller has asked + // that its own package will be restored. In this narrow use case + // we do not require the caller to hold the permission. + needPermission = false; + } + } + } + + if (needPermission) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "beginRestoreSession"); + } else { + if (DEBUG) Slog.d(TAG, "restoring self on current transport; no permission needed"); + } + + synchronized(this) { + if (mActiveRestoreSession != null) { + Slog.d(TAG, "Restore session requested but one already active"); + return null; + } + mActiveRestoreSession = new ActiveRestoreSession(packageName, transport); + mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_TIMEOUT, TIMEOUT_RESTORE_INTERVAL); + } + return mActiveRestoreSession; + } + + void clearRestoreSession(ActiveRestoreSession currentSession) { + synchronized(this) { + if (currentSession != mActiveRestoreSession) { + Slog.e(TAG, "ending non-current restore session"); + } else { + if (DEBUG) Slog.v(TAG, "Clearing restore session and halting timeout"); + mActiveRestoreSession = null; + mBackupHandler.removeMessages(MSG_RESTORE_TIMEOUT); + } + } + } + + // Note that a currently-active backup agent has notified us that it has + // completed the given outstanding asynchronous backup/restore operation. + @Override + public void opComplete(int token) { + if (MORE_DEBUG) Slog.v(TAG, "opComplete: " + Integer.toHexString(token)); + Operation op = null; + synchronized (mCurrentOpLock) { + op = mCurrentOperations.get(token); + if (op != null) { + op.state = OP_ACKNOWLEDGED; + } + mCurrentOpLock.notifyAll(); + } + + // The completion callback, if any, is invoked on the handler + if (op != null && op.callback != null) { + Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, op.callback); + mBackupHandler.sendMessage(msg); + } + } + + // ----- Restore session ----- + + class ActiveRestoreSession extends IRestoreSession.Stub { + private static final String TAG = "RestoreSession"; + + private String mPackageName; + private IBackupTransport mRestoreTransport = null; + RestoreSet[] mRestoreSets = null; + boolean mEnded = false; + + ActiveRestoreSession(String packageName, String transport) { + mPackageName = packageName; + mRestoreTransport = getTransport(transport); + } + + // --- Binder interface --- + public synchronized int getAvailableRestoreSets(IRestoreObserver observer) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "getAvailableRestoreSets"); + if (observer == null) { + throw new IllegalArgumentException("Observer must not be null"); + } + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + long oldId = Binder.clearCallingIdentity(); + try { + if (mRestoreTransport == null) { + Slog.w(TAG, "Null transport getting restore sets"); + return -1; + } + // spin off the transport request to our service thread + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_GET_RESTORE_SETS, + new RestoreGetSetsParams(mRestoreTransport, this, observer)); + mBackupHandler.sendMessage(msg); + return 0; + } catch (Exception e) { + Slog.e(TAG, "Error in getAvailableRestoreSets", e); + return -1; + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + public synchronized int restoreAll(long token, IRestoreObserver observer) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "performRestore"); + + if (DEBUG) Slog.d(TAG, "restoreAll token=" + Long.toHexString(token) + + " observer=" + observer); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mRestoreTransport == null || mRestoreSets == null) { + Slog.e(TAG, "Ignoring restoreAll() with no restore set"); + return -1; + } + + if (mPackageName != null) { + Slog.e(TAG, "Ignoring restoreAll() on single-package session"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + synchronized (mQueueLock) { + for (int i = 0; i < mRestoreSets.length; i++) { + if (token == mRestoreSets[i].token) { + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, + observer, token, true); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + } + } + + Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); + return -1; + } + + public synchronized int restoreSome(long token, IRestoreObserver observer, + String[] packages) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, + "performRestore"); + + if (DEBUG) { + StringBuilder b = new StringBuilder(128); + b.append("restoreSome token="); + b.append(Long.toHexString(token)); + b.append(" observer="); + b.append(observer.toString()); + b.append(" packages="); + if (packages == null) { + b.append("null"); + } else { + b.append('{'); + boolean first = true; + for (String s : packages) { + if (!first) { + b.append(", "); + } else first = false; + b.append(s); + } + b.append('}'); + } + Slog.d(TAG, b.toString()); + } + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mRestoreTransport == null || mRestoreSets == null) { + Slog.e(TAG, "Ignoring restoreAll() with no restore set"); + return -1; + } + + if (mPackageName != null) { + Slog.e(TAG, "Ignoring restoreAll() on single-package session"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + synchronized (mQueueLock) { + for (int i = 0; i < mRestoreSets.length; i++) { + if (token == mRestoreSets[i].token) { + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, token, + packages, true); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + } + } + + Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found"); + return -1; + } + + public synchronized int restorePackage(String packageName, IRestoreObserver observer) { + if (DEBUG) Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + if (mPackageName != null) { + if (! mPackageName.equals(packageName)) { + Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName + + " on session for package " + mPackageName); + return -1; + } + } + + PackageInfo app = null; + try { + app = mPackageManager.getPackageInfo(packageName, 0); + } catch (NameNotFoundException nnf) { + Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName); + return -1; + } + + // If the caller is not privileged and is not coming from the target + // app's uid, throw a permission exception back to the caller. + int perm = mContext.checkPermission(android.Manifest.permission.BACKUP, + Binder.getCallingPid(), Binder.getCallingUid()); + if ((perm == PackageManager.PERMISSION_DENIED) && + (app.applicationInfo.uid != Binder.getCallingUid())) { + Slog.w(TAG, "restorePackage: bad packageName=" + packageName + + " or calling uid=" + Binder.getCallingUid()); + throw new SecurityException("No permission to restore other packages"); + } + + // If the package has no backup agent, we obviously cannot proceed + if (app.applicationInfo.backupAgentName == null) { + Slog.w(TAG, "Asked to restore package " + packageName + " with no agent"); + return -1; + } + + // So far so good; we're allowed to try to restore this package. Now + // check whether there is data for it in the current dataset, falling back + // to the ancestral dataset if not. + long token = getAvailableRestoreToken(packageName); + + // If we didn't come up with a place to look -- no ancestral dataset and + // the app has never been backed up from this device -- there's nothing + // to do but return failure. + if (token == 0) { + if (DEBUG) Slog.w(TAG, "No data available for this package; not restoring"); + return -1; + } + + String dirName; + try { + dirName = mRestoreTransport.transportDirName(); + } catch (RemoteException e) { + // Transport went AWOL; fail. + Slog.e(TAG, "Unable to contact transport for restore"); + return -1; + } + + // Ready to go: enqueue the restore request and claim success + long oldId = Binder.clearCallingIdentity(); + mWakelock.acquire(); + Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE); + msg.obj = new RestoreParams(mRestoreTransport, dirName, + observer, token, app, 0, false); + mBackupHandler.sendMessage(msg); + Binder.restoreCallingIdentity(oldId); + return 0; + } + + // Posted to the handler to tear down a restore session in a cleanly synchronized way + class EndRestoreRunnable implements Runnable { + BackupManagerService mBackupManager; + ActiveRestoreSession mSession; + + EndRestoreRunnable(BackupManagerService manager, ActiveRestoreSession session) { + mBackupManager = manager; + mSession = session; + } + + public void run() { + // clean up the session's bookkeeping + synchronized (mSession) { + try { + if (mSession.mRestoreTransport != null) { + mSession.mRestoreTransport.finishRestore(); + } + } catch (Exception e) { + Slog.e(TAG, "Error in finishRestore", e); + } finally { + mSession.mRestoreTransport = null; + mSession.mEnded = true; + } + } + + // clean up the BackupManagerService side of the bookkeeping + // and cancel any pending timeout message + mBackupManager.clearRestoreSession(mSession); + } + } + + public synchronized void endRestoreSession() { + if (DEBUG) Slog.d(TAG, "endRestoreSession"); + + if (mEnded) { + throw new IllegalStateException("Restore session already ended"); + } + + mBackupHandler.post(new EndRestoreRunnable(BackupManagerService.this, this)); + } + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + long identityToken = Binder.clearCallingIdentity(); + try { + dumpInternal(pw); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } + + private void dumpInternal(PrintWriter pw) { + synchronized (mQueueLock) { + pw.println("Backup Manager is " + (mEnabled ? "enabled" : "disabled") + + " / " + (!mProvisioned ? "not " : "") + "provisioned / " + + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init"); + pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled")); + if (mBackupRunning) pw.println("Backup currently running"); + pw.println("Last backup pass started: " + mLastBackupPass + + " (now = " + System.currentTimeMillis() + ')'); + pw.println(" next scheduled: " + mNextBackupPass); + + pw.println("Available transports:"); + for (String t : listAllTransports()) { + pw.println((t.equals(mCurrentTransport) ? " * " : " ") + t); + try { + IBackupTransport transport = getTransport(t); + File dir = new File(mBaseStateDir, transport.transportDirName()); + pw.println(" destination: " + transport.currentDestinationString()); + pw.println(" intent: " + transport.configurationIntent()); + for (File f : dir.listFiles()) { + pw.println(" " + f.getName() + " - " + f.length() + " state bytes"); + } + } catch (Exception e) { + Slog.e(TAG, "Error in transport", e); + pw.println(" Error: " + e); + } + } + + pw.println("Pending init: " + mPendingInits.size()); + for (String s : mPendingInits) { + pw.println(" " + s); + } + + if (DEBUG_BACKUP_TRACE) { + synchronized (mBackupTrace) { + if (!mBackupTrace.isEmpty()) { + pw.println("Most recent backup trace:"); + for (String s : mBackupTrace) { + pw.println(" " + s); + } + } + } + } + + int N = mBackupParticipants.size(); + pw.println("Participants:"); + for (int i=0; i participants = mBackupParticipants.valueAt(i); + for (String app: participants) { + pw.println(" " + app); + } + } + + pw.println("Ancestral packages: " + + (mAncestralPackages == null ? "none" : mAncestralPackages.size())); + if (mAncestralPackages != null) { + for (String pkg : mAncestralPackages) { + pw.println(" " + pkg); + } + } + + pw.println("Ever backed up: " + mEverStoredApps.size()); + for (String pkg : mEverStoredApps) { + pw.println(" " + pkg); + } + + pw.println("Pending backup: " + mPendingBackups.size()); + for (BackupRequest req : mPendingBackups.values()) { + pw.println(" " + req); + } + } + } +} diff --git a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java new file mode 100644 index 0000000..495da88 --- /dev/null +++ b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import android.app.backup.BackupAgent; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * We back up the signatures of each package so that during a system restore, + * we can verify that the app whose data we think we have matches the app + * actually resident on the device. + * + * Since the Package Manager isn't a proper "application" we just provide a + * direct IBackupAgent implementation and hand-construct it at need. + */ +public class PackageManagerBackupAgent extends BackupAgent { + private static final String TAG = "PMBA"; + private static final boolean DEBUG = false; + + // key under which we store global metadata (individual app metadata + // is stored using the package name as a key) + private static final String GLOBAL_METADATA_KEY = "@meta@"; + + private List mAllPackages; + private PackageManager mPackageManager; + // version & signature info of each app in a restore set + private HashMap mRestoredSignatures; + // The version info of each backed-up app as read from the state file + private HashMap mStateVersions = new HashMap(); + + private final HashSet mExisting = new HashSet(); + private int mStoredSdkVersion; + private String mStoredIncrementalVersion; + private boolean mHasMetadata; + + public class Metadata { + public int versionCode; + public Signature[] signatures; + + Metadata(int version, Signature[] sigs) { + versionCode = version; + signatures = sigs; + } + } + + // We're constructed with the set of applications that are participating + // in backup. This set changes as apps are installed & removed. + PackageManagerBackupAgent(PackageManager packageMgr, List packages) { + mPackageManager = packageMgr; + mAllPackages = packages; + mRestoredSignatures = null; + mHasMetadata = false; + } + + public boolean hasMetadata() { + return mHasMetadata; + } + + public Metadata getRestoredMetadata(String packageName) { + if (mRestoredSignatures == null) { + Slog.w(TAG, "getRestoredMetadata() before metadata read!"); + return null; + } + + return mRestoredSignatures.get(packageName); + } + + public Set getRestoredPackages() { + if (mRestoredSignatures == null) { + Slog.w(TAG, "getRestoredPackages() before metadata read!"); + return null; + } + + // This is technically the set of packages on the originating handset + // that had backup agents at all, not limited to the set of packages + // that had actually contributed a restore dataset, but it's a + // close enough approximation for our purposes and does not require any + // additional involvement by the transport to obtain. + return mRestoredSignatures.keySet(); + } + + // The backed up data is the signature block for each app, keyed by + // the package name. + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + if (DEBUG) Slog.v(TAG, "onBackup()"); + + ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); // we'll reuse these + DataOutputStream outputBufferStream = new DataOutputStream(outputBuffer); + parseStateFile(oldState); + + // If the stored version string differs, we need to re-backup all + // of the metadata. We force this by removing everything from the + // "already backed up" map built by parseStateFile(). + if (mStoredIncrementalVersion == null + || !mStoredIncrementalVersion.equals(Build.VERSION.INCREMENTAL)) { + Slog.i(TAG, "Previous metadata " + mStoredIncrementalVersion + " mismatch vs " + + Build.VERSION.INCREMENTAL + " - rewriting"); + mExisting.clear(); + } + + try { + /* + * Global metadata: + * + * int SDKversion -- the SDK version of the OS itself on the device + * that produced this backup set. Used to reject + * backups from later OSes onto earlier ones. + * String incremental -- the incremental release name of the OS stored in + * the backup set. + */ + if (!mExisting.contains(GLOBAL_METADATA_KEY)) { + if (DEBUG) Slog.v(TAG, "Storing global metadata key"); + outputBufferStream.writeInt(Build.VERSION.SDK_INT); + outputBufferStream.writeUTF(Build.VERSION.INCREMENTAL); + writeEntity(data, GLOBAL_METADATA_KEY, outputBuffer.toByteArray()); + } else { + if (DEBUG) Slog.v(TAG, "Global metadata key already stored"); + // don't consider it to have been skipped/deleted + mExisting.remove(GLOBAL_METADATA_KEY); + } + + // For each app we have on device, see if we've backed it up yet. If not, + // write its signature block to the output, keyed on the package name. + for (PackageInfo pkg : mAllPackages) { + String packName = pkg.packageName; + if (packName.equals(GLOBAL_METADATA_KEY)) { + // We've already handled the metadata key; skip it here + continue; + } else { + PackageInfo info = null; + try { + info = mPackageManager.getPackageInfo(packName, + PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + // Weird; we just found it, and now are told it doesn't exist. + // Treat it as having been removed from the device. + mExisting.add(packName); + continue; + } + + if (mExisting.contains(packName)) { + // We have backed up this app before. Check whether the version + // of the backup matches the version of the current app; if they + // don't match, the app has been updated and we need to store its + // metadata again. In either case, take it out of mExisting so that + // we don't consider it deleted later. + mExisting.remove(packName); + if (info.versionCode == mStateVersions.get(packName).versionCode) { + continue; + } + } + + if (info.signatures == null || info.signatures.length == 0) + { + Slog.w(TAG, "Not backing up package " + packName + + " since it appears to have no signatures."); + continue; + } + + // We need to store this app's metadata + /* + * Metadata for each package: + * + * int version -- [4] the package's versionCode + * byte[] signatures -- [len] flattened Signature[] of the package + */ + + // marshal the version code in a canonical form + outputBuffer.reset(); + outputBufferStream.writeInt(info.versionCode); + writeSignatureArray(outputBufferStream, info.signatures); + + if (DEBUG) { + Slog.v(TAG, "+ writing metadata for " + packName + + " version=" + info.versionCode + + " entityLen=" + outputBuffer.size()); + } + + // Now we can write the backup entity for this package + writeEntity(data, packName, outputBuffer.toByteArray()); + } + } + + // At this point, the only entries in 'existing' are apps that were + // mentioned in the saved state file, but appear to no longer be present + // on the device. Write a deletion entity for them. + for (String app : mExisting) { + if (DEBUG) Slog.v(TAG, "- removing metadata for deleted pkg " + app); + try { + data.writeEntityHeader(app, -1); + } catch (IOException e) { + Slog.e(TAG, "Unable to write package deletions!"); + return; + } + } + } catch (IOException e) { + // Real error writing data + Slog.e(TAG, "Unable to write package backup data file!"); + return; + } + + // Finally, write the new state blob -- just the list of all apps we handled + writeStateFile(mAllPackages, newState); + } + + private static void writeEntity(BackupDataOutput data, String key, byte[] bytes) + throws IOException { + data.writeEntityHeader(key, bytes.length); + data.writeEntityData(bytes, bytes.length); + } + + // "Restore" here is a misnomer. What we're really doing is reading back the + // set of app signatures associated with each backed-up app in this restore + // image. We'll use those later to determine what we can legitimately restore. + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + List restoredApps = new ArrayList(); + HashMap sigMap = new HashMap(); + if (DEBUG) Slog.v(TAG, "onRestore()"); + int storedSystemVersion = -1; + + while (data.readNextHeader()) { + String key = data.getKey(); + int dataSize = data.getDataSize(); + + if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize); + + // generic setup to parse any entity data + byte[] inputBytes = new byte[dataSize]; + data.readEntityData(inputBytes, 0, dataSize); + ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes); + DataInputStream inputBufferStream = new DataInputStream(inputBuffer); + + if (key.equals(GLOBAL_METADATA_KEY)) { + int storedSdkVersion = inputBufferStream.readInt(); + if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion); + if (storedSystemVersion > Build.VERSION.SDK_INT) { + // returning before setting the sig map means we rejected the restore set + Slog.w(TAG, "Restore set was from a later version of Android; not restoring"); + return; + } + mStoredSdkVersion = storedSdkVersion; + mStoredIncrementalVersion = inputBufferStream.readUTF(); + mHasMetadata = true; + if (DEBUG) { + Slog.i(TAG, "Restore set version " + storedSystemVersion + + " is compatible with OS version " + Build.VERSION.SDK_INT + + " (" + mStoredIncrementalVersion + " vs " + + Build.VERSION.INCREMENTAL + ")"); + } + } else { + // it's a file metadata record + int versionCode = inputBufferStream.readInt(); + Signature[] sigs = readSignatureArray(inputBufferStream); + if (DEBUG) { + Slog.i(TAG, " read metadata for " + key + + " dataSize=" + dataSize + + " versionCode=" + versionCode + " sigs=" + sigs); + } + + if (sigs == null || sigs.length == 0) { + Slog.w(TAG, "Not restoring package " + key + + " since it appears to have no signatures."); + continue; + } + + ApplicationInfo app = new ApplicationInfo(); + app.packageName = key; + restoredApps.add(app); + sigMap.put(key, new Metadata(versionCode, sigs)); + } + } + + // On successful completion, cache the signature map for the Backup Manager to use + mRestoredSignatures = sigMap; + } + + private static void writeSignatureArray(DataOutputStream out, Signature[] sigs) + throws IOException { + // write the number of signatures in the array + out.writeInt(sigs.length); + + // write the signatures themselves, length + flattened buffer + for (Signature sig : sigs) { + byte[] flat = sig.toByteArray(); + out.writeInt(flat.length); + out.write(flat); + } + } + + private static Signature[] readSignatureArray(DataInputStream in) { + try { + int num; + try { + num = in.readInt(); + } catch (EOFException e) { + // clean termination + Slog.w(TAG, "Read empty signature block"); + return null; + } + + if (DEBUG) Slog.v(TAG, " ... unflatten read " + num); + + // Sensical? + if (num > 20) { + Slog.e(TAG, "Suspiciously large sig count in restore data; aborting"); + throw new IllegalStateException("Bad restore state"); + } + + Signature[] sigs = new Signature[num]; + for (int i = 0; i < num; i++) { + int len = in.readInt(); + byte[] flatSig = new byte[len]; + in.read(flatSig); + sigs[i] = new Signature(flatSig); + } + return sigs; + } catch (IOException e) { + Slog.e(TAG, "Unable to read signatures"); + return null; + } + } + + // Util: parse out an existing state file into a usable structure + private void parseStateFile(ParcelFileDescriptor stateFile) { + mExisting.clear(); + mStateVersions.clear(); + mStoredSdkVersion = 0; + mStoredIncrementalVersion = null; + + // The state file is just the list of app names we have stored signatures for + // with the exception of the metadata block, to which is also appended the + // version numbers corresponding with the last time we wrote this PM block. + // If they mismatch the current system, we'll re-store the metadata key. + FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor()); + DataInputStream in = new DataInputStream(instream); + + int bufSize = 256; + byte[] buf = new byte[bufSize]; + try { + String pkg = in.readUTF(); + if (pkg.equals(GLOBAL_METADATA_KEY)) { + mStoredSdkVersion = in.readInt(); + mStoredIncrementalVersion = in.readUTF(); + mExisting.add(GLOBAL_METADATA_KEY); + } else { + Slog.e(TAG, "No global metadata in state file!"); + return; + } + + // The global metadata was first; now read all the apps + while (true) { + pkg = in.readUTF(); + int versionCode = in.readInt(); + mExisting.add(pkg); + mStateVersions.put(pkg, new Metadata(versionCode, null)); + } + } catch (EOFException eof) { + // safe; we're done + } catch (IOException e) { + // whoops, bad state file. abort. + Slog.e(TAG, "Unable to read Package Manager state file: " + e); + } + } + + // Util: write out our new backup state file + private void writeStateFile(List pkgs, ParcelFileDescriptor stateFile) { + FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor()); + DataOutputStream out = new DataOutputStream(outstream); + + try { + // by the time we get here we know we've stored the global metadata record + out.writeUTF(GLOBAL_METADATA_KEY); + out.writeInt(Build.VERSION.SDK_INT); + out.writeUTF(Build.VERSION.INCREMENTAL); + + // now write all the app names too + for (PackageInfo pkg : pkgs) { + out.writeUTF(pkg.packageName); + out.writeInt(pkg.versionCode); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to write package manager state file!"); + return; + } + } +} diff --git a/services/backup/java/com/android/server/backup/SystemBackupAgent.java b/services/backup/java/com/android/server/backup/SystemBackupAgent.java new file mode 100644 index 0000000..26e2e2a --- /dev/null +++ b/services/backup/java/com/android/server/backup/SystemBackupAgent.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + + +import android.app.IWallpaperManager; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupAgentHelper; +import android.app.backup.FullBackup; +import android.app.backup.FullBackupDataOutput; +import android.app.backup.WallpaperBackupHelper; +import android.content.Context; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; + +/** + * Backup agent for various system-managed data, currently just the system wallpaper + */ +public class SystemBackupAgent extends BackupAgentHelper { + private static final String TAG = "SystemBackupAgent"; + + // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME + // are also used in the full-backup file format, so must not change unless steps are + // taken to support the legacy backed-up datasets. + private static final String WALLPAPER_IMAGE_FILENAME = "wallpaper"; + private static final String WALLPAPER_INFO_FILENAME = "wallpaper_info.xml"; + + // TODO: Will need to change if backing up non-primary user's wallpaper + private static final String WALLPAPER_IMAGE_DIR = + Environment.getUserSystemDirectory(UserHandle.USER_OWNER).getAbsolutePath(); + private static final String WALLPAPER_IMAGE = WallpaperBackupHelper.WALLPAPER_IMAGE; + + // TODO: Will need to change if backing up non-primary user's wallpaper + private static final String WALLPAPER_INFO_DIR = + Environment.getUserSystemDirectory(UserHandle.USER_OWNER).getAbsolutePath(); + private static final String WALLPAPER_INFO = WallpaperBackupHelper.WALLPAPER_INFO; + // Use old keys to keep legacy data compatibility and avoid writing two wallpapers + private static final String WALLPAPER_IMAGE_KEY = WallpaperBackupHelper.WALLPAPER_IMAGE_KEY; + private static final String WALLPAPER_INFO_KEY = WallpaperBackupHelper.WALLPAPER_INFO_KEY; + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + // We only back up the data under the current "wallpaper" schema with metadata + IWallpaperManager wallpaper = (IWallpaperManager)ServiceManager.getService( + Context.WALLPAPER_SERVICE); + String[] files = new String[] { WALLPAPER_IMAGE, WALLPAPER_INFO }; + String[] keys = new String[] { WALLPAPER_IMAGE_KEY, WALLPAPER_INFO_KEY }; + if (wallpaper != null) { + try { + final String wallpaperName = wallpaper.getName(); + if (wallpaperName != null && wallpaperName.length() > 0) { + // When the wallpaper has a name, back up the info by itself. + // TODO: Don't rely on the innards of the service object like this! + // TODO: Send a delete for any stored wallpaper image in this case? + files = new String[] { WALLPAPER_INFO }; + keys = new String[] { WALLPAPER_INFO_KEY }; + } + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't get wallpaper name\n" + re); + } + } + addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, files, keys)); + super.onBackup(oldState, data, newState); + } + + @Override + public void onFullBackup(FullBackupDataOutput data) throws IOException { + // At present we back up only the wallpaper + fullWallpaperBackup(data); + } + + private void fullWallpaperBackup(FullBackupDataOutput output) { + // Back up the data files directly. We do them in this specific order -- + // info file followed by image -- because then we need take no special + // steps during restore; the restore will happen properly when the individual + // files are restored piecemeal. + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, + WALLPAPER_INFO_DIR, WALLPAPER_INFO, output.getData()); + FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, + WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output.getData()); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + // On restore, we also support a previous data schema "system_files" + addHelper("wallpaper", new WallpaperBackupHelper(SystemBackupAgent.this, + new String[] { WALLPAPER_IMAGE, WALLPAPER_INFO }, + new String[] { WALLPAPER_IMAGE_KEY, WALLPAPER_INFO_KEY} )); + addHelper("system_files", new WallpaperBackupHelper(SystemBackupAgent.this, + new String[] { WALLPAPER_IMAGE }, + new String[] { WALLPAPER_IMAGE_KEY} )); + + try { + super.onRestore(data, appVersionCode, newState); + + IWallpaperManager wallpaper = (IWallpaperManager) ServiceManager.getService( + Context.WALLPAPER_SERVICE); + if (wallpaper != null) { + try { + wallpaper.settingsRestored(); + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't restore settings\n" + re); + } + } + } catch (IOException ex) { + // If there was a failure, delete everything for the wallpaper, this is too aggressive, + // but this is hopefully a rare failure. + Slog.d(TAG, "restore failed", ex); + (new File(WALLPAPER_IMAGE)).delete(); + (new File(WALLPAPER_INFO)).delete(); + } + } + + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + Slog.i(TAG, "Restoring file domain=" + domain + " path=" + path); + + // Bits to indicate postprocessing we may need to perform + boolean restoredWallpaper = false; + + File outFile = null; + // Various domain+files we understand a priori + if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) { + if (path.equals(WALLPAPER_INFO_FILENAME)) { + outFile = new File(WALLPAPER_INFO); + restoredWallpaper = true; + } else if (path.equals(WALLPAPER_IMAGE_FILENAME)) { + outFile = new File(WALLPAPER_IMAGE); + restoredWallpaper = true; + } + } + + try { + if (outFile == null) { + Slog.w(TAG, "Skipping unrecognized system file: [ " + domain + " : " + path + " ]"); + } + FullBackup.restoreFile(data, size, type, mode, mtime, outFile); + + if (restoredWallpaper) { + IWallpaperManager wallpaper = + (IWallpaperManager)ServiceManager.getService( + Context.WALLPAPER_SERVICE); + if (wallpaper != null) { + try { + wallpaper.settingsRestored(); + } catch (RemoteException re) { + Slog.e(TAG, "Couldn't restore settings\n" + re); + } + } + } + } catch (IOException e) { + if (restoredWallpaper) { + // Make sure we wind up in a good state + (new File(WALLPAPER_IMAGE)).delete(); + (new File(WALLPAPER_INFO)).delete(); + } + } + } +} diff --git a/services/backup/java/service.mk b/services/backup/java/service.mk new file mode 100644 index 0000000..bd22b95 --- /dev/null +++ b/services/backup/java/service.mk @@ -0,0 +1,11 @@ +# Include only if the service is required +ifneq ($(findstring backup,$(REQUIRED_SERVICES)),) + +SUB_DIR := backup/java + +LOCAL_SRC_FILES += \ + $(call all-java-files-under,$(SUB_DIR)) + +#DEFINED_SERVICES += com.android.server.backup.BackupManagerService + +endif diff --git a/services/common_time/Android.mk b/services/common_time/Android.mk deleted file mode 100644 index 75eb528..0000000 --- a/services/common_time/Android.mk +++ /dev/null @@ -1,36 +0,0 @@ -LOCAL_PATH:= $(call my-dir) - -# -# common_time_service -# - -include $(CLEAR_VARS) - -LOCAL_SRC_FILES := \ - common_clock_service.cpp \ - common_time_config_service.cpp \ - common_time_server.cpp \ - common_time_server_api.cpp \ - common_time_server_packets.cpp \ - clock_recovery.cpp \ - common_clock.cpp \ - main.cpp \ - utils.cpp - -# Uncomment to enable vesbose logging and debug service. -#TIME_SERVICE_DEBUG=true -ifeq ($(TIME_SERVICE_DEBUG), true) -LOCAL_SRC_FILES += diag_thread.cpp -LOCAL_CFLAGS += -DTIME_SERVICE_DEBUG -endif - -LOCAL_SHARED_LIBRARIES := \ - libbinder \ - libcommon_time_client \ - libutils \ - liblog - -LOCAL_MODULE_TAGS := optional -LOCAL_MODULE := common_time - -include $(BUILD_EXECUTABLE) diff --git a/services/common_time/clock_recovery.cpp b/services/common_time/clock_recovery.cpp deleted file mode 100644 index 3a7c70c..0000000 --- a/services/common_time/clock_recovery.cpp +++ /dev/null @@ -1,423 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * A service that exchanges time synchronization information between - * a master that defines a timeline and clients that follow the timeline. - */ - -#define __STDC_LIMIT_MACROS -#define LOG_TAG "common_time" -#include -#include - -#include -#include - -#include "clock_recovery.h" -#include "common_clock.h" -#ifdef TIME_SERVICE_DEBUG -#include "diag_thread.h" -#endif - -// Define log macro so we can make LOGV into LOGE when we are exclusively -// debugging this code. -#ifdef TIME_SERVICE_DEBUG -#define LOG_TS ALOGE -#else -#define LOG_TS ALOGV -#endif - -namespace android { - -ClockRecoveryLoop::ClockRecoveryLoop(LocalClock* local_clock, - CommonClock* common_clock) { - assert(NULL != local_clock); - assert(NULL != common_clock); - - local_clock_ = local_clock; - common_clock_ = common_clock; - - local_clock_can_slew_ = local_clock_->initCheck() && - (local_clock_->setLocalSlew(0) == OK); - tgt_correction_ = 0; - cur_correction_ = 0; - - // Precompute the max rate at which we are allowed to change the VCXO - // control. - uint64_t N = 0x10000ull * 1000ull; - uint64_t D = local_clock_->getLocalFreq() * kMinFullRangeSlewChange_mSec; - LinearTransform::reduce(&N, &D); - while ((N > INT32_MAX) || (D > UINT32_MAX)) { - N >>= 1; - D >>= 1; - LinearTransform::reduce(&N, &D); - } - time_to_cur_slew_.a_to_b_numer = static_cast(N); - time_to_cur_slew_.a_to_b_denom = static_cast(D); - - reset(true, true); - -#ifdef TIME_SERVICE_DEBUG - diag_thread_ = new DiagThread(common_clock_, local_clock_); - if (diag_thread_ != NULL) { - status_t res = diag_thread_->startWorkThread(); - if (res != OK) - ALOGW("Failed to start A@H clock recovery diagnostic thread."); - } else - ALOGW("Failed to allocate diagnostic thread."); -#endif -} - -ClockRecoveryLoop::~ClockRecoveryLoop() { -#ifdef TIME_SERVICE_DEBUG - diag_thread_->stopWorkThread(); -#endif -} - -// Constants. -const float ClockRecoveryLoop::dT = 1.0; -const float ClockRecoveryLoop::Kc = 1.0f; -const float ClockRecoveryLoop::Ti = 15.0f; -const float ClockRecoveryLoop::Tf = 0.05; -const float ClockRecoveryLoop::bias_Fc = 0.01; -const float ClockRecoveryLoop::bias_RC = (dT / (2 * 3.14159f * bias_Fc)); -const float ClockRecoveryLoop::bias_Alpha = (dT / (bias_RC + dT)); -const int64_t ClockRecoveryLoop::panic_thresh_ = 50000; -const int64_t ClockRecoveryLoop::control_thresh_ = 10000; -const float ClockRecoveryLoop::COmin = -100.0f; -const float ClockRecoveryLoop::COmax = 100.0f; -const uint32_t ClockRecoveryLoop::kMinFullRangeSlewChange_mSec = 300; -const int ClockRecoveryLoop::kSlewChangeStepPeriod_mSec = 10; - - -void ClockRecoveryLoop::reset(bool position, bool frequency) { - Mutex::Autolock lock(&lock_); - reset_l(position, frequency); -} - -uint32_t ClockRecoveryLoop::findMinRTTNdx(DisciplineDataPoint* data, - uint32_t count) { - uint32_t min_rtt = 0; - for (uint32_t i = 1; i < count; ++i) - if (data[min_rtt].rtt > data[i].rtt) - min_rtt = i; - - return min_rtt; -} - -bool ClockRecoveryLoop::pushDisciplineEvent(int64_t local_time, - int64_t nominal_common_time, - int64_t rtt) { - Mutex::Autolock lock(&lock_); - - int64_t local_common_time = 0; - common_clock_->localToCommon(local_time, &local_common_time); - int64_t raw_delta = nominal_common_time - local_common_time; - -#ifdef TIME_SERVICE_DEBUG - ALOGE("local=%lld, common=%lld, delta=%lld, rtt=%lld\n", - local_common_time, nominal_common_time, - raw_delta, rtt); -#endif - - // If we have not defined a basis for common time, then we need to use these - // initial points to do so. In order to avoid significant initial error - // from a particularly bad startup data point, we collect the first N data - // points and choose the best of them before moving on. - if (!common_clock_->isValid()) { - if (startup_filter_wr_ < kStartupFilterSize) { - DisciplineDataPoint& d = startup_filter_data_[startup_filter_wr_]; - d.local_time = local_time; - d.nominal_common_time = nominal_common_time; - d.rtt = rtt; - startup_filter_wr_++; - } - - if (startup_filter_wr_ == kStartupFilterSize) { - uint32_t min_rtt = findMinRTTNdx(startup_filter_data_, - kStartupFilterSize); - - common_clock_->setBasis( - startup_filter_data_[min_rtt].local_time, - startup_filter_data_[min_rtt].nominal_common_time); - } - - return true; - } - - int64_t observed_common; - int64_t delta; - float delta_f, dCO; - int32_t tgt_correction; - - if (OK != common_clock_->localToCommon(local_time, &observed_common)) { - // Since we just checked to make certain that this conversion was valid, - // and no one else in the system should be messing with it, if this - // conversion is suddenly invalid, it is a good reason to panic. - ALOGE("Failed to convert local time to common time in %s:%d", - __PRETTY_FUNCTION__, __LINE__); - return false; - } - - // Implement a filter which should match NTP filtering behavior when a - // client is associated with only one peer of lower stratum. Basically, - // always use the best of the N last data points, where best is defined as - // lowest round trip time. NTP uses an N of 8; we use a value of 6. - // - // TODO(johngro) : experiment with other filter strategies. The goal here - // is to mitigate the effects of high RTT data points which typically have - // large asymmetries in the TX/RX legs. Downside of the existing NTP - // approach (particularly because of the PID controller we are using to - // produce the control signal from the filtered data) are that the rate at - // which discipline events are actually acted upon becomes irregular and can - // become drawn out (the time between actionable event can go way up). If - // the system receives a strong high quality data point, the proportional - // component of the controller can produce a strong correction which is left - // in place for too long causing overshoot. In addition, the integral - // component of the system currently is an approximation based on the - // assumption of a more or less homogeneous sampling of the error. Its - // unclear what the effect of undermining this assumption would be right - // now. - - // Two ideas which come to mind immediately would be to... - // 1) Keep a history of more data points (32 or so) and ignore data points - // whose RTT is more than a certain number of standard deviations outside - // of the norm. - // 2) Eliminate the PID controller portion of this system entirely. - // Instead, move to a system which uses a very wide filter (128 data - // points or more) with a sum-of-least-squares line fitting approach to - // tracking the long term drift. This would take the place of the I - // component in the current PID controller. Also use a much more narrow - // outlier-rejector filter (as described in #1) to drive a short term - // correction factor similar to the P component of the PID controller. - assert(filter_wr_ < kFilterSize); - filter_data_[filter_wr_].local_time = local_time; - filter_data_[filter_wr_].observed_common_time = observed_common; - filter_data_[filter_wr_].nominal_common_time = nominal_common_time; - filter_data_[filter_wr_].rtt = rtt; - filter_data_[filter_wr_].point_used = false; - uint32_t current_point = filter_wr_; - filter_wr_ = (filter_wr_ + 1) % kFilterSize; - if (!filter_wr_) - filter_full_ = true; - - uint32_t scan_end = filter_full_ ? kFilterSize : filter_wr_; - uint32_t min_rtt = findMinRTTNdx(filter_data_, scan_end); - // We only use packets with low RTTs for control. If the packet RTT - // is less than the panic threshold, we can probably eat the jitter with the - // control loop. Otherwise, take the packet only if it better than all - // of the packets we have in the history. That way we try to track - // something, even if it is noisy. - if (current_point == min_rtt || rtt < control_thresh_) { - delta_f = delta = nominal_common_time - observed_common; - - last_error_est_valid_ = true; - last_error_est_usec_ = delta; - - // Compute the error then clamp to the panic threshold. If we ever - // exceed this amt of error, its time to panic and reset the system. - // Given that the error in the measurement of the error could be as - // high as the RTT of the data point, we don't actually panic until - // the implied error (delta) is greater than the absolute panic - // threashold plus the RTT. IOW - we don't panic until we are - // absoluely sure that our best case sync is worse than the absolute - // panic threshold. - int64_t effective_panic_thresh = panic_thresh_ + rtt; - if ((delta > effective_panic_thresh) || - (delta < -effective_panic_thresh)) { - // PANIC!!! - reset_l(false, true); - return false; - } - - } else { - // We do not have a good packet to look at, but we also do not want to - // free-run the clock at some crazy slew rate. So we guess the - // trajectory of the clock based on the last controller output and the - // estimated bias of our clock against the master. - // The net effect of this is that CO == CObias after some extended - // period of no feedback. - delta_f = last_delta_f_ - dT*(CO - CObias); - delta = delta_f; - } - - // Velocity form PI control equation. - dCO = Kc * (1.0f + dT/Ti) * delta_f - Kc * last_delta_f_; - CO += dCO * Tf; // Filter CO by applying gain <1 here. - - // Save error terms for later. - last_delta_f_ = delta_f; - - // Clamp CO to +/- 100ppm. - if (CO < COmin) - CO = COmin; - else if (CO > COmax) - CO = COmax; - - // Update the controller bias. - CObias = bias_Alpha * CO + (1.0f - bias_Alpha) * lastCObias; - lastCObias = CObias; - - // Convert PPM to 16-bit int range. Add some guard band (-0.01) so we - // don't get fp weirdness. - tgt_correction = CO * 327.66; - - // If there was a change in the amt of correction to use, update the - // system. - setTargetCorrection_l(tgt_correction); - - LOG_TS("clock_loop %lld %f %f %f %d\n", raw_delta, delta_f, CO, CObias, tgt_correction); - -#ifdef TIME_SERVICE_DEBUG - diag_thread_->pushDisciplineEvent( - local_time, - observed_common, - nominal_common_time, - tgt_correction, - rtt); -#endif - - return true; -} - -int32_t ClockRecoveryLoop::getLastErrorEstimate() { - Mutex::Autolock lock(&lock_); - - if (last_error_est_valid_) - return last_error_est_usec_; - else - return ICommonClock::kErrorEstimateUnknown; -} - -void ClockRecoveryLoop::reset_l(bool position, bool frequency) { - assert(NULL != common_clock_); - - if (position) { - common_clock_->resetBasis(); - startup_filter_wr_ = 0; - } - - if (frequency) { - last_error_est_valid_ = false; - last_error_est_usec_ = 0; - last_delta_f_ = 0.0; - CO = 0.0f; - lastCObias = CObias = 0.0f; - setTargetCorrection_l(0); - applySlew_l(); - } - - filter_wr_ = 0; - filter_full_ = false; -} - -void ClockRecoveryLoop::setTargetCorrection_l(int32_t tgt) { - // When we make a change to the slew rate, we need to be careful to not - // change it too quickly as it can anger some HDMI sinks out there, notably - // some Sony panels from the 2010-2011 timeframe. From experimenting with - // some of these sinks, it seems like swinging from one end of the range to - // another in less that 190mSec or so can start to cause trouble. Adding in - // a hefty margin, we limit the system to a full range sweep in no less than - // 300mSec. - if (tgt_correction_ != tgt) { - int64_t now = local_clock_->getLocalTime(); - status_t res; - - tgt_correction_ = tgt; - - // Set up the transformation to figure out what the slew should be at - // any given point in time in the future. - time_to_cur_slew_.a_zero = now; - time_to_cur_slew_.b_zero = cur_correction_; - - // Make sure the sign of the slope is headed in the proper direction. - bool needs_increase = (cur_correction_ < tgt_correction_); - bool is_increasing = (time_to_cur_slew_.a_to_b_numer > 0); - if (( needs_increase && !is_increasing) || - (!needs_increase && is_increasing)) { - time_to_cur_slew_.a_to_b_numer = -time_to_cur_slew_.a_to_b_numer; - } - - // Finally, figure out when the change will be finished and start the - // slew operation. - time_to_cur_slew_.doReverseTransform(tgt_correction_, - &slew_change_end_time_); - - applySlew_l(); - } -} - -bool ClockRecoveryLoop::applySlew_l() { - bool ret = true; - - // If cur == tgt, there is no ongoing sleq rate change and we are already - // finished. - if (cur_correction_ == tgt_correction_) - goto bailout; - - if (local_clock_can_slew_) { - int64_t now = local_clock_->getLocalTime(); - int64_t tmp; - - if (now >= slew_change_end_time_) { - cur_correction_ = tgt_correction_; - next_slew_change_timeout_.setTimeout(-1); - } else { - time_to_cur_slew_.doForwardTransform(now, &tmp); - - if (tmp > INT16_MAX) - cur_correction_ = INT16_MAX; - else if (tmp < INT16_MIN) - cur_correction_ = INT16_MIN; - else - cur_correction_ = static_cast(tmp); - - next_slew_change_timeout_.setTimeout(kSlewChangeStepPeriod_mSec); - ret = false; - } - - local_clock_->setLocalSlew(cur_correction_); - } else { - // Since we are not actually changing the rate of a HW clock, we don't - // need to worry to much about changing the slew rate so fast that we - // anger any downstream HDMI devices. - cur_correction_ = tgt_correction_; - next_slew_change_timeout_.setTimeout(-1); - - // The SW clock recovery implemented by the common clock class expects - // values expressed in PPM. CO is in ppm. - common_clock_->setSlew(local_clock_->getLocalTime(), CO); - } - -bailout: - return ret; -} - -int ClockRecoveryLoop::applyRateLimitedSlew() { - Mutex::Autolock lock(&lock_); - - int ret = next_slew_change_timeout_.msecTillTimeout(); - if (!ret) { - if (applySlew_l()) - next_slew_change_timeout_.setTimeout(-1); - ret = next_slew_change_timeout_.msecTillTimeout(); - } - - return ret; -} - -} // namespace android diff --git a/services/common_time/clock_recovery.h b/services/common_time/clock_recovery.h deleted file mode 100644 index b6c87ff..0000000 --- a/services/common_time/clock_recovery.h +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __CLOCK_RECOVERY_H__ -#define __CLOCK_RECOVERY_H__ - -#include -#include -#include -#include - -#ifdef TIME_SERVICE_DEBUG -#include "diag_thread.h" -#endif - -#include "utils.h" - -namespace android { - -class CommonClock; -class LocalClock; - -class ClockRecoveryLoop { - public: - ClockRecoveryLoop(LocalClock* local_clock, CommonClock* common_clock); - ~ClockRecoveryLoop(); - - void reset(bool position, bool frequency); - bool pushDisciplineEvent(int64_t local_time, - int64_t nominal_common_time, - int64_t data_point_rtt); - int32_t getLastErrorEstimate(); - - // Applies the next step in any ongoing slew change operation. Returns a - // timeout suitable for use with poll/select indicating the number of mSec - // until the next change should be applied. - int applyRateLimitedSlew(); - - private: - - // Tuned using the "Good Gain" method. - // See: - // http://techteach.no/publications/books/dynamics_and_control/tuning_pid_controller.pdf - - // Controller period (1Hz for now). - static const float dT; - - // Controller gain, positive and unitless. Larger values converge faster, - // but can cause instability. - static const float Kc; - - // Integral reset time. Smaller values cause loop to track faster, but can - // also cause instability. - static const float Ti; - - // Controller output filter time constant. Range (0-1). Smaller values make - // output smoother, but slow convergence. - static const float Tf; - - // Low-pass filter for bias tracker. - static const float bias_Fc; // HZ - static const float bias_RC; // Computed in constructor. - static const float bias_Alpha; // Computed inconstructor. - - // The maximum allowed error (as indicated by a pushDisciplineEvent) before - // we panic. - static const int64_t panic_thresh_; - - // The maximum allowed error rtt time for packets to be used for control - // feedback, unless the packet is the best in recent memory. - static const int64_t control_thresh_; - - typedef struct { - int64_t local_time; - int64_t observed_common_time; - int64_t nominal_common_time; - int64_t rtt; - bool point_used; - } DisciplineDataPoint; - - static uint32_t findMinRTTNdx(DisciplineDataPoint* data, uint32_t count); - - void reset_l(bool position, bool frequency); - void setTargetCorrection_l(int32_t tgt); - bool applySlew_l(); - - // The local clock HW abstraction we use as the basis for common time. - LocalClock* local_clock_; - bool local_clock_can_slew_; - - // The common clock we end up controlling along with the lock used to - // serialize operations. - CommonClock* common_clock_; - Mutex lock_; - - // parameters maintained while running and reset during a reset - // of the frequency correction. - bool last_error_est_valid_; - int32_t last_error_est_usec_; - float last_delta_f_; - int32_t integrated_error_; - int32_t tgt_correction_; - int32_t cur_correction_; - LinearTransform time_to_cur_slew_; - int64_t slew_change_end_time_; - Timeout next_slew_change_timeout_; - - // Contoller Output. - float CO; - - // Bias tracking for trajectory estimation. - float CObias; - float lastCObias; - - // Controller output bounds. The controller will not try to - // slew faster that +/-100ppm offset from center per interation. - static const float COmin; - static const float COmax; - - // State kept for filtering the discipline data. - static const uint32_t kFilterSize = 16; - DisciplineDataPoint filter_data_[kFilterSize]; - uint32_t filter_wr_; - bool filter_full_; - - static const uint32_t kStartupFilterSize = 4; - DisciplineDataPoint startup_filter_data_[kStartupFilterSize]; - uint32_t startup_filter_wr_; - - // Minimum number of milliseconds over which we allow a full range change - // (from rail to rail) of the VCXO control signal. This is the rate - // limiting factor which keeps us from changing the clock rate so fast that - // we get in trouble with certain HDMI sinks. - static const uint32_t kMinFullRangeSlewChange_mSec; - - // How much time (in msec) to wait - static const int kSlewChangeStepPeriod_mSec; - -#ifdef TIME_SERVICE_DEBUG - sp diag_thread_; -#endif -}; - -} // namespace android - -#endif // __CLOCK_RECOVERY_H__ diff --git a/services/common_time/common_clock.cpp b/services/common_time/common_clock.cpp deleted file mode 100644 index c9eb388..0000000 --- a/services/common_time/common_clock.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#define __STDC_LIMIT_MACROS - -#define LOG_TAG "common_time" -#include - -#include - -#include -#include - -#include "common_clock.h" - -namespace android { - -CommonClock::CommonClock() { - cur_slew_ = 0; - cur_trans_valid_ = false; - - cur_trans_.a_zero = 0; - cur_trans_.b_zero = 0; - cur_trans_.a_to_b_numer = local_to_common_freq_numer_ = 1; - cur_trans_.a_to_b_denom = local_to_common_freq_denom_ = 1; - duration_trans_ = cur_trans_; -} - -bool CommonClock::init(uint64_t local_freq) { - Mutex::Autolock lock(&lock_); - - if (!local_freq) - return false; - - uint64_t numer = kCommonFreq; - uint64_t denom = local_freq; - - LinearTransform::reduce(&numer, &denom); - if ((numer > UINT32_MAX) || (denom > UINT32_MAX)) { - ALOGE("Overflow in CommonClock::init while trying to reduce %lld/%lld", - kCommonFreq, local_freq); - return false; - } - - cur_trans_.a_to_b_numer = local_to_common_freq_numer_ = - static_cast(numer); - cur_trans_.a_to_b_denom = local_to_common_freq_denom_ = - static_cast(denom); - duration_trans_ = cur_trans_; - - return true; -} - -status_t CommonClock::localToCommon(int64_t local, int64_t *common_out) const { - Mutex::Autolock lock(&lock_); - - if (!cur_trans_valid_) - return INVALID_OPERATION; - - if (!cur_trans_.doForwardTransform(local, common_out)) - return INVALID_OPERATION; - - return OK; -} - -status_t CommonClock::commonToLocal(int64_t common, int64_t *local_out) const { - Mutex::Autolock lock(&lock_); - - if (!cur_trans_valid_) - return INVALID_OPERATION; - - if (!cur_trans_.doReverseTransform(common, local_out)) - return INVALID_OPERATION; - - return OK; -} - -int64_t CommonClock::localDurationToCommonDuration(int64_t localDur) const { - int64_t ret; - duration_trans_.doForwardTransform(localDur, &ret); - return ret; -} - -void CommonClock::setBasis(int64_t local, int64_t common) { - Mutex::Autolock lock(&lock_); - - cur_trans_.a_zero = local; - cur_trans_.b_zero = common; - cur_trans_valid_ = true; -} - -void CommonClock::resetBasis() { - Mutex::Autolock lock(&lock_); - - cur_trans_.a_zero = 0; - cur_trans_.b_zero = 0; - cur_trans_valid_ = false; -} - -status_t CommonClock::setSlew(int64_t change_time, int32_t ppm) { - Mutex::Autolock lock(&lock_); - - int64_t new_local_basis; - int64_t new_common_basis; - - if (cur_trans_valid_) { - new_local_basis = change_time; - if (!cur_trans_.doForwardTransform(change_time, &new_common_basis)) { - ALOGE("Overflow when attempting to set slew rate to %d", ppm); - return INVALID_OPERATION; - } - } else { - new_local_basis = 0; - new_common_basis = 0; - } - - cur_slew_ = ppm; - uint32_t n1 = local_to_common_freq_numer_; - uint32_t n2 = 1000000 + cur_slew_; - - uint32_t d1 = local_to_common_freq_denom_; - uint32_t d2 = 1000000; - - // n1/d1 has already been reduced, no need to do so here. - LinearTransform::reduce(&n1, &d2); - LinearTransform::reduce(&n2, &d1); - LinearTransform::reduce(&n2, &d2); - - cur_trans_.a_zero = new_local_basis; - cur_trans_.b_zero = new_common_basis; - cur_trans_.a_to_b_numer = n1 * n2; - cur_trans_.a_to_b_denom = d1 * d2; - - return OK; -} - -} // namespace android diff --git a/services/common_time/common_clock.h b/services/common_time/common_clock.h deleted file mode 100644 index b786fdc..0000000 --- a/services/common_time/common_clock.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __COMMON_CLOCK_H__ -#define __COMMON_CLOCK_H__ - -#include - -#include -#include -#include - -namespace android { - -class CommonClock { - public: - CommonClock(); - - bool init(uint64_t local_freq); - - status_t localToCommon(int64_t local, int64_t *common_out) const; - status_t commonToLocal(int64_t common, int64_t *local_out) const; - int64_t localDurationToCommonDuration(int64_t localDur) const; - uint64_t getCommonFreq() const { return kCommonFreq; } - bool isValid() const { return cur_trans_valid_; } - status_t setSlew(int64_t change_time, int32_t ppm); - void setBasis(int64_t local, int64_t common); - void resetBasis(); - private: - mutable Mutex lock_; - - int32_t cur_slew_; - uint32_t local_to_common_freq_numer_; - uint32_t local_to_common_freq_denom_; - - LinearTransform duration_trans_; - LinearTransform cur_trans_; - bool cur_trans_valid_; - - static const uint64_t kCommonFreq = 1000000ull; -}; - -} // namespace android -#endif // __COMMON_CLOCK_H__ diff --git a/services/common_time/common_clock_service.cpp b/services/common_time/common_clock_service.cpp deleted file mode 100644 index 9ca6f35..0000000 --- a/services/common_time/common_clock_service.cpp +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include "common_clock_service.h" -#include "common_clock.h" -#include "common_time_server.h" - -namespace android { - -sp CommonClockService::instantiate( - CommonTimeServer& timeServer) { - sp tcc = new CommonClockService(timeServer); - if (tcc == NULL) - return NULL; - - defaultServiceManager()->addService(ICommonClock::kServiceName, tcc); - return tcc; -} - -status_t CommonClockService::dump(int fd, const Vector& args) { - Mutex::Autolock lock(mRegistrationLock); - return mTimeServer.dumpClockInterface(fd, args, mListeners.size()); -} - -status_t CommonClockService::isCommonTimeValid(bool* valid, - uint32_t* timelineID) { - return mTimeServer.isCommonTimeValid(valid, timelineID); -} - -status_t CommonClockService::commonTimeToLocalTime(int64_t commonTime, - int64_t* localTime) { - return mTimeServer.getCommonClock().commonToLocal(commonTime, localTime); -} - -status_t CommonClockService::localTimeToCommonTime(int64_t localTime, - int64_t* commonTime) { - return mTimeServer.getCommonClock().localToCommon(localTime, commonTime); -} - -status_t CommonClockService::getCommonTime(int64_t* commonTime) { - return localTimeToCommonTime(mTimeServer.getLocalClock().getLocalTime(), commonTime); -} - -status_t CommonClockService::getCommonFreq(uint64_t* freq) { - *freq = mTimeServer.getCommonClock().getCommonFreq(); - return OK; -} - -status_t CommonClockService::getLocalTime(int64_t* localTime) { - *localTime = mTimeServer.getLocalClock().getLocalTime(); - return OK; -} - -status_t CommonClockService::getLocalFreq(uint64_t* freq) { - *freq = mTimeServer.getLocalClock().getLocalFreq(); - return OK; -} - -status_t CommonClockService::getEstimatedError(int32_t* estimate) { - *estimate = mTimeServer.getEstimatedError(); - return OK; -} - -status_t CommonClockService::getTimelineID(uint64_t* id) { - *id = mTimeServer.getTimelineID(); - return OK; -} - -status_t CommonClockService::getState(State* state) { - *state = mTimeServer.getState(); - return OK; -} - -status_t CommonClockService::getMasterAddr(struct sockaddr_storage* addr) { - return mTimeServer.getMasterAddr(addr); -} - -status_t CommonClockService::registerListener( - const sp& listener) { - Mutex::Autolock lock(mRegistrationLock); - - { // scoping for autolock pattern - Mutex::Autolock lock(mCallbackLock); - // check whether this is a duplicate - for (size_t i = 0; i < mListeners.size(); i++) { - if (mListeners[i]->asBinder() == listener->asBinder()) - return ALREADY_EXISTS; - } - } - - mListeners.add(listener); - mTimeServer.reevaluateAutoDisableState(0 != mListeners.size()); - return listener->asBinder()->linkToDeath(this); -} - -status_t CommonClockService::unregisterListener( - const sp& listener) { - Mutex::Autolock lock(mRegistrationLock); - status_t ret_val = NAME_NOT_FOUND; - - { // scoping for autolock pattern - Mutex::Autolock lock(mCallbackLock); - for (size_t i = 0; i < mListeners.size(); i++) { - if (mListeners[i]->asBinder() == listener->asBinder()) { - mListeners[i]->asBinder()->unlinkToDeath(this); - mListeners.removeAt(i); - ret_val = OK; - break; - } - } - } - - mTimeServer.reevaluateAutoDisableState(0 != mListeners.size()); - return ret_val; -} - -void CommonClockService::binderDied(const wp& who) { - Mutex::Autolock lock(mRegistrationLock); - - { // scoping for autolock pattern - Mutex::Autolock lock(mCallbackLock); - for (size_t i = 0; i < mListeners.size(); i++) { - if (mListeners[i]->asBinder() == who) { - mListeners.removeAt(i); - break; - } - } - } - - mTimeServer.reevaluateAutoDisableState(0 != mListeners.size()); -} - -void CommonClockService::notifyOnTimelineChanged(uint64_t timelineID) { - Mutex::Autolock lock(mCallbackLock); - - for (size_t i = 0; i < mListeners.size(); i++) { - mListeners[i]->onTimelineChanged(timelineID); - } -} - -}; // namespace android diff --git a/services/common_time/common_clock_service.h b/services/common_time/common_clock_service.h deleted file mode 100644 index bd663f0..0000000 --- a/services/common_time/common_clock_service.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_COMMON_CLOCK_SERVICE_H -#define ANDROID_COMMON_CLOCK_SERVICE_H - -#include -#include - -namespace android { - -class CommonTimeServer; - -class CommonClockService : public BnCommonClock, - public android::IBinder::DeathRecipient { - public: - static sp instantiate(CommonTimeServer& timeServer); - - virtual status_t dump(int fd, const Vector& args); - - virtual status_t isCommonTimeValid(bool* valid, uint32_t *timelineID); - virtual status_t commonTimeToLocalTime(int64_t common_time, - int64_t* local_time); - virtual status_t localTimeToCommonTime(int64_t local_time, - int64_t* common_time); - virtual status_t getCommonTime(int64_t* common_time); - virtual status_t getCommonFreq(uint64_t* freq); - virtual status_t getLocalTime(int64_t* local_time); - virtual status_t getLocalFreq(uint64_t* freq); - virtual status_t getEstimatedError(int32_t* estimate); - virtual status_t getTimelineID(uint64_t* id); - virtual status_t getState(ICommonClock::State* state); - virtual status_t getMasterAddr(struct sockaddr_storage* addr); - - virtual status_t registerListener( - const sp& listener); - virtual status_t unregisterListener( - const sp& listener); - - void notifyOnTimelineChanged(uint64_t timelineID); - - private: - CommonClockService(CommonTimeServer& timeServer) - : mTimeServer(timeServer) { }; - - virtual void binderDied(const wp& who); - - CommonTimeServer& mTimeServer; - - // locks used to synchronize access to the list of registered listeners. - // The callback lock is held whenever the list is used to perform callbacks - // or while the list is being modified. The registration lock used to - // serialize access across registerListener, unregisterListener, and - // binderDied. - // - // The reason for two locks is that registerListener, unregisterListener, - // and binderDied each call into the core service and obtain the core - // service thread lock when they call reevaluateAutoDisableState. The core - // service thread obtains the main thread lock whenever its thread is - // running, and sometimes needs to call notifyOnTimelineChanged which then - // obtains the callback lock. If callers of registration functions were - // holding the callback lock when they called into the core service, we - // would have a classic A/B, B/A ordering deadlock. To avoid this, the - // registration functions hold the registration lock for the duration of - // their call, but hold the callback lock only while they mutate the list. - // This way, the list's size cannot change (because of the registration - // lock) during the call into reevaluateAutoDisableState, but the core work - // thread can still safely call notifyOnTimelineChanged while holding the - // main thread lock. - Mutex mCallbackLock; - Mutex mRegistrationLock; - - Vector > mListeners; -}; - -}; // namespace android - -#endif // ANDROID_COMMON_CLOCK_SERVICE_H diff --git a/services/common_time/common_time_config_service.cpp b/services/common_time/common_time_config_service.cpp deleted file mode 100644 index 9585618..0000000 --- a/services/common_time/common_time_config_service.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include "common_time_config_service.h" -#include "common_time_server.h" - -namespace android { - -sp CommonTimeConfigService::instantiate( - CommonTimeServer& timeServer) { - sp ctcs = new CommonTimeConfigService(timeServer); - if (ctcs == NULL) - return NULL; - - defaultServiceManager()->addService(ICommonTimeConfig::kServiceName, ctcs); - return ctcs; -} - -status_t CommonTimeConfigService::dump(int fd, const Vector& args) { - return mTimeServer.dumpConfigInterface(fd, args); -} - -status_t CommonTimeConfigService::getMasterElectionPriority(uint8_t *priority) { - return mTimeServer.getMasterElectionPriority(priority); -} - -status_t CommonTimeConfigService::setMasterElectionPriority(uint8_t priority) { - return mTimeServer.setMasterElectionPriority(priority); -} - -status_t CommonTimeConfigService::getMasterElectionEndpoint( - struct sockaddr_storage *addr) { - return mTimeServer.getMasterElectionEndpoint(addr); -} - -status_t CommonTimeConfigService::setMasterElectionEndpoint( - const struct sockaddr_storage *addr) { - return mTimeServer.setMasterElectionEndpoint(addr); -} - -status_t CommonTimeConfigService::getMasterElectionGroupId(uint64_t *id) { - return mTimeServer.getMasterElectionGroupId(id); -} - -status_t CommonTimeConfigService::setMasterElectionGroupId(uint64_t id) { - return mTimeServer.setMasterElectionGroupId(id); -} - -status_t CommonTimeConfigService::getInterfaceBinding(String16& ifaceName) { - String8 tmp; - status_t ret = mTimeServer.getInterfaceBinding(tmp); - ifaceName = String16(tmp); - return ret; -} - -status_t CommonTimeConfigService::setInterfaceBinding(const String16& ifaceName) { - String8 tmp(ifaceName); - return mTimeServer.setInterfaceBinding(tmp); -} - -status_t CommonTimeConfigService::getMasterAnnounceInterval(int *interval) { - return mTimeServer.getMasterAnnounceInterval(interval); -} - -status_t CommonTimeConfigService::setMasterAnnounceInterval(int interval) { - return mTimeServer.setMasterAnnounceInterval(interval); -} - -status_t CommonTimeConfigService::getClientSyncInterval(int *interval) { - return mTimeServer.getClientSyncInterval(interval); -} - -status_t CommonTimeConfigService::setClientSyncInterval(int interval) { - return mTimeServer.setClientSyncInterval(interval); -} - -status_t CommonTimeConfigService::getPanicThreshold(int *threshold) { - return mTimeServer.getPanicThreshold(threshold); -} - -status_t CommonTimeConfigService::setPanicThreshold(int threshold) { - return mTimeServer.setPanicThreshold(threshold); -} - -status_t CommonTimeConfigService::getAutoDisable(bool *autoDisable) { - return mTimeServer.getAutoDisable(autoDisable); -} - -status_t CommonTimeConfigService::setAutoDisable(bool autoDisable) { - return mTimeServer.setAutoDisable(autoDisable); -} - -status_t CommonTimeConfigService::forceNetworklessMasterMode() { - return mTimeServer.forceNetworklessMasterMode(); -} - -}; // namespace android diff --git a/services/common_time/common_time_config_service.h b/services/common_time/common_time_config_service.h deleted file mode 100644 index 89806dd..0000000 --- a/services/common_time/common_time_config_service.h +++ /dev/null @@ -1,60 +0,0 @@ -/* * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_COMMON_TIME_CONFIG_SERVICE_H -#define ANDROID_COMMON_TIME_CONFIG_SERVICE_H - -#include -#include - -namespace android { - -class String16; -class CommonTimeServer; - -class CommonTimeConfigService : public BnCommonTimeConfig { - public: - static sp instantiate(CommonTimeServer& timeServer); - - virtual status_t dump(int fd, const Vector& args); - - virtual status_t getMasterElectionPriority(uint8_t *priority); - virtual status_t setMasterElectionPriority(uint8_t priority); - virtual status_t getMasterElectionEndpoint(struct sockaddr_storage *addr); - virtual status_t setMasterElectionEndpoint(const struct sockaddr_storage *addr); - virtual status_t getMasterElectionGroupId(uint64_t *id); - virtual status_t setMasterElectionGroupId(uint64_t id); - virtual status_t getInterfaceBinding(String16& ifaceName); - virtual status_t setInterfaceBinding(const String16& ifaceName); - virtual status_t getMasterAnnounceInterval(int *interval); - virtual status_t setMasterAnnounceInterval(int interval); - virtual status_t getClientSyncInterval(int *interval); - virtual status_t setClientSyncInterval(int interval); - virtual status_t getPanicThreshold(int *threshold); - virtual status_t setPanicThreshold(int threshold); - virtual status_t getAutoDisable(bool *autoDisable); - virtual status_t setAutoDisable(bool autoDisable); - virtual status_t forceNetworklessMasterMode(); - - private: - CommonTimeConfigService(CommonTimeServer& timeServer) - : mTimeServer(timeServer) { } - CommonTimeServer& mTimeServer; - -}; - -}; // namespace android - -#endif // ANDROID_COMMON_TIME_CONFIG_SERVICE_H diff --git a/services/common_time/common_time_server.cpp b/services/common_time/common_time_server.cpp deleted file mode 100644 index 21e706f..0000000 --- a/services/common_time/common_time_server.cpp +++ /dev/null @@ -1,1506 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * A service that exchanges time synchronization information between - * a master that defines a timeline and clients that follow the timeline. - */ - -#define LOG_TAG "common_time" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "common_clock_service.h" -#include "common_time_config_service.h" -#include "common_time_server.h" -#include "common_time_server_packets.h" -#include "clock_recovery.h" -#include "common_clock.h" - -#define MAX_INT ((int)0x7FFFFFFF) - -namespace android { - -const char* CommonTimeServer::kDefaultMasterElectionAddr = "255.255.255.255"; -const uint16_t CommonTimeServer::kDefaultMasterElectionPort = 8886; -const uint64_t CommonTimeServer::kDefaultSyncGroupID = 1; -const uint8_t CommonTimeServer::kDefaultMasterPriority = 1; -const uint32_t CommonTimeServer::kDefaultMasterAnnounceIntervalMs = 10000; -const uint32_t CommonTimeServer::kDefaultSyncRequestIntervalMs = 1000; -const uint32_t CommonTimeServer::kDefaultPanicThresholdUsec = 50000; -const bool CommonTimeServer::kDefaultAutoDisable = true; -const int CommonTimeServer::kSetupRetryTimeoutMs = 30000; -const int64_t CommonTimeServer::kNoGoodDataPanicThresholdUsec = 600000000ll; -const uint32_t CommonTimeServer::kRTTDiscardPanicThreshMultiplier = 5; - -// timeout value representing an infinite timeout -const int CommonTimeServer::kInfiniteTimeout = -1; - -/*** Initial state constants ***/ - -// number of WhoIsMaster attempts sent before giving up -const int CommonTimeServer::kInitial_NumWhoIsMasterRetries = 6; - -// timeout used when waiting for a response to a WhoIsMaster request -const int CommonTimeServer::kInitial_WhoIsMasterTimeoutMs = 500; - -/*** Client state constants ***/ - -// number of sync requests that can fail before a client assumes its master -// is dead -const int CommonTimeServer::kClient_NumSyncRequestRetries = 10; - -/*** Master state constants ***/ - -/*** Ronin state constants ***/ - -// number of WhoIsMaster attempts sent before declaring ourselves master -const int CommonTimeServer::kRonin_NumWhoIsMasterRetries = 20; - -// timeout used when waiting for a response to a WhoIsMaster request -const int CommonTimeServer::kRonin_WhoIsMasterTimeoutMs = 500; - -/*** WaitForElection state constants ***/ - -// how long do we wait for an announcement from a master before -// trying another election? -const int CommonTimeServer::kWaitForElection_TimeoutMs = 12500; - -CommonTimeServer::CommonTimeServer() - : Thread(false) - , mState(ICommonClock::STATE_INITIAL) - , mClockRecovery(&mLocalClock, &mCommonClock) - , mSocket(-1) - , mLastPacketRxLocalTime(0) - , mTimelineID(ICommonClock::kInvalidTimelineID) - , mClockSynced(false) - , mCommonClockHasClients(false) - , mStateChangeLog("Recent State Change Events", 30) - , mElectionLog("Recent Master Election Traffic", 30) - , mBadPktLog("Recent Bad Packet RX Info", 8) - , mInitial_WhoIsMasterRequestTimeouts(0) - , mClient_MasterDeviceID(0) - , mClient_MasterDevicePriority(0) - , mRonin_WhoIsMasterRequestTimeouts(0) { - // zero out sync stats - resetSyncStats(); - - // Setup the master election endpoint to use the default. - struct sockaddr_in* meep = - reinterpret_cast(&mMasterElectionEP); - memset(&mMasterElectionEP, 0, sizeof(mMasterElectionEP)); - inet_aton(kDefaultMasterElectionAddr, &meep->sin_addr); - meep->sin_family = AF_INET; - meep->sin_port = htons(kDefaultMasterElectionPort); - - // Zero out the master endpoint. - memset(&mMasterEP, 0, sizeof(mMasterEP)); - mMasterEPValid = false; - mBindIfaceValid = false; - setForceLowPriority(false); - - // Set all remaining configuration parameters to their defaults. - mDeviceID = 0; - mSyncGroupID = kDefaultSyncGroupID; - mMasterPriority = kDefaultMasterPriority; - mMasterAnnounceIntervalMs = kDefaultMasterAnnounceIntervalMs; - mSyncRequestIntervalMs = kDefaultSyncRequestIntervalMs; - mPanicThresholdUsec = kDefaultPanicThresholdUsec; - mAutoDisable = kDefaultAutoDisable; - - // Create the eventfd we will use to signal our thread to wake up when - // needed. - mWakeupThreadFD = eventfd(0, EFD_NONBLOCK); - - // seed the random number generator (used to generated timeline IDs) - srand48(static_cast(systemTime())); -} - -CommonTimeServer::~CommonTimeServer() { - shutdownThread(); - - // No need to grab the lock here. We are in the destructor; if the the user - // has a thread in any of the APIs while the destructor is being called, - // there is a threading problem a the application level we cannot reasonably - // do anything about. - cleanupSocket_l(); - - if (mWakeupThreadFD >= 0) { - close(mWakeupThreadFD); - mWakeupThreadFD = -1; - } -} - -bool CommonTimeServer::startServices() { - // start the ICommonClock service - mICommonClock = CommonClockService::instantiate(*this); - if (mICommonClock == NULL) - return false; - - // start the ICommonTimeConfig service - mICommonTimeConfig = CommonTimeConfigService::instantiate(*this); - if (mICommonTimeConfig == NULL) - return false; - - return true; -} - -bool CommonTimeServer::threadLoop() { - // Register our service interfaces. - if (!startServices()) - return false; - - // Hold the lock while we are in the main thread loop. It will release the - // lock when it blocks, and hold the lock at all other times. - mLock.lock(); - runStateMachine_l(); - mLock.unlock(); - - IPCThreadState::self()->stopProcess(); - return false; -} - -bool CommonTimeServer::runStateMachine_l() { - if (!mLocalClock.initCheck()) - return false; - - if (!mCommonClock.init(mLocalClock.getLocalFreq())) - return false; - - // Enter the initial state. - becomeInitial("startup"); - - // run the state machine - while (!exitPending()) { - struct pollfd pfds[2]; - int rc, timeout; - int eventCnt = 0; - int64_t wakeupTime; - uint32_t t1, t2; - bool needHandleTimeout = false; - - // We are always interested in our wakeup FD. - pfds[eventCnt].fd = mWakeupThreadFD; - pfds[eventCnt].events = POLLIN; - pfds[eventCnt].revents = 0; - eventCnt++; - - // If we have a valid socket, then we are interested in what it has to - // say as well. - if (mSocket >= 0) { - pfds[eventCnt].fd = mSocket; - pfds[eventCnt].events = POLLIN; - pfds[eventCnt].revents = 0; - eventCnt++; - } - - t1 = static_cast(mCurTimeout.msecTillTimeout()); - t2 = static_cast(mClockRecovery.applyRateLimitedSlew()); - timeout = static_cast(t1 < t2 ? t1 : t2); - - // Note, we were holding mLock when this function was called. We - // release it only while we are blocking and hold it at all other times. - mLock.unlock(); - rc = poll(pfds, eventCnt, timeout); - wakeupTime = mLocalClock.getLocalTime(); - mLock.lock(); - - // Is it time to shutdown? If so, don't hesitate... just do it. - if (exitPending()) - break; - - // Did the poll fail? This should never happen and is fatal if it does. - if (rc < 0) { - ALOGE("%s:%d poll failed", __PRETTY_FUNCTION__, __LINE__); - return false; - } - - if (rc == 0) { - needHandleTimeout = !mCurTimeout.msecTillTimeout(); - if (needHandleTimeout) - mCurTimeout.setTimeout(kInfiniteTimeout); - } - - // Were we woken up on purpose? If so, clear the eventfd with a read. - if (pfds[0].revents) - clearPendingWakeupEvents_l(); - - // Is out bind address dirty? If so, clean up our socket (if any). - // Alternatively, do we have an active socket but should be auto - // disabled? If so, release the socket and enter the proper sync state. - bool droppedSocket = false; - if (mBindIfaceDirty || ((mSocket >= 0) && shouldAutoDisable())) { - cleanupSocket_l(); - mBindIfaceDirty = false; - droppedSocket = true; - } - - // Do we not have a socket but should have one? If so, try to set one - // up. - if ((mSocket < 0) && mBindIfaceValid && !shouldAutoDisable()) { - if (setupSocket_l()) { - // Success! We are now joining a new network (either coming - // from no network, or coming from a potentially different - // network). Force our priority to be lower so that we defer to - // any other masters which may already be on the network we are - // joining. Later, when we enter either the client or the - // master state, we will clear this flag and go back to our - // normal election priority. - setForceLowPriority(true); - switch (mState) { - // If we were in initial (whether we had a immediately - // before this network or not) we want to simply reset the - // system and start again. Forcing a transition from - // INITIAL to INITIAL should do the job. - case CommonClockService::STATE_INITIAL: - becomeInitial("bound interface"); - break; - - // If we were in the master state, then either we were the - // master in a no-network situation, or we were the master - // of a different network and have moved to a new interface. - // In either case, immediately transition to Ronin at low - // priority. If there is no one in the network we just - // joined, we will become master soon enough. If there is, - // we want to be certain to defer master status to the - // existing timeline currently running on the network. - // - case CommonClockService::STATE_MASTER: - becomeRonin("leaving networkless mode"); - break; - - // If we were in any other state (CLIENT, RONIN, or - // WAIT_FOR_ELECTION) then we must be moving from one - // network to another. We have lost our old master; - // transition to RONIN in an attempt to find a new master. - // If there are none out there, we will just assume - // responsibility for the timeline we used to be a client - // of. - default: - becomeRonin("bound interface"); - break; - } - } else { - // That's odd... we failed to set up our socket. This could be - // due to some transient network change which will work itself - // out shortly; schedule a retry attempt in the near future. - mCurTimeout.setTimeout(kSetupRetryTimeoutMs); - } - - // One way or the other, we don't have any data to process at this - // point (since we just tried to bulid a new socket). Loop back - // around and wait for the next thing to do. - continue; - } else if (droppedSocket) { - // We just lost our socket, and for whatever reason (either no - // config, or auto disable engaged) we are not supposed to rebuild - // one at this time. We are not going to rebuild our socket until - // something about our config/auto-disabled status changes, so we - // are basically in network-less mode. If we are already in either - // INITIAL or MASTER, just stay there until something changes. If - // we are in any other state (CLIENT, RONIN or WAIT_FOR_ELECTION), - // then transition to either INITIAL or MASTER depending on whether - // or not our timeline is valid. - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "Entering networkless mode interface is %s, " - "shouldAutoDisable = %s", - mBindIfaceValid ? "valid" : "invalid", - shouldAutoDisable() ? "true" : "false"); - if ((mState != ICommonClock::STATE_INITIAL) && - (mState != ICommonClock::STATE_MASTER)) { - if (mTimelineID == ICommonClock::kInvalidTimelineID) - becomeInitial("network-less mode"); - else - becomeMaster("network-less mode"); - } - - continue; - } - - // Time to handle the timeouts? - if (needHandleTimeout) { - if (!handleTimeout()) - ALOGE("handleTimeout failed"); - continue; - } - - // Does our socket have data for us (assuming we still have one, we - // may have RXed a packet at the same time as a config change telling us - // to shut our socket down)? If so, process its data. - if ((mSocket >= 0) && (eventCnt > 1) && (pfds[1].revents)) { - mLastPacketRxLocalTime = wakeupTime; - if (!handlePacket()) - ALOGE("handlePacket failed"); - } - } - - cleanupSocket_l(); - return true; -} - -void CommonTimeServer::clearPendingWakeupEvents_l() { - int64_t tmp; - read(mWakeupThreadFD, &tmp, sizeof(tmp)); -} - -void CommonTimeServer::wakeupThread_l() { - int64_t tmp = 1; - write(mWakeupThreadFD, &tmp, sizeof(tmp)); -} - -void CommonTimeServer::cleanupSocket_l() { - if (mSocket >= 0) { - close(mSocket); - mSocket = -1; - } -} - -void CommonTimeServer::shutdownThread() { - // Flag the work thread for shutdown. - this->requestExit(); - - // Signal the thread in case its sleeping. - mLock.lock(); - wakeupThread_l(); - mLock.unlock(); - - // Wait for the thread to exit. - this->join(); -} - -bool CommonTimeServer::setupSocket_l() { - int rc; - bool ret_val = false; - struct sockaddr_in* ipv4_addr = NULL; - char masterElectionEPStr[64]; - const int one = 1; - - // This should never be needed, but if we happened to have an old socket - // lying around, be sure to not leak it before proceeding. - cleanupSocket_l(); - - // If we don't have a valid endpoint to bind to, then how did we get here in - // the first place? Regardless, we know that we are going to fail to bind, - // so don't even try. - if (!mBindIfaceValid) - return false; - - sockaddrToString(mMasterElectionEP, true, masterElectionEPStr, - sizeof(masterElectionEPStr)); - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "Building socket :: bind = %s master election = %s", - mBindIface.string(), masterElectionEPStr); - - // TODO: add proper support for IPv6. Right now, we block IPv6 addresses at - // the configuration interface level. - if (AF_INET != mMasterElectionEP.ss_family) { - mStateChangeLog.log(ANDROID_LOG_WARN, LOG_TAG, - "TODO: add proper IPv6 support"); - goto bailout; - } - - // open a UDP socket for the timeline serivce - mSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (mSocket < 0) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to create socket (errno = %d)", errno); - goto bailout; - } - - // Bind to the selected interface using Linux's spiffy SO_BINDTODEVICE. - struct ifreq ifr; - memset(&ifr, 0, sizeof(ifr)); - snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", mBindIface.string()); - ifr.ifr_name[sizeof(ifr.ifr_name) - 1] = 0; - rc = setsockopt(mSocket, SOL_SOCKET, SO_BINDTODEVICE, - (void *)&ifr, sizeof(ifr)); - if (rc) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to bind socket at to interface %s " - "(errno = %d)", ifr.ifr_name, errno); - goto bailout; - } - - // Bind our socket to INADDR_ANY and the master election port. The - // interface binding we made using SO_BINDTODEVICE should limit us to - // traffic only on the interface we are interested in. We need to bind to - // INADDR_ANY and the specific master election port in order to be able to - // receive both unicast traffic and master election multicast traffic with - // just a single socket. - struct sockaddr_in bindAddr; - ipv4_addr = reinterpret_cast(&mMasterElectionEP); - memcpy(&bindAddr, ipv4_addr, sizeof(bindAddr)); - bindAddr.sin_addr.s_addr = INADDR_ANY; - rc = bind(mSocket, - reinterpret_cast(&bindAddr), - sizeof(bindAddr)); - if (rc) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to bind socket to port %hu (errno = %d)", - ntohs(bindAddr.sin_port), errno); - goto bailout; - } - - if (0xE0000000 == (ntohl(ipv4_addr->sin_addr.s_addr) & 0xF0000000)) { - // If our master election endpoint is a multicast address, be sure to join - // the multicast group. - struct ip_mreq mreq; - mreq.imr_multiaddr = ipv4_addr->sin_addr; - mreq.imr_interface.s_addr = htonl(INADDR_ANY); - rc = setsockopt(mSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, - &mreq, sizeof(mreq)); - if (rc == -1) { - ALOGE("Failed to join multicast group at %s. (errno = %d)", - masterElectionEPStr, errno); - goto bailout; - } - - // disable loopback of multicast packets - const int zero = 0; - rc = setsockopt(mSocket, IPPROTO_IP, IP_MULTICAST_LOOP, - &zero, sizeof(zero)); - if (rc == -1) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to disable multicast loopback " - "(errno = %d)", errno); - goto bailout; - } - } else - if (ntohl(ipv4_addr->sin_addr.s_addr) == 0xFFFFFFFF) { - // If the master election address is the broadcast address, then enable - // the broadcast socket option - rc = setsockopt(mSocket, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one)); - if (rc == -1) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to enable broadcast (errno = %d)", - errno); - goto bailout; - } - } else { - // If the master election address is neither broadcast, nor multicast, - // then we are misconfigured. The config API layer should prevent this - // from ever happening. - goto bailout; - } - - // Set the TTL of sent packets to 1. (Time protocol sync should never leave - // the local subnet) - rc = setsockopt(mSocket, IPPROTO_IP, IP_TTL, &one, sizeof(one)); - if (rc == -1) { - mStateChangeLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "Failed to set TTL to %d (errno = %d)", one, errno); - goto bailout; - } - - // get the device's unique ID - if (!assignDeviceID()) - goto bailout; - - ret_val = true; - -bailout: - if (!ret_val) - cleanupSocket_l(); - return ret_val; -} - -// generate a unique device ID that can be used for arbitration -bool CommonTimeServer::assignDeviceID() { - if (!mBindIfaceValid) - return false; - - struct ifreq ifr; - memset(&ifr, 0, sizeof(ifr)); - ifr.ifr_addr.sa_family = AF_INET; - strlcpy(ifr.ifr_name, mBindIface.string(), IFNAMSIZ); - - int rc = ioctl(mSocket, SIOCGIFHWADDR, &ifr); - if (rc) { - ALOGE("%s:%d ioctl failed", __PRETTY_FUNCTION__, __LINE__); - return false; - } - - if (ifr.ifr_addr.sa_family != ARPHRD_ETHER) { - ALOGE("%s:%d got non-Ethernet address", __PRETTY_FUNCTION__, __LINE__); - return false; - } - - mDeviceID = 0; - for (int i = 0; i < ETH_ALEN; i++) { - mDeviceID = (mDeviceID << 8) | ifr.ifr_hwaddr.sa_data[i]; - } - - return true; -} - -// generate a new timeline ID -void CommonTimeServer::assignTimelineID() { - do { - mTimelineID = (static_cast(lrand48()) << 32) - | static_cast(lrand48()); - } while (mTimelineID == ICommonClock::kInvalidTimelineID); -} - -// Select a preference between the device IDs of two potential masters. -// Returns true if the first ID wins, or false if the second ID wins. -bool CommonTimeServer::arbitrateMaster( - uint64_t deviceID1, uint8_t devicePrio1, - uint64_t deviceID2, uint8_t devicePrio2) { - return ((devicePrio1 > devicePrio2) || - ((devicePrio1 == devicePrio2) && (deviceID1 > deviceID2))); -} - -static void hexDumpToString(const uint8_t* src, size_t src_len, - char* dst, size_t dst_len) { - size_t offset = 0; - size_t i; - - for (i = 0; (i < src_len) && (offset < dst_len); ++i) { - int res; - if (0 == (i % 16)) { - res = snprintf(dst + offset, dst_len - offset, "\n%04x :", i); - if (res < 0) - break; - offset += res; - if (offset >= dst_len) - break; - } - - res = snprintf(dst + offset, dst_len - offset, " %02x", src[i]); - if (res < 0) - break; - offset += res; - } - - dst[dst_len - 1] = 0; -} - -bool CommonTimeServer::handlePacket() { - uint8_t buf[256]; - struct sockaddr_storage srcAddr; - socklen_t srcAddrLen = sizeof(srcAddr); - - ssize_t recvBytes = recvfrom( - mSocket, buf, sizeof(buf), 0, - reinterpret_cast(&srcAddr), &srcAddrLen); - - if (recvBytes < 0) { - mBadPktLog.log(ANDROID_LOG_ERROR, LOG_TAG, - "recvfrom failed (res %d, errno %d)", - recvBytes, errno); - return false; - } - - UniversalTimeServicePacket pkt; - if (pkt.deserializePacket(buf, recvBytes, mSyncGroupID) < 0) { - char hex[256]; - char srcEPStr[64]; - - hexDumpToString(buf, static_cast(recvBytes), hex, sizeof(hex)); - sockaddrToString(srcAddr, true, srcEPStr, sizeof(srcEPStr)); - - mBadPktLog.log("Failed to parse %d byte packet from %s.%s", - recvBytes, srcEPStr, hex); - return false; - } - - bool result; - switch (pkt.packetType) { - case TIME_PACKET_WHO_IS_MASTER_REQUEST: - result = handleWhoIsMasterRequest(&pkt.p.who_is_master_request, - srcAddr); - break; - - case TIME_PACKET_WHO_IS_MASTER_RESPONSE: - result = handleWhoIsMasterResponse(&pkt.p.who_is_master_response, - srcAddr); - break; - - case TIME_PACKET_SYNC_REQUEST: - result = handleSyncRequest(&pkt.p.sync_request, srcAddr); - break; - - case TIME_PACKET_SYNC_RESPONSE: - result = handleSyncResponse(&pkt.p.sync_response, srcAddr); - break; - - case TIME_PACKET_MASTER_ANNOUNCEMENT: - result = handleMasterAnnouncement(&pkt.p.master_announcement, - srcAddr); - break; - - default: { - char srcEPStr[64]; - sockaddrToString(srcAddr, true, srcEPStr, sizeof(srcEPStr)); - - mBadPktLog.log(ANDROID_LOG_WARN, LOG_TAG, - "unknown packet type (%d) from %s", - pkt.packetType, srcEPStr); - - result = false; - } break; - } - - return result; -} - -bool CommonTimeServer::handleTimeout() { - // If we have no socket, then this must be a timeout to retry socket setup. - if (mSocket < 0) - return true; - - switch (mState) { - case ICommonClock::STATE_INITIAL: - return handleTimeoutInitial(); - case ICommonClock::STATE_CLIENT: - return handleTimeoutClient(); - case ICommonClock::STATE_MASTER: - return handleTimeoutMaster(); - case ICommonClock::STATE_RONIN: - return handleTimeoutRonin(); - case ICommonClock::STATE_WAIT_FOR_ELECTION: - return handleTimeoutWaitForElection(); - } - - return false; -} - -bool CommonTimeServer::handleTimeoutInitial() { - if (++mInitial_WhoIsMasterRequestTimeouts == - kInitial_NumWhoIsMasterRetries) { - // none of our attempts to discover a master succeeded, so make - // this device the master - return becomeMaster("initial timeout"); - } else { - // retry the WhoIsMaster request - return sendWhoIsMasterRequest(); - } -} - -bool CommonTimeServer::handleTimeoutClient() { - if (shouldPanicNotGettingGoodData()) - return becomeInitial("timeout panic, no good data"); - - if (mClient_SyncRequestPending) { - mClient_SyncRequestPending = false; - - if (++mClient_SyncRequestTimeouts < kClient_NumSyncRequestRetries) { - // a sync request has timed out, so retry - return sendSyncRequest(); - } else { - // The master has failed to respond to a sync request for too many - // times in a row. Assume the master is dead and start electing - // a new master. - return becomeRonin("master not responding"); - } - } else { - // initiate the next sync request - return sendSyncRequest(); - } -} - -bool CommonTimeServer::handleTimeoutMaster() { - // send another announcement from the master - return sendMasterAnnouncement(); -} - -bool CommonTimeServer::handleTimeoutRonin() { - if (++mRonin_WhoIsMasterRequestTimeouts == kRonin_NumWhoIsMasterRetries) { - // no other master is out there, so we won the election - return becomeMaster("no better masters detected"); - } else { - return sendWhoIsMasterRequest(); - } -} - -bool CommonTimeServer::handleTimeoutWaitForElection() { - return becomeRonin("timeout waiting for election conclusion"); -} - -bool CommonTimeServer::handleWhoIsMasterRequest( - const WhoIsMasterRequestPacket* request, - const sockaddr_storage& srcAddr) { - // Skip our own messages which come back via broadcast loopback. - if (request->senderDeviceID == mDeviceID) - return true; - - char srcEPStr[64]; - sockaddrToString(srcAddr, true, srcEPStr, sizeof(srcEPStr)); - mElectionLog.log("RXed WhoIs master request while in state %s. " - "src %s reqTID %016llx ourTID %016llx", - stateToString(mState), srcEPStr, - request->timelineID, mTimelineID); - - if (mState == ICommonClock::STATE_MASTER) { - // is this request related to this master's timeline? - if (request->timelineID != ICommonClock::kInvalidTimelineID && - request->timelineID != mTimelineID) - return true; - - WhoIsMasterResponsePacket pkt; - pkt.initHeader(mTimelineID, mSyncGroupID); - pkt.deviceID = mDeviceID; - pkt.devicePriority = effectivePriority(); - - mElectionLog.log("TXing WhoIs master resp to %s while in state %s. " - "ourTID %016llx ourGID %016llx ourDID %016llx " - "ourPrio %u", - srcEPStr, stateToString(mState), - mTimelineID, mSyncGroupID, - pkt.deviceID, pkt.devicePriority); - - uint8_t buf[256]; - ssize_t bufSz = pkt.serializePacket(buf, sizeof(buf)); - if (bufSz < 0) - return false; - - ssize_t sendBytes = sendto( - mSocket, buf, bufSz, 0, - reinterpret_cast(&srcAddr), - sizeof(srcAddr)); - if (sendBytes == -1) { - ALOGE("%s:%d sendto failed", __PRETTY_FUNCTION__, __LINE__); - return false; - } - } else if (mState == ICommonClock::STATE_RONIN) { - // if we hear a WhoIsMaster request from another device following - // the same timeline and that device wins arbitration, then we will stop - // trying to elect ourselves master and will instead wait for an - // announcement from the election winner - if (request->timelineID != mTimelineID) - return true; - - if (arbitrateMaster(request->senderDeviceID, - request->senderDevicePriority, - mDeviceID, - effectivePriority())) - return becomeWaitForElection("would lose election"); - - return true; - } else if (mState == ICommonClock::STATE_INITIAL) { - // If a group of devices booted simultaneously (e.g. after a power - // outage) and all of them are in the initial state and there is no - // master, then each device may time out and declare itself master at - // the same time. To avoid this, listen for - // WhoIsMaster(InvalidTimeline) requests from peers. If we would lose - // arbitration against that peer, reset our timeout count so that the - // peer has a chance to become master before we time out. - if (request->timelineID == ICommonClock::kInvalidTimelineID && - arbitrateMaster(request->senderDeviceID, - request->senderDevicePriority, - mDeviceID, - effectivePriority())) { - mInitial_WhoIsMasterRequestTimeouts = 0; - } - } - - return true; -} - -bool CommonTimeServer::handleWhoIsMasterResponse( - const WhoIsMasterResponsePacket* response, - const sockaddr_storage& srcAddr) { - // Skip our own messages which come back via broadcast loopback. - if (response->deviceID == mDeviceID) - return true; - - char srcEPStr[64]; - sockaddrToString(srcAddr, true, srcEPStr, sizeof(srcEPStr)); - mElectionLog.log("RXed WhoIs master response while in state %s. " - "src %s respTID %016llx respDID %016llx respPrio %u " - "ourTID %016llx", - stateToString(mState), srcEPStr, - response->timelineID, - response->deviceID, - static_cast(response->devicePriority), - mTimelineID); - - if (mState == ICommonClock::STATE_INITIAL || mState == ICommonClock::STATE_RONIN) { - return becomeClient(srcAddr, - response->deviceID, - response->devicePriority, - response->timelineID, - "heard whois response"); - } else if (mState == ICommonClock::STATE_CLIENT) { - // if we get multiple responses because there are multiple devices - // who believe that they are master, then follow the master that - // wins arbitration - if (arbitrateMaster(response->deviceID, - response->devicePriority, - mClient_MasterDeviceID, - mClient_MasterDevicePriority)) { - return becomeClient(srcAddr, - response->deviceID, - response->devicePriority, - response->timelineID, - "heard whois response"); - } - } - - return true; -} - -bool CommonTimeServer::handleSyncRequest(const SyncRequestPacket* request, - const sockaddr_storage& srcAddr) { - SyncResponsePacket pkt; - pkt.initHeader(mTimelineID, mSyncGroupID); - - if ((mState == ICommonClock::STATE_MASTER) && - (mTimelineID == request->timelineID)) { - int64_t rxLocalTime = mLastPacketRxLocalTime; - int64_t rxCommonTime; - - // If we are master on an actual network and have actual clients, then - // we are no longer low priority. - setForceLowPriority(false); - - if (OK != mCommonClock.localToCommon(rxLocalTime, &rxCommonTime)) { - return false; - } - - int64_t txLocalTime = mLocalClock.getLocalTime();; - int64_t txCommonTime; - if (OK != mCommonClock.localToCommon(txLocalTime, &txCommonTime)) { - return false; - } - - pkt.nak = 0; - pkt.clientTxLocalTime = request->clientTxLocalTime; - pkt.masterRxCommonTime = rxCommonTime; - pkt.masterTxCommonTime = txCommonTime; - } else { - pkt.nak = 1; - pkt.clientTxLocalTime = 0; - pkt.masterRxCommonTime = 0; - pkt.masterTxCommonTime = 0; - } - - uint8_t buf[256]; - ssize_t bufSz = pkt.serializePacket(buf, sizeof(buf)); - if (bufSz < 0) - return false; - - ssize_t sendBytes = sendto( - mSocket, &buf, bufSz, 0, - reinterpret_cast(&srcAddr), - sizeof(srcAddr)); - if (sendBytes == -1) { - ALOGE("%s:%d sendto failed", __PRETTY_FUNCTION__, __LINE__); - return false; - } - - return true; -} - -bool CommonTimeServer::handleSyncResponse( - const SyncResponsePacket* response, - const sockaddr_storage& srcAddr) { - if (mState != ICommonClock::STATE_CLIENT) - return true; - - assert(mMasterEPValid); - if (!sockaddrMatch(srcAddr, mMasterEP, true)) { - char srcEP[64], expectedEP[64]; - sockaddrToString(srcAddr, true, srcEP, sizeof(srcEP)); - sockaddrToString(mMasterEP, true, expectedEP, sizeof(expectedEP)); - ALOGI("Dropping sync response from unexpected address." - " Expected %s Got %s", expectedEP, srcEP); - return true; - } - - if (response->nak) { - // if our master is no longer accepting requests, then we need to find - // a new master - return becomeRonin("master NAK'ed"); - } - - mClient_SyncRequestPending = 0; - mClient_SyncRequestTimeouts = 0; - mClient_PacketRTTLog.logRX(response->clientTxLocalTime, - mLastPacketRxLocalTime); - - bool result; - if (!(mClient_SyncRespsRXedFromCurMaster++)) { - // the first request/response exchange between a client and a master - // may take unusually long due to ARP, so discard it. - result = true; - } else { - int64_t clientTxLocalTime = response->clientTxLocalTime; - int64_t clientRxLocalTime = mLastPacketRxLocalTime; - int64_t masterTxCommonTime = response->masterTxCommonTime; - int64_t masterRxCommonTime = response->masterRxCommonTime; - - int64_t rtt = (clientRxLocalTime - clientTxLocalTime); - int64_t avgLocal = (clientTxLocalTime + clientRxLocalTime) >> 1; - int64_t avgCommon = (masterTxCommonTime + masterRxCommonTime) >> 1; - - // if the RTT of the packet is significantly larger than the panic - // threshold, we should simply discard it. Its better to do nothing - // than to take cues from a packet like that. - int rttCommon = mCommonClock.localDurationToCommonDuration(rtt); - if (rttCommon > (static_cast(mPanicThresholdUsec) * - kRTTDiscardPanicThreshMultiplier)) { - ALOGV("Dropping sync response with RTT of %lld uSec", rttCommon); - mClient_ExpiredSyncRespsRXedFromCurMaster++; - if (shouldPanicNotGettingGoodData()) - return becomeInitial("RX panic, no good data"); - } else { - result = mClockRecovery.pushDisciplineEvent(avgLocal, avgCommon, rttCommon); - mClient_LastGoodSyncRX = clientRxLocalTime; - - if (result) { - // indicate to listeners that we've synced to the common timeline - notifyClockSync(); - } else { - ALOGE("Panic! Observed clock sync error is too high to tolerate," - " resetting state machine and starting over."); - notifyClockSyncLoss(); - return becomeInitial("panic"); - } - } - } - - mCurTimeout.setTimeout(mSyncRequestIntervalMs); - return result; -} - -bool CommonTimeServer::handleMasterAnnouncement( - const MasterAnnouncementPacket* packet, - const sockaddr_storage& srcAddr) { - uint64_t newDeviceID = packet->deviceID; - uint8_t newDevicePrio = packet->devicePriority; - uint64_t newTimelineID = packet->timelineID; - - // Skip our own messages which come back via broadcast loopback. - if (newDeviceID == mDeviceID) - return true; - - char srcEPStr[64]; - sockaddrToString(srcAddr, true, srcEPStr, sizeof(srcEPStr)); - mElectionLog.log("RXed master announcement while in state %s. " - "src %s srcDevID %lld srcPrio %u srcTID %016llx", - stateToString(mState), srcEPStr, - newDeviceID, static_cast(newDevicePrio), - newTimelineID); - - if (mState == ICommonClock::STATE_INITIAL || - mState == ICommonClock::STATE_RONIN || - mState == ICommonClock::STATE_WAIT_FOR_ELECTION) { - // if we aren't currently following a master, then start following - // this new master - return becomeClient(srcAddr, - newDeviceID, - newDevicePrio, - newTimelineID, - "heard master announcement"); - } else if (mState == ICommonClock::STATE_CLIENT) { - // if the new master wins arbitration against our current master, - // then become a client of the new master - if (arbitrateMaster(newDeviceID, - newDevicePrio, - mClient_MasterDeviceID, - mClient_MasterDevicePriority)) - return becomeClient(srcAddr, - newDeviceID, - newDevicePrio, - newTimelineID, - "heard master announcement"); - } else if (mState == ICommonClock::STATE_MASTER) { - // two masters are competing - if the new one wins arbitration, then - // cease acting as master - if (arbitrateMaster(newDeviceID, newDevicePrio, - mDeviceID, effectivePriority())) - return becomeClient(srcAddr, newDeviceID, - newDevicePrio, newTimelineID, - "heard master announcement"); - } - - return true; -} - -bool CommonTimeServer::sendWhoIsMasterRequest() { - assert(mState == ICommonClock::STATE_INITIAL || mState == ICommonClock::STATE_RONIN); - - // If we have no socket, then we must be in the unconfigured initial state. - // Don't report any errors, just don't try to send the initial who-is-master - // query. Eventually, our network will either become configured, or we will - // be forced into network-less master mode by higher level code. - if (mSocket < 0) { - assert(mState == ICommonClock::STATE_INITIAL); - return true; - } - - bool ret = false; - WhoIsMasterRequestPacket pkt; - pkt.initHeader(mSyncGroupID); - pkt.senderDeviceID = mDeviceID; - pkt.senderDevicePriority = effectivePriority(); - - uint8_t buf[256]; - ssize_t bufSz = pkt.serializePacket(buf, sizeof(buf)); - if (bufSz >= 0) { - char dstEPStr[64]; - sockaddrToString(mMasterElectionEP, true, dstEPStr, sizeof(dstEPStr)); - mElectionLog.log("TXing WhoIs master request to %s while in state %s. " - "ourTID %016llx ourGID %016llx ourDID %016llx " - "ourPrio %u", - dstEPStr, stateToString(mState), - mTimelineID, mSyncGroupID, - pkt.senderDeviceID, pkt.senderDevicePriority); - - ssize_t sendBytes = sendto( - mSocket, buf, bufSz, 0, - reinterpret_cast(&mMasterElectionEP), - sizeof(mMasterElectionEP)); - if (sendBytes < 0) - ALOGE("WhoIsMaster sendto failed (errno %d)", errno); - ret = true; - } - - if (mState == ICommonClock::STATE_INITIAL) { - mCurTimeout.setTimeout(kInitial_WhoIsMasterTimeoutMs); - } else { - mCurTimeout.setTimeout(kRonin_WhoIsMasterTimeoutMs); - } - - return ret; -} - -bool CommonTimeServer::sendSyncRequest() { - // If we are sending sync requests, then we must be in the client state and - // we must have a socket (when we have no network, we are only supposed to - // be in INITIAL or MASTER) - assert(mState == ICommonClock::STATE_CLIENT); - assert(mSocket >= 0); - - bool ret = false; - SyncRequestPacket pkt; - pkt.initHeader(mTimelineID, mSyncGroupID); - pkt.clientTxLocalTime = mLocalClock.getLocalTime(); - - if (!mClient_FirstSyncTX) - mClient_FirstSyncTX = pkt.clientTxLocalTime; - - mClient_PacketRTTLog.logTX(pkt.clientTxLocalTime); - - uint8_t buf[256]; - ssize_t bufSz = pkt.serializePacket(buf, sizeof(buf)); - if (bufSz >= 0) { - ssize_t sendBytes = sendto( - mSocket, buf, bufSz, 0, - reinterpret_cast(&mMasterEP), - sizeof(mMasterEP)); - if (sendBytes < 0) - ALOGE("SyncRequest sendto failed (errno %d)", errno); - ret = true; - } - - mClient_SyncsSentToCurMaster++; - mCurTimeout.setTimeout(mSyncRequestIntervalMs); - mClient_SyncRequestPending = true; - - return ret; -} - -bool CommonTimeServer::sendMasterAnnouncement() { - bool ret = false; - assert(mState == ICommonClock::STATE_MASTER); - - // If we are being asked to send a master announcement, but we have no - // socket, we must be in network-less master mode. Don't bother to send the - // announcement, and don't bother to schedule a timeout. When the network - // comes up, the work thread will get poked and start the process of - // figuring out who the current master should be. - if (mSocket < 0) { - mCurTimeout.setTimeout(kInfiniteTimeout); - return true; - } - - MasterAnnouncementPacket pkt; - pkt.initHeader(mTimelineID, mSyncGroupID); - pkt.deviceID = mDeviceID; - pkt.devicePriority = effectivePriority(); - - uint8_t buf[256]; - ssize_t bufSz = pkt.serializePacket(buf, sizeof(buf)); - if (bufSz >= 0) { - char dstEPStr[64]; - sockaddrToString(mMasterElectionEP, true, dstEPStr, sizeof(dstEPStr)); - mElectionLog.log("TXing Master announcement to %s while in state %s. " - "ourTID %016llx ourGID %016llx ourDID %016llx " - "ourPrio %u", - dstEPStr, stateToString(mState), - mTimelineID, mSyncGroupID, - pkt.deviceID, pkt.devicePriority); - - ssize_t sendBytes = sendto( - mSocket, buf, bufSz, 0, - reinterpret_cast(&mMasterElectionEP), - sizeof(mMasterElectionEP)); - if (sendBytes < 0) - ALOGE("MasterAnnouncement sendto failed (errno %d)", errno); - ret = true; - } - - mCurTimeout.setTimeout(mMasterAnnounceIntervalMs); - return ret; -} - -bool CommonTimeServer::becomeClient(const sockaddr_storage& masterEP, - uint64_t masterDeviceID, - uint8_t masterDevicePriority, - uint64_t timelineID, - const char* cause) { - char newEPStr[64], oldEPStr[64]; - sockaddrToString(masterEP, true, newEPStr, sizeof(newEPStr)); - sockaddrToString(mMasterEP, mMasterEPValid, oldEPStr, sizeof(oldEPStr)); - - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "%s --> CLIENT (%s) :%s" - " OldMaster: %02x-%014llx::%016llx::%s" - " NewMaster: %02x-%014llx::%016llx::%s", - stateToString(mState), cause, - (mTimelineID != timelineID) ? " (new timeline)" : "", - mClient_MasterDevicePriority, mClient_MasterDeviceID, - mTimelineID, oldEPStr, - masterDevicePriority, masterDeviceID, - timelineID, newEPStr); - - if (mTimelineID != timelineID) { - // start following a new timeline - mTimelineID = timelineID; - mClockRecovery.reset(true, true); - notifyClockSyncLoss(); - } else { - // start following a new master on the existing timeline - mClockRecovery.reset(false, true); - } - - mMasterEP = masterEP; - mMasterEPValid = true; - - // If we are on a real network as a client of a real master, then we should - // no longer force low priority. If our master disappears, we should have - // the high priority bit set during the election to replace the master - // because this group was a real group and not a singleton created in - // networkless mode. - setForceLowPriority(false); - - mClient_MasterDeviceID = masterDeviceID; - mClient_MasterDevicePriority = masterDevicePriority; - resetSyncStats(); - - setState(ICommonClock::STATE_CLIENT); - - // add some jitter to when the various clients send their requests - // in order to reduce the likelihood that a group of clients overload - // the master after receiving a master announcement - usleep((lrand48() % 100) * 1000); - - return sendSyncRequest(); -} - -bool CommonTimeServer::becomeMaster(const char* cause) { - uint64_t oldTimelineID = mTimelineID; - if (mTimelineID == ICommonClock::kInvalidTimelineID) { - // this device has not been following any existing timeline, - // so it will create a new timeline and declare itself master - assert(!mCommonClock.isValid()); - - // set the common time basis - mCommonClock.setBasis(mLocalClock.getLocalTime(), 0); - - // assign an arbitrary timeline iD - assignTimelineID(); - - // notify listeners that we've created a common timeline - notifyClockSync(); - } - - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "%s --> MASTER (%s) : %s timeline %016llx", - stateToString(mState), cause, - (oldTimelineID == mTimelineID) ? "taking ownership of" - : "creating new", - mTimelineID); - - memset(&mMasterEP, 0, sizeof(mMasterEP)); - mMasterEPValid = false; - mClient_MasterDevicePriority = effectivePriority(); - mClient_MasterDeviceID = mDeviceID; - mClockRecovery.reset(false, true); - resetSyncStats(); - - setState(ICommonClock::STATE_MASTER); - return sendMasterAnnouncement(); -} - -bool CommonTimeServer::becomeRonin(const char* cause) { - // If we were the client of a given timeline, but had never received even a - // single time sync packet, then we transition back to Initial instead of - // Ronin. If we transition to Ronin and end up becoming the new Master, we - // will be unable to service requests for other clients because we never - // actually knew what time it was. By going to initial, we ensure that - // other clients who know what time it is, but would lose master arbitration - // in the Ronin case, will step up and become the proper new master of the - // old timeline. - - char oldEPStr[64]; - sockaddrToString(mMasterEP, mMasterEPValid, oldEPStr, sizeof(oldEPStr)); - memset(&mMasterEP, 0, sizeof(mMasterEP)); - mMasterEPValid = false; - - if (mCommonClock.isValid()) { - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "%s --> RONIN (%s) : lost track of previously valid timeline " - "%02x-%014llx::%016llx::%s (%d TXed %d RXed %d RXExpired)", - stateToString(mState), cause, - mClient_MasterDevicePriority, mClient_MasterDeviceID, - mTimelineID, oldEPStr, - mClient_SyncsSentToCurMaster, - mClient_SyncRespsRXedFromCurMaster, - mClient_ExpiredSyncRespsRXedFromCurMaster); - - mRonin_WhoIsMasterRequestTimeouts = 0; - setState(ICommonClock::STATE_RONIN); - return sendWhoIsMasterRequest(); - } else { - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "%s --> INITIAL (%s) : never synced timeline " - "%02x-%014llx::%016llx::%s (%d TXed %d RXed %d RXExpired)", - stateToString(mState), cause, - mClient_MasterDevicePriority, mClient_MasterDeviceID, - mTimelineID, oldEPStr, - mClient_SyncsSentToCurMaster, - mClient_SyncRespsRXedFromCurMaster, - mClient_ExpiredSyncRespsRXedFromCurMaster); - - return becomeInitial("ronin, no timeline"); - } -} - -bool CommonTimeServer::becomeWaitForElection(const char* cause) { - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "%s --> WAIT_FOR_ELECTION (%s) : dropping out of election," - " waiting %d mSec for completion.", - stateToString(mState), cause, kWaitForElection_TimeoutMs); - - setState(ICommonClock::STATE_WAIT_FOR_ELECTION); - mCurTimeout.setTimeout(kWaitForElection_TimeoutMs); - return true; -} - -bool CommonTimeServer::becomeInitial(const char* cause) { - mStateChangeLog.log(ANDROID_LOG_INFO, LOG_TAG, - "Entering INITIAL (%s), total reset.", - cause); - - setState(ICommonClock::STATE_INITIAL); - - // reset clock recovery - mClockRecovery.reset(true, true); - - // reset internal state bookkeeping. - mCurTimeout.setTimeout(kInfiniteTimeout); - memset(&mMasterEP, 0, sizeof(mMasterEP)); - mMasterEPValid = false; - mLastPacketRxLocalTime = 0; - mTimelineID = ICommonClock::kInvalidTimelineID; - mClockSynced = false; - mInitial_WhoIsMasterRequestTimeouts = 0; - mClient_MasterDeviceID = 0; - mClient_MasterDevicePriority = 0; - mRonin_WhoIsMasterRequestTimeouts = 0; - resetSyncStats(); - - // send the first request to discover the master - return sendWhoIsMasterRequest(); -} - -void CommonTimeServer::notifyClockSync() { - if (!mClockSynced) { - mClockSynced = true; - mICommonClock->notifyOnTimelineChanged(mTimelineID); - } -} - -void CommonTimeServer::notifyClockSyncLoss() { - if (mClockSynced) { - mClockSynced = false; - mICommonClock->notifyOnTimelineChanged( - ICommonClock::kInvalidTimelineID); - } -} - -void CommonTimeServer::setState(ICommonClock::State s) { - mState = s; -} - -const char* CommonTimeServer::stateToString(ICommonClock::State s) { - switch(s) { - case ICommonClock::STATE_INITIAL: - return "INITIAL"; - case ICommonClock::STATE_CLIENT: - return "CLIENT"; - case ICommonClock::STATE_MASTER: - return "MASTER"; - case ICommonClock::STATE_RONIN: - return "RONIN"; - case ICommonClock::STATE_WAIT_FOR_ELECTION: - return "WAIT_FOR_ELECTION"; - default: - return "unknown"; - } -} - -void CommonTimeServer::sockaddrToString(const sockaddr_storage& addr, - bool addrValid, - char* buf, size_t bufLen) { - if (!bufLen || !buf) - return; - - if (addrValid) { - switch (addr.ss_family) { - case AF_INET: { - const struct sockaddr_in* sa = - reinterpret_cast(&addr); - unsigned long a = ntohl(sa->sin_addr.s_addr); - uint16_t p = ntohs(sa->sin_port); - snprintf(buf, bufLen, "%lu.%lu.%lu.%lu:%hu", - ((a >> 24) & 0xFF), ((a >> 16) & 0xFF), - ((a >> 8) & 0xFF), (a & 0xFF), p); - } break; - - case AF_INET6: { - const struct sockaddr_in6* sa = - reinterpret_cast(&addr); - const uint8_t* a = sa->sin6_addr.s6_addr; - uint16_t p = ntohs(sa->sin6_port); - snprintf(buf, bufLen, - "%02X%02X:%02X%02X:%02X%02X:%02X%02X:" - "%02X%02X:%02X%02X:%02X%02X:%02X%02X port %hd", - a[0], a[1], a[ 2], a[ 3], a[ 4], a[ 5], a[ 6], a[ 7], - a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15], - p); - } break; - - default: - snprintf(buf, bufLen, - "", addr.ss_family); - break; - } - } else { - snprintf(buf, bufLen, ""); - } - - buf[bufLen - 1] = 0; -} - -bool CommonTimeServer::sockaddrMatch(const sockaddr_storage& a1, - const sockaddr_storage& a2, - bool matchAddressOnly) { - if (a1.ss_family != a2.ss_family) - return false; - - switch (a1.ss_family) { - case AF_INET: { - const struct sockaddr_in* sa1 = - reinterpret_cast(&a1); - const struct sockaddr_in* sa2 = - reinterpret_cast(&a2); - - if (sa1->sin_addr.s_addr != sa2->sin_addr.s_addr) - return false; - - return (matchAddressOnly || (sa1->sin_port == sa2->sin_port)); - } break; - - case AF_INET6: { - const struct sockaddr_in6* sa1 = - reinterpret_cast(&a1); - const struct sockaddr_in6* sa2 = - reinterpret_cast(&a2); - - if (memcmp(&sa1->sin6_addr, &sa2->sin6_addr, sizeof(sa2->sin6_addr))) - return false; - - return (matchAddressOnly || (sa1->sin6_port == sa2->sin6_port)); - } break; - - // Huh? We don't deal in non-IPv[46] addresses. Not sure how we got - // here, but we don't know how to comapre these addresses and simply - // default to a no-match decision. - default: return false; - } -} - -bool CommonTimeServer::shouldPanicNotGettingGoodData() { - if (mClient_FirstSyncTX) { - int64_t now = mLocalClock.getLocalTime(); - int64_t delta = now - (mClient_LastGoodSyncRX - ? mClient_LastGoodSyncRX - : mClient_FirstSyncTX); - int64_t deltaUsec = mCommonClock.localDurationToCommonDuration(delta); - - if (deltaUsec >= kNoGoodDataPanicThresholdUsec) - return true; - } - - return false; -} - -void CommonTimeServer::PacketRTTLog::logTX(int64_t txTime) { - txTimes[wrPtr] = txTime; - rxTimes[wrPtr] = 0; - wrPtr = (wrPtr + 1) % RTT_LOG_SIZE; - if (!wrPtr) - logFull = true; -} - -void CommonTimeServer::PacketRTTLog::logRX(int64_t txTime, int64_t rxTime) { - if (!logFull && !wrPtr) - return; - - uint32_t i = logFull ? wrPtr : 0; - do { - if (txTimes[i] == txTime) { - rxTimes[i] = rxTime; - break; - } - i = (i + 1) % RTT_LOG_SIZE; - } while (i != wrPtr); -} - -} // namespace android diff --git a/services/common_time/common_time_server.h b/services/common_time/common_time_server.h deleted file mode 100644 index 6e18050..0000000 --- a/services/common_time/common_time_server.h +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_COMMON_TIME_SERVER_H -#define ANDROID_COMMON_TIME_SERVER_H - -#include -#include -#include - -#include -#include -#include - -#include "clock_recovery.h" -#include "common_clock.h" -#include "common_time_server_packets.h" -#include "utils.h" - -#define RTT_LOG_SIZE 30 - -namespace android { - -class CommonClockService; -class CommonTimeConfigService; - -/***** time service implementation *****/ - -class CommonTimeServer : public Thread { - public: - CommonTimeServer(); - ~CommonTimeServer(); - - bool startServices(); - - // Common Clock API methods - CommonClock& getCommonClock() { return mCommonClock; } - LocalClock& getLocalClock() { return mLocalClock; } - uint64_t getTimelineID(); - int32_t getEstimatedError(); - ICommonClock::State getState(); - status_t getMasterAddr(struct sockaddr_storage* addr); - status_t isCommonTimeValid(bool* valid, uint32_t* timelineID); - - // Config API methods - status_t getMasterElectionPriority(uint8_t *priority); - status_t setMasterElectionPriority(uint8_t priority); - status_t getMasterElectionEndpoint(struct sockaddr_storage *addr); - status_t setMasterElectionEndpoint(const struct sockaddr_storage *addr); - status_t getMasterElectionGroupId(uint64_t *id); - status_t setMasterElectionGroupId(uint64_t id); - status_t getInterfaceBinding(String8& ifaceName); - status_t setInterfaceBinding(const String8& ifaceName); - status_t getMasterAnnounceInterval(int *interval); - status_t setMasterAnnounceInterval(int interval); - status_t getClientSyncInterval(int *interval); - status_t setClientSyncInterval(int interval); - status_t getPanicThreshold(int *threshold); - status_t setPanicThreshold(int threshold); - status_t getAutoDisable(bool *autoDisable); - status_t setAutoDisable(bool autoDisable); - status_t forceNetworklessMasterMode(); - - // Method used by the CommonClockService to notify the core service about - // changes in the number of active common clock clients. - void reevaluateAutoDisableState(bool commonClockHasClients); - - status_t dumpClockInterface(int fd, const Vector& args, - size_t activeClients); - status_t dumpConfigInterface(int fd, const Vector& args); - - private: - class PacketRTTLog { - public: - PacketRTTLog() { - resetLog(); - } - - void resetLog() { - wrPtr = 0; - logFull = 0; - } - - void logTX(int64_t txTime); - void logRX(int64_t txTime, int64_t rxTime); - void dumpLog(int fd, const CommonClock& cclk); - - private: - uint32_t wrPtr; - bool logFull; - int64_t txTimes[RTT_LOG_SIZE]; - int64_t rxTimes[RTT_LOG_SIZE]; - }; - - bool threadLoop(); - - bool runStateMachine_l(); - bool setupSocket_l(); - - void assignTimelineID(); - bool assignDeviceID(); - - static bool arbitrateMaster(uint64_t deviceID1, uint8_t devicePrio1, - uint64_t deviceID2, uint8_t devicePrio2); - - bool handlePacket(); - bool handleWhoIsMasterRequest (const WhoIsMasterRequestPacket* request, - const sockaddr_storage& srcAddr); - bool handleWhoIsMasterResponse(const WhoIsMasterResponsePacket* response, - const sockaddr_storage& srcAddr); - bool handleSyncRequest (const SyncRequestPacket* request, - const sockaddr_storage& srcAddr); - bool handleSyncResponse (const SyncResponsePacket* response, - const sockaddr_storage& srcAddr); - bool handleMasterAnnouncement (const MasterAnnouncementPacket* packet, - const sockaddr_storage& srcAddr); - - bool handleTimeout(); - bool handleTimeoutInitial(); - bool handleTimeoutClient(); - bool handleTimeoutMaster(); - bool handleTimeoutRonin(); - bool handleTimeoutWaitForElection(); - - bool sendWhoIsMasterRequest(); - bool sendSyncRequest(); - bool sendMasterAnnouncement(); - - bool becomeClient(const sockaddr_storage& masterAddr, - uint64_t masterDeviceID, - uint8_t masterDevicePriority, - uint64_t timelineID, - const char* cause); - bool becomeMaster(const char* cause); - bool becomeRonin(const char* cause); - bool becomeWaitForElection(const char* cause); - bool becomeInitial(const char* cause); - - void notifyClockSync(); - void notifyClockSyncLoss(); - - ICommonClock::State mState; - void setState(ICommonClock::State s); - - void clearPendingWakeupEvents_l(); - void wakeupThread_l(); - void cleanupSocket_l(); - void shutdownThread(); - - inline uint8_t effectivePriority() const { - return (mMasterPriority & 0x7F) | - (mForceLowPriority ? 0x00 : 0x80); - } - - inline bool shouldAutoDisable() const { - return (mAutoDisable && !mCommonClockHasClients); - } - - inline void resetSyncStats() { - mClient_SyncRequestPending = false; - mClient_SyncRequestTimeouts = 0; - mClient_SyncsSentToCurMaster = 0; - mClient_SyncRespsRXedFromCurMaster = 0; - mClient_ExpiredSyncRespsRXedFromCurMaster = 0; - mClient_FirstSyncTX = 0; - mClient_LastGoodSyncRX = 0; - mClient_PacketRTTLog.resetLog(); - } - - bool shouldPanicNotGettingGoodData(); - - // Helper to keep track of the state machine's current timeout - Timeout mCurTimeout; - - // common clock, local clock abstraction, and clock recovery loop - CommonClock mCommonClock; - LocalClock mLocalClock; - ClockRecoveryLoop mClockRecovery; - - // implementation of ICommonClock - sp mICommonClock; - - // implementation of ICommonTimeConfig - sp mICommonTimeConfig; - - // UDP socket for the time sync protocol - int mSocket; - - // eventfd used to wakeup the work thread in response to configuration - // changes. - int mWakeupThreadFD; - - // timestamp captured when a packet is received - int64_t mLastPacketRxLocalTime; - - // ID of the timeline that this device is following - uint64_t mTimelineID; - - // flag for whether the clock has been synced to a timeline - bool mClockSynced; - - // flag used to indicate that clients should be considered to be lower - // priority than all of their peers during elections. This flag is set and - // cleared by the state machine. It is set when the client joins a new - // network. If the client had been a master in the old network (or an - // isolated master with no network connectivity) it should defer to any - // masters which may already be on the network. It will be cleared whenever - // the state machine transitions to the master state. - bool mForceLowPriority; - inline void setForceLowPriority(bool val) { - mForceLowPriority = val; - if (mState == ICommonClock::STATE_MASTER) - mClient_MasterDevicePriority = effectivePriority(); - } - - // Lock to synchronize access to internal state and configuration. - Mutex mLock; - - // Flag updated by the common clock service to indicate that it does or does - // not currently have registered clients. When the the auto disable flag is - // cleared on the common time service, the service will participate in - // network synchronization whenever it has a valid network interface to bind - // to. When the auto disable flag is set on the common time service, it - // will only participate in network synchronization when it has both a valid - // interface AND currently active common clock clients. - bool mCommonClockHasClients; - - // Internal logs used for dumpsys. - LogRing mStateChangeLog; - LogRing mElectionLog; - LogRing mBadPktLog; - - // Configuration info - struct sockaddr_storage mMasterElectionEP; // Endpoint over which we conduct master election - String8 mBindIface; // Endpoint for the service to bind to. - bool mBindIfaceValid; // whether or not the bind Iface is valid. - bool mBindIfaceDirty; // whether or not the bind Iface is valid. - struct sockaddr_storage mMasterEP; // Endpoint of our current master (if any) - bool mMasterEPValid; - uint64_t mDeviceID; // unique ID of this device - uint64_t mSyncGroupID; // synchronization group ID of this device. - uint8_t mMasterPriority; // Priority of this device in master election. - uint32_t mMasterAnnounceIntervalMs; - uint32_t mSyncRequestIntervalMs; - uint32_t mPanicThresholdUsec; - bool mAutoDisable; - - // Config defaults. - static const char* kDefaultMasterElectionAddr; - static const uint16_t kDefaultMasterElectionPort; - static const uint64_t kDefaultSyncGroupID; - static const uint8_t kDefaultMasterPriority; - static const uint32_t kDefaultMasterAnnounceIntervalMs; - static const uint32_t kDefaultSyncRequestIntervalMs; - static const uint32_t kDefaultPanicThresholdUsec; - static const bool kDefaultAutoDisable; - - // Priority mask and shift fields. - static const uint64_t kDeviceIDMask; - static const uint8_t kDevicePriorityMask; - static const uint8_t kDevicePriorityHiLowBit; - static const uint32_t kDevicePriorityShift; - - // Unconfgurable constants - static const int kSetupRetryTimeoutMs; - static const int64_t kNoGoodDataPanicThresholdUsec; - static const uint32_t kRTTDiscardPanicThreshMultiplier; - - /*** status while in the Initial state ***/ - int mInitial_WhoIsMasterRequestTimeouts; - static const int kInitial_NumWhoIsMasterRetries; - static const int kInitial_WhoIsMasterTimeoutMs; - - /*** status while in the Client state ***/ - uint64_t mClient_MasterDeviceID; - uint8_t mClient_MasterDevicePriority; - bool mClient_SyncRequestPending; - int mClient_SyncRequestTimeouts; - uint32_t mClient_SyncsSentToCurMaster; - uint32_t mClient_SyncRespsRXedFromCurMaster; - uint32_t mClient_ExpiredSyncRespsRXedFromCurMaster; - int64_t mClient_FirstSyncTX; - int64_t mClient_LastGoodSyncRX; - PacketRTTLog mClient_PacketRTTLog; - static const int kClient_NumSyncRequestRetries; - - - /*** status while in the Master state ***/ - static const uint32_t kDefaultMaster_AnnouncementIntervalMs; - - /*** status while in the Ronin state ***/ - int mRonin_WhoIsMasterRequestTimeouts; - static const int kRonin_NumWhoIsMasterRetries; - static const int kRonin_WhoIsMasterTimeoutMs; - - /*** status while in the WaitForElection state ***/ - static const int kWaitForElection_TimeoutMs; - - static const int kInfiniteTimeout; - - static const char* stateToString(ICommonClock::State s); - static void sockaddrToString(const sockaddr_storage& addr, bool addrValid, - char* buf, size_t bufLen); - static bool sockaddrMatch(const sockaddr_storage& a1, - const sockaddr_storage& a2, - bool matchAddressOnly); -}; - -} // namespace android - -#endif // ANDROID_COMMON_TIME_SERVER_H diff --git a/services/common_time/common_time_server_api.cpp b/services/common_time/common_time_server_api.cpp deleted file mode 100644 index e157071..0000000 --- a/services/common_time/common_time_server_api.cpp +++ /dev/null @@ -1,438 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * A service that exchanges time synchronization information between - * a master that defines a timeline and clients that follow the timeline. - */ - -#define LOG_TAG "common_time" -#include - -#include -#include - -#include "common_time_server.h" - -namespace android { - -// -// Clock API -// -uint64_t CommonTimeServer::getTimelineID() { - AutoMutex _lock(&mLock); - return mTimelineID; -} - -ICommonClock::State CommonTimeServer::getState() { - AutoMutex _lock(&mLock); - return mState; -} - -status_t CommonTimeServer::getMasterAddr(struct sockaddr_storage* addr) { - AutoMutex _lock(&mLock); - if (mMasterEPValid) { - memcpy(addr, &mMasterEP, sizeof(*addr)); - return OK; - } - - return UNKNOWN_ERROR; -} - -int32_t CommonTimeServer::getEstimatedError() { - AutoMutex _lock(&mLock); - - if (ICommonClock::STATE_MASTER == mState) - return 0; - - if (!mClockSynced) - return ICommonClock::kErrorEstimateUnknown; - - return mClockRecovery.getLastErrorEstimate(); -} - -status_t CommonTimeServer::isCommonTimeValid(bool* valid, - uint32_t* timelineID) { - AutoMutex _lock(&mLock); - *valid = mCommonClock.isValid(); - *timelineID = mTimelineID; - return OK; -} - -// -// Config API -// -status_t CommonTimeServer::getMasterElectionPriority(uint8_t *priority) { - AutoMutex _lock(&mLock); - *priority = mMasterPriority; - return OK; -} - -status_t CommonTimeServer::setMasterElectionPriority(uint8_t priority) { - AutoMutex _lock(&mLock); - - if (priority > 0x7F) - return BAD_VALUE; - - mMasterPriority = priority; - return OK; -} - -status_t CommonTimeServer::getMasterElectionEndpoint( - struct sockaddr_storage *addr) { - AutoMutex _lock(&mLock); - memcpy(addr, &mMasterElectionEP, sizeof(*addr)); - return OK; -} - -status_t CommonTimeServer::setMasterElectionEndpoint( - const struct sockaddr_storage *addr) { - AutoMutex _lock(&mLock); - - if (!addr) - return BAD_VALUE; - - // TODO: add proper support for IPv6 - if (addr->ss_family != AF_INET) - return BAD_VALUE; - - // Only multicast and broadcast endpoints with explicit ports are allowed. - uint16_t ipv4Port = ntohs( - reinterpret_cast(addr)->sin_port); - if (!ipv4Port) - return BAD_VALUE; - - uint32_t ipv4Addr = ntohl( - reinterpret_cast(addr)->sin_addr.s_addr); - if ((ipv4Addr != 0xFFFFFFFF) && (0xE0000000 != (ipv4Addr & 0xF0000000))) - return BAD_VALUE; - - memcpy(&mMasterElectionEP, addr, sizeof(mMasterElectionEP)); - - // Force a rebind in order to change election enpoints. - mBindIfaceDirty = true; - wakeupThread_l(); - return OK; -} - -status_t CommonTimeServer::getMasterElectionGroupId(uint64_t *id) { - AutoMutex _lock(&mLock); - *id = mSyncGroupID; - return OK; -} - -status_t CommonTimeServer::setMasterElectionGroupId(uint64_t id) { - AutoMutex _lock(&mLock); - mSyncGroupID = id; - return OK; -} - -status_t CommonTimeServer::getInterfaceBinding(String8& ifaceName) { - AutoMutex _lock(&mLock); - if (!mBindIfaceValid) - return INVALID_OPERATION; - ifaceName = mBindIface; - return OK; -} - -status_t CommonTimeServer::setInterfaceBinding(const String8& ifaceName) { - AutoMutex _lock(&mLock); - - mBindIfaceDirty = true; - if (ifaceName.size()) { - mBindIfaceValid = true; - mBindIface = ifaceName; - } else { - mBindIfaceValid = false; - mBindIface.clear(); - } - - wakeupThread_l(); - return OK; -} - -status_t CommonTimeServer::getMasterAnnounceInterval(int *interval) { - AutoMutex _lock(&mLock); - *interval = mMasterAnnounceIntervalMs; - return OK; -} - -status_t CommonTimeServer::setMasterAnnounceInterval(int interval) { - AutoMutex _lock(&mLock); - - if (interval > (6 *3600000)) // Max interval is once every 6 hrs - return BAD_VALUE; - - if (interval < 500) // Min interval is once per 0.5 seconds - return BAD_VALUE; - - mMasterAnnounceIntervalMs = interval; - if (ICommonClock::STATE_MASTER == mState) { - int pendingTimeout = mCurTimeout.msecTillTimeout(); - if ((kInfiniteTimeout == pendingTimeout) || - (pendingTimeout > interval)) { - mCurTimeout.setTimeout(mMasterAnnounceIntervalMs); - wakeupThread_l(); - } - } - - return OK; -} - -status_t CommonTimeServer::getClientSyncInterval(int *interval) { - AutoMutex _lock(&mLock); - *interval = mSyncRequestIntervalMs; - return OK; -} - -status_t CommonTimeServer::setClientSyncInterval(int interval) { - AutoMutex _lock(&mLock); - - if (interval > (3600000)) // Max interval is once every 60 min - return BAD_VALUE; - - if (interval < 250) // Min interval is once per 0.25 seconds - return BAD_VALUE; - - mSyncRequestIntervalMs = interval; - if (ICommonClock::STATE_CLIENT == mState) { - int pendingTimeout = mCurTimeout.msecTillTimeout(); - if ((kInfiniteTimeout == pendingTimeout) || - (pendingTimeout > interval)) { - mCurTimeout.setTimeout(mSyncRequestIntervalMs); - wakeupThread_l(); - } - } - - return OK; -} - -status_t CommonTimeServer::getPanicThreshold(int *threshold) { - AutoMutex _lock(&mLock); - *threshold = mPanicThresholdUsec; - return OK; -} - -status_t CommonTimeServer::setPanicThreshold(int threshold) { - AutoMutex _lock(&mLock); - - if (threshold < 1000) // Min threshold is 1mSec - return BAD_VALUE; - - mPanicThresholdUsec = threshold; - return OK; -} - -status_t CommonTimeServer::getAutoDisable(bool *autoDisable) { - AutoMutex _lock(&mLock); - *autoDisable = mAutoDisable; - return OK; -} - -status_t CommonTimeServer::setAutoDisable(bool autoDisable) { - AutoMutex _lock(&mLock); - mAutoDisable = autoDisable; - wakeupThread_l(); - return OK; -} - -status_t CommonTimeServer::forceNetworklessMasterMode() { - AutoMutex _lock(&mLock); - - // Can't force networkless master mode if we are currently bound to a - // network. - if (mSocket >= 0) - return INVALID_OPERATION; - - becomeMaster("force networkless"); - - return OK; -} - -void CommonTimeServer::reevaluateAutoDisableState(bool commonClockHasClients) { - AutoMutex _lock(&mLock); - bool needWakeup = (mAutoDisable && mMasterEPValid && - (commonClockHasClients != mCommonClockHasClients)); - - mCommonClockHasClients = commonClockHasClients; - - if (needWakeup) { - ALOGI("Waking up service, auto-disable is engaged and service now has%s" - " clients", mCommonClockHasClients ? "" : " no"); - wakeupThread_l(); - } -} - -#define dump_printf(a, b...) do { \ - int res; \ - res = snprintf(buffer, sizeof(buffer), a, b); \ - buffer[sizeof(buffer) - 1] = 0; \ - if (res > 0) \ - write(fd, buffer, res); \ -} while (0) -#define checked_percentage(a, b) ((0 == b) ? 0.0f : ((100.0f * a) / b)) - -status_t CommonTimeServer::dumpClockInterface(int fd, - const Vector& args, - size_t activeClients) { - AutoMutex _lock(&mLock); - const size_t SIZE = 256; - char buffer[SIZE]; - - if (checkCallingPermission(String16("android.permission.DUMP")) == false) { - snprintf(buffer, SIZE, "Permission Denial: " - "can't dump CommonClockService from pid=%d, uid=%d\n", - IPCThreadState::self()->getCallingPid(), - IPCThreadState::self()->getCallingUid()); - write(fd, buffer, strlen(buffer)); - } else { - int64_t commonTime; - int64_t localTime; - bool synced; - char maStr[64]; - - localTime = mLocalClock.getLocalTime(); - synced = (OK == mCommonClock.localToCommon(localTime, &commonTime)); - sockaddrToString(mMasterEP, mMasterEPValid, maStr, sizeof(maStr)); - - dump_printf("Common Clock Service Status\nLocal time : %lld\n", - localTime); - - if (synced) - dump_printf("Common time : %lld\n", commonTime); - else - dump_printf("Common time : %s\n", "not synced"); - - dump_printf("Timeline ID : %016llx\n", mTimelineID); - dump_printf("State : %s\n", stateToString(mState)); - dump_printf("Master Addr : %s\n", maStr); - - - if (synced) { - int32_t est = (ICommonClock::STATE_MASTER != mState) - ? mClockRecovery.getLastErrorEstimate() - : 0; - dump_printf("Error Est. : %.3f msec\n", - static_cast(est) / 1000.0); - } else { - dump_printf("Error Est. : %s\n", "unknown"); - } - - dump_printf("Syncs TXes : %u\n", mClient_SyncsSentToCurMaster); - dump_printf("Syncs RXes : %u (%.2f%%)\n", - mClient_SyncRespsRXedFromCurMaster, - checked_percentage( - mClient_SyncRespsRXedFromCurMaster, - mClient_SyncsSentToCurMaster)); - dump_printf("RXs Expired : %u (%.2f%%)\n", - mClient_ExpiredSyncRespsRXedFromCurMaster, - checked_percentage( - mClient_ExpiredSyncRespsRXedFromCurMaster, - mClient_SyncsSentToCurMaster)); - - if (!mClient_LastGoodSyncRX) { - dump_printf("Last Good RX : %s\n", "unknown"); - } else { - int64_t localDelta, usecDelta; - localDelta = localTime - mClient_LastGoodSyncRX; - usecDelta = mCommonClock.localDurationToCommonDuration(localDelta); - dump_printf("Last Good RX : %lld uSec ago\n", usecDelta); - } - - dump_printf("Active Clients : %u\n", activeClients); - mClient_PacketRTTLog.dumpLog(fd, mCommonClock); - mStateChangeLog.dumpLog(fd); - mElectionLog.dumpLog(fd); - mBadPktLog.dumpLog(fd); - } - - return NO_ERROR; -} - -status_t CommonTimeServer::dumpConfigInterface(int fd, - const Vector& args) { - AutoMutex _lock(&mLock); - const size_t SIZE = 256; - char buffer[SIZE]; - - if (checkCallingPermission(String16("android.permission.DUMP")) == false) { - snprintf(buffer, SIZE, "Permission Denial: " - "can't dump CommonTimeConfigService from pid=%d, uid=%d\n", - IPCThreadState::self()->getCallingPid(), - IPCThreadState::self()->getCallingUid()); - write(fd, buffer, strlen(buffer)); - } else { - char meStr[64]; - - sockaddrToString(mMasterElectionEP, true, meStr, sizeof(meStr)); - - dump_printf("Common Time Config Service Status\n" - "Bound Interface : %s\n", - mBindIfaceValid ? mBindIface.string() : ""); - dump_printf("Master Election Endpoint : %s\n", meStr); - dump_printf("Master Election Group ID : %016llx\n", mSyncGroupID); - dump_printf("Master Announce Interval : %d mSec\n", - mMasterAnnounceIntervalMs); - dump_printf("Client Sync Interval : %d mSec\n", - mSyncRequestIntervalMs); - dump_printf("Panic Threshold : %d uSec\n", - mPanicThresholdUsec); - dump_printf("Base ME Prio : 0x%02x\n", - static_cast(mMasterPriority)); - dump_printf("Effective ME Prio : 0x%02x\n", - static_cast(effectivePriority())); - dump_printf("Auto Disable Allowed : %s\n", - mAutoDisable ? "yes" : "no"); - dump_printf("Auto Disable Engaged : %s\n", - shouldAutoDisable() ? "yes" : "no"); - } - - return NO_ERROR; -} - -void CommonTimeServer::PacketRTTLog::dumpLog(int fd, const CommonClock& cclk) { - const size_t SIZE = 256; - char buffer[SIZE]; - uint32_t avail = !logFull ? wrPtr : RTT_LOG_SIZE; - - if (!avail) - return; - - dump_printf("\nPacket Log (%d entries)\n", avail); - - uint32_t ndx = 0; - uint32_t i = logFull ? wrPtr : 0; - do { - if (rxTimes[i]) { - int64_t delta = rxTimes[i] - txTimes[i]; - int64_t deltaUsec = cclk.localDurationToCommonDuration(delta); - dump_printf("pkt[%2d] : localTX %12lld localRX %12lld " - "(%.3f msec RTT)\n", - ndx, txTimes[i], rxTimes[i], - static_cast(deltaUsec) / 1000.0); - } else { - dump_printf("pkt[%2d] : localTX %12lld localRX never\n", - ndx, txTimes[i]); - } - i = (i + 1) % RTT_LOG_SIZE; - ndx++; - } while (i != wrPtr); -} - -#undef dump_printf -#undef checked_percentage - -} // namespace android diff --git a/services/common_time/common_time_server_packets.cpp b/services/common_time/common_time_server_packets.cpp deleted file mode 100644 index 9833c37..0000000 --- a/services/common_time/common_time_server_packets.cpp +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * A service that exchanges time synchronization information between - * a master that defines a timeline and clients that follow the timeline. - */ - -#define LOG_TAG "common_time" -#include - -#include -#include - -#include "common_time_server_packets.h" - -namespace android { - -const uint32_t TimeServicePacketHeader::kMagic = - (static_cast('c') << 24) | - (static_cast('c') << 16) | - (static_cast('l') << 8) | - static_cast('k'); - -const uint16_t TimeServicePacketHeader::kCurVersion = 1; - -#define SERIALIZE_FIELD(field_name, type, converter) \ - do { \ - if ((offset + sizeof(field_name)) > length) \ - return -1; \ - *((type*)(data + offset)) = converter(field_name); \ - offset += sizeof(field_name); \ - } while (0) -#define SERIALIZE_INT16(field_name) SERIALIZE_FIELD(field_name, int16_t, htons) -#define SERIALIZE_INT32(field_name) SERIALIZE_FIELD(field_name, int32_t, htonl) -#define SERIALIZE_INT64(field_name) SERIALIZE_FIELD(field_name, int64_t, htonq) - -#define DESERIALIZE_FIELD(field_name, type, converter) \ - do { \ - if ((offset + sizeof(field_name)) > length) \ - return -1; \ - field_name = converter(*((type*)(data + offset))); \ - offset += sizeof(field_name); \ - } while (0) -#define DESERIALIZE_INT16(field_name) DESERIALIZE_FIELD(field_name, int16_t, ntohs) -#define DESERIALIZE_INT32(field_name) DESERIALIZE_FIELD(field_name, int32_t, ntohl) -#define DESERIALIZE_INT64(field_name) DESERIALIZE_FIELD(field_name, int64_t, ntohq) - -#define kDevicePriorityShift 56 -#define kDeviceIDMask ((static_cast(1) << kDevicePriorityShift) - 1) - -inline uint64_t packDeviceID(uint64_t devID, uint8_t prio) { - return (devID & kDeviceIDMask) | - (static_cast(prio) << kDevicePriorityShift); -} - -inline uint64_t unpackDeviceID(uint64_t packed) { - return (packed & kDeviceIDMask); -} - -inline uint8_t unpackDevicePriority(uint64_t packed) { - return static_cast(packed >> kDevicePriorityShift); -} - -ssize_t TimeServicePacketHeader::serializeHeader(uint8_t* data, - uint32_t length) { - ssize_t offset = 0; - int16_t pktType = static_cast(packetType); - SERIALIZE_INT32(magic); - SERIALIZE_INT16(version); - SERIALIZE_INT16(pktType); - SERIALIZE_INT64(timelineID); - SERIALIZE_INT64(syncGroupID); - return offset; -} - -ssize_t TimeServicePacketHeader::deserializeHeader(const uint8_t* data, - uint32_t length) { - ssize_t offset = 0; - int16_t tmp; - DESERIALIZE_INT32(magic); - DESERIALIZE_INT16(version); - DESERIALIZE_INT16(tmp); - DESERIALIZE_INT64(timelineID); - DESERIALIZE_INT64(syncGroupID); - packetType = static_cast(tmp); - return offset; -} - -ssize_t TimeServicePacketHeader::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t ret, tmp; - - ret = serializeHeader(data, length); - if (ret < 0) - return ret; - - data += ret; - length -= ret; - - switch (packetType) { - case TIME_PACKET_WHO_IS_MASTER_REQUEST: - tmp =((WhoIsMasterRequestPacket*)(this))->serializePacket(data, - length); - break; - case TIME_PACKET_WHO_IS_MASTER_RESPONSE: - tmp =((WhoIsMasterResponsePacket*)(this))->serializePacket(data, - length); - break; - case TIME_PACKET_SYNC_REQUEST: - tmp =((SyncRequestPacket*)(this))->serializePacket(data, length); - break; - case TIME_PACKET_SYNC_RESPONSE: - tmp =((SyncResponsePacket*)(this))->serializePacket(data, length); - break; - case TIME_PACKET_MASTER_ANNOUNCEMENT: - tmp =((MasterAnnouncementPacket*)(this))->serializePacket(data, - length); - break; - default: - return -1; - } - - if (tmp < 0) - return tmp; - - return ret + tmp; -} - -ssize_t UniversalTimeServicePacket::deserializePacket( - const uint8_t* data, - uint32_t length, - uint64_t expectedSyncGroupID) { - ssize_t ret; - TimeServicePacketHeader* header; - if (length < 8) - return -1; - - packetType = ntohs(*((uint16_t*)(data + 6))); - switch (packetType) { - case TIME_PACKET_WHO_IS_MASTER_REQUEST: - ret = p.who_is_master_request.deserializePacket(data, length); - header = &p.who_is_master_request; - break; - case TIME_PACKET_WHO_IS_MASTER_RESPONSE: - ret = p.who_is_master_response.deserializePacket(data, length); - header = &p.who_is_master_response; - break; - case TIME_PACKET_SYNC_REQUEST: - ret = p.sync_request.deserializePacket(data, length); - header = &p.sync_request; - break; - case TIME_PACKET_SYNC_RESPONSE: - ret = p.sync_response.deserializePacket(data, length); - header = &p.sync_response; - break; - case TIME_PACKET_MASTER_ANNOUNCEMENT: - ret = p.master_announcement.deserializePacket(data, length); - header = &p.master_announcement; - break; - default: - return -1; - } - - if ((ret >= 0) && !header->checkPacket(expectedSyncGroupID)) - ret = -1; - - return ret; -} - -ssize_t WhoIsMasterRequestPacket::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t offset = serializeHeader(data, length); - if (offset > 0) { - uint64_t packed = packDeviceID(senderDeviceID, senderDevicePriority); - SERIALIZE_INT64(packed); - } - return offset; -} - -ssize_t WhoIsMasterRequestPacket::deserializePacket(const uint8_t* data, - uint32_t length) { - ssize_t offset = deserializeHeader(data, length); - if (offset > 0) { - uint64_t packed; - DESERIALIZE_INT64(packed); - senderDeviceID = unpackDeviceID(packed); - senderDevicePriority = unpackDevicePriority(packed); - } - return offset; -} - -ssize_t WhoIsMasterResponsePacket::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t offset = serializeHeader(data, length); - if (offset > 0) { - uint64_t packed = packDeviceID(deviceID, devicePriority); - SERIALIZE_INT64(packed); - } - return offset; -} - -ssize_t WhoIsMasterResponsePacket::deserializePacket(const uint8_t* data, - uint32_t length) { - ssize_t offset = deserializeHeader(data, length); - if (offset > 0) { - uint64_t packed; - DESERIALIZE_INT64(packed); - deviceID = unpackDeviceID(packed); - devicePriority = unpackDevicePriority(packed); - } - return offset; -} - -ssize_t SyncRequestPacket::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t offset = serializeHeader(data, length); - if (offset > 0) { - SERIALIZE_INT64(clientTxLocalTime); - } - return offset; -} - -ssize_t SyncRequestPacket::deserializePacket(const uint8_t* data, - uint32_t length) { - ssize_t offset = deserializeHeader(data, length); - if (offset > 0) { - DESERIALIZE_INT64(clientTxLocalTime); - } - return offset; -} - -ssize_t SyncResponsePacket::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t offset = serializeHeader(data, length); - if (offset > 0) { - SERIALIZE_INT64(clientTxLocalTime); - SERIALIZE_INT64(masterRxCommonTime); - SERIALIZE_INT64(masterTxCommonTime); - SERIALIZE_INT32(nak); - } - return offset; -} - -ssize_t SyncResponsePacket::deserializePacket(const uint8_t* data, - uint32_t length) { - ssize_t offset = deserializeHeader(data, length); - if (offset > 0) { - DESERIALIZE_INT64(clientTxLocalTime); - DESERIALIZE_INT64(masterRxCommonTime); - DESERIALIZE_INT64(masterTxCommonTime); - DESERIALIZE_INT32(nak); - } - return offset; -} - -ssize_t MasterAnnouncementPacket::serializePacket(uint8_t* data, - uint32_t length) { - ssize_t offset = serializeHeader(data, length); - if (offset > 0) { - uint64_t packed = packDeviceID(deviceID, devicePriority); - SERIALIZE_INT64(packed); - } - return offset; -} - -ssize_t MasterAnnouncementPacket::deserializePacket(const uint8_t* data, - uint32_t length) { - ssize_t offset = deserializeHeader(data, length); - if (offset > 0) { - uint64_t packed; - DESERIALIZE_INT64(packed); - deviceID = unpackDeviceID(packed); - devicePriority = unpackDevicePriority(packed); - } - return offset; -} - -} // namespace android - diff --git a/services/common_time/common_time_server_packets.h b/services/common_time/common_time_server_packets.h deleted file mode 100644 index 57ba8a2..0000000 --- a/services/common_time/common_time_server_packets.h +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_COMMON_TIME_SERVER_PACKETS_H -#define ANDROID_COMMON_TIME_SERVER_PACKETS_H - -#include -#include - -namespace android { - -/***** time sync protocol packets *****/ - -enum TimeServicePacketType { - TIME_PACKET_WHO_IS_MASTER_REQUEST = 1, - TIME_PACKET_WHO_IS_MASTER_RESPONSE, - TIME_PACKET_SYNC_REQUEST, - TIME_PACKET_SYNC_RESPONSE, - TIME_PACKET_MASTER_ANNOUNCEMENT, -}; - -class TimeServicePacketHeader { - public: - friend class UniversalTimeServicePacket; - // magic number identifying the protocol - uint32_t magic; - - // protocol version of the packet - uint16_t version; - - // type of the packet - TimeServicePacketType packetType; - - // the timeline ID - uint64_t timelineID; - - // synchronization group this packet belongs to (used to operate multiple - // synchronization domains which all use the same master election endpoint) - uint64_t syncGroupID; - - ssize_t serializePacket(uint8_t* data, uint32_t length); - - protected: - void initHeader(TimeServicePacketType type, - const uint64_t tlID, - const uint64_t groupID) { - magic = kMagic; - version = kCurVersion; - packetType = type; - timelineID = tlID; - syncGroupID = groupID; - } - - bool checkPacket(uint64_t expectedSyncGroupID) const { - return ((magic == kMagic) && - (version == kCurVersion) && - (!expectedSyncGroupID || (syncGroupID == expectedSyncGroupID))); - } - - ssize_t serializeHeader(uint8_t* data, uint32_t length); - ssize_t deserializeHeader(const uint8_t* data, uint32_t length); - - private: - static const uint32_t kMagic; - static const uint16_t kCurVersion; -}; - -// packet querying for a suitable master -class WhoIsMasterRequestPacket : public TimeServicePacketHeader { - public: - uint64_t senderDeviceID; - uint8_t senderDevicePriority; - - void initHeader(const uint64_t groupID) { - TimeServicePacketHeader::initHeader(TIME_PACKET_WHO_IS_MASTER_REQUEST, - ICommonClock::kInvalidTimelineID, - groupID); - } - - ssize_t serializePacket(uint8_t* data, uint32_t length); - ssize_t deserializePacket(const uint8_t* data, uint32_t length); -}; - -// response to a WhoIsMaster request -class WhoIsMasterResponsePacket : public TimeServicePacketHeader { - public: - uint64_t deviceID; - uint8_t devicePriority; - - void initHeader(const uint64_t tlID, const uint64_t groupID) { - TimeServicePacketHeader::initHeader(TIME_PACKET_WHO_IS_MASTER_RESPONSE, - tlID, groupID); - } - - ssize_t serializePacket(uint8_t* data, uint32_t length); - ssize_t deserializePacket(const uint8_t* data, uint32_t length); -}; - -// packet sent by a client requesting correspondence between local -// and common time -class SyncRequestPacket : public TimeServicePacketHeader { - public: - // local time when this request was transmitted - int64_t clientTxLocalTime; - - void initHeader(const uint64_t tlID, const uint64_t groupID) { - TimeServicePacketHeader::initHeader(TIME_PACKET_SYNC_REQUEST, - tlID, groupID); - } - - ssize_t serializePacket(uint8_t* data, uint32_t length); - ssize_t deserializePacket(const uint8_t* data, uint32_t length); -}; - -// response to a sync request sent by the master -class SyncResponsePacket : public TimeServicePacketHeader { - public: - // local time when this request was transmitted by the client - int64_t clientTxLocalTime; - - // common time when the master received the request - int64_t masterRxCommonTime; - - // common time when the master transmitted the response - int64_t masterTxCommonTime; - - // flag that is set if the recipient of the sync request is not acting - // as a master for the requested timeline - uint32_t nak; - - void initHeader(const uint64_t tlID, const uint64_t groupID) { - TimeServicePacketHeader::initHeader(TIME_PACKET_SYNC_RESPONSE, - tlID, groupID); - } - - ssize_t serializePacket(uint8_t* data, uint32_t length); - ssize_t deserializePacket(const uint8_t* data, uint32_t length); -}; - -// announcement of the master's presence -class MasterAnnouncementPacket : public TimeServicePacketHeader { - public: - // the master's device ID - uint64_t deviceID; - uint8_t devicePriority; - - void initHeader(const uint64_t tlID, const uint64_t groupID) { - TimeServicePacketHeader::initHeader(TIME_PACKET_MASTER_ANNOUNCEMENT, - tlID, groupID); - } - - ssize_t serializePacket(uint8_t* data, uint32_t length); - ssize_t deserializePacket(const uint8_t* data, uint32_t length); -}; - -class UniversalTimeServicePacket { - public: - uint16_t packetType; - union { - WhoIsMasterRequestPacket who_is_master_request; - WhoIsMasterResponsePacket who_is_master_response; - SyncRequestPacket sync_request; - SyncResponsePacket sync_response; - MasterAnnouncementPacket master_announcement; - } p; - - ssize_t deserializePacket(const uint8_t* data, - uint32_t length, - uint64_t expectedSyncGroupID); -}; - -}; // namespace android - -#endif // ANDROID_COMMON_TIME_SERVER_PACKETS_H - - diff --git a/services/common_time/diag_thread.cpp b/services/common_time/diag_thread.cpp deleted file mode 100644 index 4cb9551..0000000 --- a/services/common_time/diag_thread.cpp +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#define LOG_TAG "common_time" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "common_clock.h" -#include "diag_thread.h" - -#define kMaxEvents 16 -#define kListenPort 9876 - -static bool setNonblocking(int fd) { - int flags = fcntl(fd, F_GETFL); - if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { - ALOGE("Failed to set socket (%d) to non-blocking mode (errno %d)", - fd, errno); - return false; - } - - return true; -} - -static bool setNodelay(int fd) { - int tmp = 1; - if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &tmp, sizeof(tmp)) < 0) { - ALOGE("Failed to set socket (%d) to no-delay mode (errno %d)", - fd, errno); - return false; - } - - return true; -} - -namespace android { - -DiagThread::DiagThread(CommonClock* common_clock, LocalClock* local_clock) { - common_clock_ = common_clock; - local_clock_ = local_clock; - listen_fd_ = -1; - data_fd_ = -1; - kernel_logID_basis_known_ = false; - discipline_log_ID_ = 0; -} - -DiagThread::~DiagThread() { -} - -status_t DiagThread::startWorkThread() { - status_t res; - stopWorkThread(); - res = run("Diag"); - - if (res != OK) - ALOGE("Failed to start work thread (res = %d)", res); - - return res; -} - -void DiagThread::stopWorkThread() { - status_t res; - res = requestExitAndWait(); // block until thread exit. - if (res != OK) - ALOGE("Failed to stop work thread (res = %d)", res); -} - -bool DiagThread::openListenSocket() { - bool ret = false; - int flags; - cleanupListenSocket(); - - if ((listen_fd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { - ALOGE("Socket failed."); - goto bailout; - } - - // Set non-blocking operation - if (!setNonblocking(listen_fd_)) - goto bailout; - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(kListenPort); - - if (bind(listen_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - ALOGE("Bind failed."); - goto bailout; - } - - if (listen(listen_fd_, 1) < 0) { - ALOGE("Listen failed."); - goto bailout; - } - - ret = true; -bailout: - if (!ret) - cleanupListenSocket(); - - return ret; -} - -void DiagThread::cleanupListenSocket() { - if (listen_fd_ >= 0) { - int res; - - struct linger l; - l.l_onoff = 1; - l.l_linger = 0; - - setsockopt(listen_fd_, SOL_SOCKET, SO_LINGER, &l, sizeof(l)); - shutdown(listen_fd_, SHUT_RDWR); - close(listen_fd_); - listen_fd_ = -1; - } -} - -void DiagThread::cleanupDataSocket() { - if (data_fd_ >= 0) { - int res; - - struct linger l; - l.l_onoff = 1; - l.l_linger = 0; - - setsockopt(data_fd_, SOL_SOCKET, SO_LINGER, &l, sizeof(l)); - shutdown(data_fd_, SHUT_RDWR); - close(data_fd_); - data_fd_ = -1; - } -} - -void DiagThread::resetLogIDs() { - // Drain and discard all of the events from the kernel - struct local_time_debug_event events[kMaxEvents]; - while(local_clock_->getDebugLog(events, kMaxEvents) > 0) - ; - - { - Mutex::Autolock lock(&discipline_log_lock_); - discipline_log_.clear(); - discipline_log_ID_ = 0; - } - - kernel_logID_basis_known_ = false; -} - -void DiagThread::pushDisciplineEvent(int64_t observed_local_time, - int64_t observed_common_time, - int64_t nominal_common_time, - int32_t total_correction, - int32_t rtt) { - Mutex::Autolock lock(&discipline_log_lock_); - - DisciplineEventRecord evt; - - evt.event_id = discipline_log_ID_++; - - evt.action_local_time = local_clock_->getLocalTime(); - common_clock_->localToCommon(evt.action_local_time, - &evt.action_common_time); - - evt.observed_local_time = observed_local_time; - evt.observed_common_time = observed_common_time; - evt.nominal_common_time = nominal_common_time; - evt.total_correction = total_correction; - evt.rtt = rtt; - - discipline_log_.push_back(evt); - while (discipline_log_.size() > kMaxDisciplineLogSize) - discipline_log_.erase(discipline_log_.begin()); -} - -bool DiagThread::threadLoop() { - struct pollfd poll_fds[1]; - - if (!openListenSocket()) { - ALOGE("Failed to open listen socket"); - goto bailout; - } - - while (!exitPending()) { - memset(&poll_fds, 0, sizeof(poll_fds)); - - if (data_fd_ < 0) { - poll_fds[0].fd = listen_fd_; - poll_fds[0].events = POLLIN; - } else { - poll_fds[0].fd = data_fd_; - poll_fds[0].events = POLLRDHUP | POLLIN; - } - - int poll_res = poll(poll_fds, NELEM(poll_fds), 50); - if (poll_res < 0) { - ALOGE("Fatal error (%d,%d) while waiting on events", - poll_res, errno); - goto bailout; - } - - if (exitPending()) - break; - - if (poll_fds[0].revents) { - if (poll_fds[0].fd == listen_fd_) { - data_fd_ = accept(listen_fd_, NULL, NULL); - - if (data_fd_ < 0) { - ALOGW("Failed accept on socket %d with err %d", - listen_fd_, errno); - } else { - if (!setNonblocking(data_fd_)) - cleanupDataSocket(); - if (!setNodelay(data_fd_)) - cleanupDataSocket(); - } - } else - if (poll_fds[0].fd == data_fd_) { - if (poll_fds[0].revents & POLLRDHUP) { - // Connection hung up; time to clean up. - cleanupDataSocket(); - } else - if (poll_fds[0].revents & POLLIN) { - uint8_t cmd; - if (read(data_fd_, &cmd, sizeof(cmd)) > 0) { - switch(cmd) { - case 'r': - case 'R': - resetLogIDs(); - break; - } - } - } - } - } - - struct local_time_debug_event events[kMaxEvents]; - int amt = local_clock_->getDebugLog(events, kMaxEvents); - - if (amt > 0) { - for (int i = 0; i < amt; i++) { - struct local_time_debug_event& e = events[i]; - - if (!kernel_logID_basis_known_) { - kernel_logID_basis_ = e.local_timesync_event_id; - kernel_logID_basis_known_ = true; - } - - char buf[1024]; - int64_t common_time; - status_t res = common_clock_->localToCommon(e.local_time, - &common_time); - snprintf(buf, sizeof(buf), "E,%lld,%lld,%lld,%d\n", - e.local_timesync_event_id - kernel_logID_basis_, - e.local_time, - common_time, - (OK == res) ? 1 : 0); - buf[sizeof(buf) - 1] = 0; - - if (data_fd_ >= 0) - write(data_fd_, buf, strlen(buf)); - } - } - - { // scope for autolock pattern - Mutex::Autolock lock(&discipline_log_lock_); - - while (discipline_log_.size() > 0) { - char buf[1024]; - DisciplineEventRecord& e = *discipline_log_.begin(); - snprintf(buf, sizeof(buf), - "D,%lld,%lld,%lld,%lld,%lld,%lld,%d,%d\n", - e.event_id, - e.action_local_time, - e.action_common_time, - e.observed_local_time, - e.observed_common_time, - e.nominal_common_time, - e.total_correction, - e.rtt); - buf[sizeof(buf) - 1] = 0; - - if (data_fd_ >= 0) - write(data_fd_, buf, strlen(buf)); - - discipline_log_.erase(discipline_log_.begin()); - } - } - } - -bailout: - cleanupDataSocket(); - cleanupListenSocket(); - return false; -} - -} // namespace android diff --git a/services/common_time/diag_thread.h b/services/common_time/diag_thread.h deleted file mode 100644 index c630e0d..0000000 --- a/services/common_time/diag_thread.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __DIAG_THREAD_H__ -#define __DIAG_THREAD_H__ - -#include -#include - -namespace android { - -class CommonClock; -class LocalClock; - -class DiagThread : public Thread { - public: - DiagThread(CommonClock* common_clock, LocalClock* local_clock); - ~DiagThread(); - - status_t startWorkThread(); - void stopWorkThread(); - virtual bool threadLoop(); - - void pushDisciplineEvent(int64_t observed_local_time, - int64_t observed_common_time, - int64_t nominal_common_time, - int32_t total_correction, - int32_t rtt); - - private: - typedef struct { - int64_t event_id; - int64_t action_local_time; - int64_t action_common_time; - int64_t observed_local_time; - int64_t observed_common_time; - int64_t nominal_common_time; - int32_t total_correction; - int32_t rtt; - } DisciplineEventRecord; - - bool openListenSocket(); - void cleanupListenSocket(); - void cleanupDataSocket(); - void resetLogIDs(); - - CommonClock* common_clock_; - LocalClock* local_clock_; - int listen_fd_; - int data_fd_; - - int64_t kernel_logID_basis_; - bool kernel_logID_basis_known_; - - static const size_t kMaxDisciplineLogSize = 16; - Mutex discipline_log_lock_; - List discipline_log_; - int64_t discipline_log_ID_; -}; - -} // namespace android - -#endif //__ DIAG_THREAD_H__ diff --git a/services/common_time/main.cpp b/services/common_time/main.cpp deleted file mode 100644 index 49eb30a..0000000 --- a/services/common_time/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * A service that exchanges time synchronization information between - * a master that defines a timeline and clients that follow the timeline. - */ - -#define LOG_TAG "common_time" -#include - -#include -#include - -#include "common_time_server.h" - -int main(int argc, char *argv[]) { - using namespace android; - - sp service = new CommonTimeServer(); - if (service == NULL) - return 1; - - ProcessState::self()->startThreadPool(); - service->run("CommonTimeServer", ANDROID_PRIORITY_NORMAL); - - IPCThreadState::self()->joinThreadPool(); - return 0; -} - diff --git a/services/common_time/utils.cpp b/services/common_time/utils.cpp deleted file mode 100644 index ed2c77d..0000000 --- a/services/common_time/utils.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#define LOG_TAG "common_time" -#include - -#include "utils.h" - -namespace android { - -void Timeout::setTimeout(int msec) { - if (msec < 0) { - mSystemEndTime = 0; - return; - } - - mSystemEndTime = systemTime() + (static_cast(msec) * 1000000); -} - -int Timeout::msecTillTimeout(nsecs_t nowTime) { - if (!mSystemEndTime) { - return -1; - } - - if (mSystemEndTime < nowTime) { - return 0; - } - - nsecs_t delta = mSystemEndTime - nowTime; - delta += 999999; - delta /= 1000000; - if (delta > 0x7FFFFFFF) { - return 0x7FFFFFFF; - } - - return static_cast(delta); -} - -LogRing::LogRing(const char* header, size_t entries) - : mSize(entries) - , mWr(0) - , mIsFull(false) - , mHeader(header) { - mRingBuffer = new Entry[mSize]; - if (NULL == mRingBuffer) - ALOGE("Failed to allocate log ring with %u entries.", mSize); -} - -LogRing::~LogRing() { - if (NULL != mRingBuffer) - delete[] mRingBuffer; -} - -void LogRing::log(int prio, const char* tag, const char* fmt, ...) { - va_list argp; - va_start(argp, fmt); - internalLog(prio, tag, fmt, argp); - va_end(argp); -} - -void LogRing::log(const char* fmt, ...) { - va_list argp; - va_start(argp, fmt); - internalLog(0, NULL, fmt, argp); - va_end(argp); -} - -void LogRing::internalLog(int prio, - const char* tag, - const char* fmt, - va_list argp) { - if (NULL != mRingBuffer) { - Mutex::Autolock lock(&mLock); - String8 s(String8::formatV(fmt, argp)); - Entry* last = NULL; - - if (mIsFull || mWr) - last = &(mRingBuffer[(mWr + mSize - 1) % mSize]); - - - if ((NULL != last) && !last->s.compare(s)) { - gettimeofday(&(last->last_ts), NULL); - ++last->count; - } else { - gettimeofday(&mRingBuffer[mWr].first_ts, NULL); - mRingBuffer[mWr].last_ts = mRingBuffer[mWr].first_ts; - mRingBuffer[mWr].count = 1; - mRingBuffer[mWr].s.setTo(s); - - mWr = (mWr + 1) % mSize; - if (!mWr) - mIsFull = true; - } - } - - if (NULL != tag) - LOG_PRI_VA(prio, tag, fmt, argp); -} - -void LogRing::dumpLog(int fd) { - if (NULL == mRingBuffer) - return; - - Mutex::Autolock lock(&mLock); - - if (!mWr && !mIsFull) - return; - - char buf[1024]; - int res; - size_t start = mIsFull ? mWr : 0; - size_t count = mIsFull ? mSize : mWr; - static const char* kTimeFmt = "%a %b %d %Y %H:%M:%S"; - - res = snprintf(buf, sizeof(buf), "\n%s\n", mHeader); - if (res > 0) - write(fd, buf, res); - - for (size_t i = 0; i < count; ++i) { - struct tm t; - char timebuf[64]; - char repbuf[96]; - size_t ndx = (start + i) % mSize; - - if (1 != mRingBuffer[ndx].count) { - localtime_r(&mRingBuffer[ndx].last_ts.tv_sec, &t); - strftime(timebuf, sizeof(timebuf), kTimeFmt, &t); - snprintf(repbuf, sizeof(repbuf), - " (repeated %d times, last was %s.%03ld)", - mRingBuffer[ndx].count, - timebuf, - mRingBuffer[ndx].last_ts.tv_usec / 1000); - repbuf[sizeof(repbuf) - 1] = 0; - } else { - repbuf[0] = 0; - } - - localtime_r(&mRingBuffer[ndx].first_ts.tv_sec, &t); - strftime(timebuf, sizeof(timebuf), kTimeFmt, &t); - res = snprintf(buf, sizeof(buf), "[%2d] %s.%03ld :: %s%s\n", - i, timebuf, - mRingBuffer[ndx].first_ts.tv_usec / 1000, - mRingBuffer[ndx].s.string(), - repbuf); - - if (res > 0) - write(fd, buf, res); - } -} - -} // namespace android diff --git a/services/common_time/utils.h b/services/common_time/utils.h deleted file mode 100644 index c28cf0a..0000000 --- a/services/common_time/utils.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef __UTILS_H__ -#define __UTILS_H__ - -#include -#include - -#include -#include -#include - -namespace android { - -class Timeout { - public: - Timeout() : mSystemEndTime(0) { } - - // Set a timeout which should occur msec milliseconds from now. - // Negative values will cancel any current timeout; - void setTimeout(int msec); - - // Return the number of milliseconds until the timeout occurs, or -1 if - // no timeout is scheduled. - int msecTillTimeout(nsecs_t nowTime); - int msecTillTimeout() { return msecTillTimeout(systemTime()); } - - private: - // The systemTime() at which the timeout will be complete, or 0 if no - // timeout is currently scheduled. - nsecs_t mSystemEndTime; -}; - -class LogRing { - public: - LogRing(const char* header, size_t entries); - ~LogRing(); - - // Send a log message to logcat as well as storing it in the ring buffer. - void log(int prio, const char* tag, const char* fmt, ...); - - // Add a log message the ring buffer, do not send the message to logcat. - void log(const char* fmt, ...); - - // Dump the log to an fd (dumpsys style) - void dumpLog(int fd); - - private: - class Entry { - public: - uint32_t count; - struct timeval first_ts; - struct timeval last_ts; - String8 s; - }; - - Mutex mLock; - Entry* mRingBuffer; - size_t mSize; - size_t mWr; - bool mIsFull; - const char* mHeader; - - void internalLog(int prio, const char* tag, const char* fmt, va_list va); -}; - -} // namespace android - -#endif // __UTILS_H__ diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java new file mode 100644 index 0000000..3cdf170 --- /dev/null +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -0,0 +1,1515 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.Activity; +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.IAlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.WorkSource; +import android.text.TextUtils; +import android.util.Pair; +import android.util.Slog; +import android.util.TimeUtils; + +import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.TimeZone; + +import static android.app.AlarmManager.RTC_WAKEUP; +import static android.app.AlarmManager.RTC; +import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; +import static android.app.AlarmManager.ELAPSED_REALTIME; + +import com.android.internal.util.LocalLog; + +class AlarmManagerService extends SystemService { + // The threshold for how long an alarm can be late before we print a + // warning message. The time duration is in milliseconds. + private static final long LATE_ALARM_THRESHOLD = 10 * 1000; + + private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP; + private static final int RTC_MASK = 1 << RTC; + private static final int ELAPSED_REALTIME_WAKEUP_MASK = 1 << ELAPSED_REALTIME_WAKEUP; + private static final int ELAPSED_REALTIME_MASK = 1 << ELAPSED_REALTIME; + static final int TIME_CHANGED_MASK = 1 << 16; + static final int IS_WAKEUP_MASK = RTC_WAKEUP_MASK|ELAPSED_REALTIME_WAKEUP_MASK; + + // Mask for testing whether a given alarm type is wakeup vs non-wakeup + static final int TYPE_NONWAKEUP_MASK = 0x1; // low bit => non-wakeup + + static final String TAG = "AlarmManager"; + static final String ClockReceiver_TAG = "ClockReceiver"; + static final boolean localLOGV = false; + static final boolean DEBUG_BATCH = localLOGV || false; + static final boolean DEBUG_VALIDATE = localLOGV || false; + static final int ALARM_EVENT = 1; + static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; + + static final Intent mBackgroundIntent + = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND); + static final IncreasingTimeOrder sIncreasingTimeOrder = new IncreasingTimeOrder(); + + static final boolean WAKEUP_STATS = false; + + final LocalLog mLog = new LocalLog(TAG); + + final Object mLock = new Object(); + + long mNativeData; + private long mNextWakeup; + private long mNextNonWakeup; + int mBroadcastRefCount = 0; + PowerManager.WakeLock mWakeLock; + ArrayList mInFlight = new ArrayList(); + final AlarmHandler mHandler = new AlarmHandler(); + ClockReceiver mClockReceiver; + private UninstallReceiver mUninstallReceiver; + final ResultReceiver mResultReceiver = new ResultReceiver(); + PendingIntent mTimeTickSender; + PendingIntent mDateChangeSender; + + class WakeupEvent { + public long when; + public int uid; + public String action; + + public WakeupEvent(long theTime, int theUid, String theAction) { + when = theTime; + uid = theUid; + action = theAction; + } + } + + final LinkedList mRecentWakeups = new LinkedList(); + final long RECENT_WAKEUP_PERIOD = 1000L * 60 * 60 * 24; // one day + + static final class Batch { + long start; // These endpoints are always in ELAPSED + long end; + boolean standalone; // certain "batches" don't participate in coalescing + + final ArrayList alarms = new ArrayList(); + + Batch() { + start = 0; + end = Long.MAX_VALUE; + } + + Batch(Alarm seed) { + start = seed.whenElapsed; + end = seed.maxWhen; + alarms.add(seed); + } + + int size() { + return alarms.size(); + } + + Alarm get(int index) { + return alarms.get(index); + } + + boolean canHold(long whenElapsed, long maxWhen) { + return (end >= whenElapsed) && (start <= maxWhen); + } + + boolean add(Alarm alarm) { + boolean newStart = false; + // narrows the batch if necessary; presumes that canHold(alarm) is true + int index = Collections.binarySearch(alarms, alarm, sIncreasingTimeOrder); + if (index < 0) { + index = 0 - index - 1; + } + alarms.add(index, alarm); + if (DEBUG_BATCH) { + Slog.v(TAG, "Adding " + alarm + " to " + this); + } + if (alarm.whenElapsed > start) { + start = alarm.whenElapsed; + newStart = true; + } + if (alarm.maxWhen < end) { + end = alarm.maxWhen; + } + + if (DEBUG_BATCH) { + Slog.v(TAG, " => now " + this); + } + return newStart; + } + + boolean remove(final PendingIntent operation) { + boolean didRemove = false; + long newStart = 0; // recalculate endpoints as we go + long newEnd = Long.MAX_VALUE; + for (int i = 0; i < alarms.size(); ) { + Alarm alarm = alarms.get(i); + if (alarm.operation.equals(operation)) { + alarms.remove(i); + didRemove = true; + } else { + if (alarm.whenElapsed > newStart) { + newStart = alarm.whenElapsed; + } + if (alarm.maxWhen < newEnd) { + newEnd = alarm.maxWhen; + } + i++; + } + } + if (didRemove) { + // commit the new batch bounds + start = newStart; + end = newEnd; + } + return didRemove; + } + + boolean remove(final String packageName) { + boolean didRemove = false; + long newStart = 0; // recalculate endpoints as we go + long newEnd = Long.MAX_VALUE; + for (int i = 0; i < alarms.size(); ) { + Alarm alarm = alarms.get(i); + if (alarm.operation.getTargetPackage().equals(packageName)) { + alarms.remove(i); + didRemove = true; + } else { + if (alarm.whenElapsed > newStart) { + newStart = alarm.whenElapsed; + } + if (alarm.maxWhen < newEnd) { + newEnd = alarm.maxWhen; + } + i++; + } + } + if (didRemove) { + // commit the new batch bounds + start = newStart; + end = newEnd; + } + return didRemove; + } + + boolean remove(final int userHandle) { + boolean didRemove = false; + long newStart = 0; // recalculate endpoints as we go + long newEnd = Long.MAX_VALUE; + for (int i = 0; i < alarms.size(); ) { + Alarm alarm = alarms.get(i); + if (UserHandle.getUserId(alarm.operation.getCreatorUid()) == userHandle) { + alarms.remove(i); + didRemove = true; + } else { + if (alarm.whenElapsed > newStart) { + newStart = alarm.whenElapsed; + } + if (alarm.maxWhen < newEnd) { + newEnd = alarm.maxWhen; + } + i++; + } + } + if (didRemove) { + // commit the new batch bounds + start = newStart; + end = newEnd; + } + return didRemove; + } + + boolean hasPackage(final String packageName) { + final int N = alarms.size(); + for (int i = 0; i < N; i++) { + Alarm a = alarms.get(i); + if (a.operation.getTargetPackage().equals(packageName)) { + return true; + } + } + return false; + } + + boolean hasWakeups() { + final int N = alarms.size(); + for (int i = 0; i < N; i++) { + Alarm a = alarms.get(i); + // non-wakeup alarms are types 1 and 3, i.e. have the low bit set + if ((a.type & TYPE_NONWAKEUP_MASK) == 0) { + return true; + } + } + return false; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(40); + b.append("Batch{"); b.append(Integer.toHexString(this.hashCode())); + b.append(" num="); b.append(size()); + b.append(" start="); b.append(start); + b.append(" end="); b.append(end); + if (standalone) { + b.append(" STANDALONE"); + } + b.append('}'); + return b.toString(); + } + } + + static class BatchTimeOrder implements Comparator { + public int compare(Batch b1, Batch b2) { + long when1 = b1.start; + long when2 = b2.start; + if (when1 - when2 > 0) { + return 1; + } + if (when1 - when2 < 0) { + return -1; + } + return 0; + } + } + + // minimum recurrence period or alarm futurity for us to be able to fuzz it + static final long MIN_FUZZABLE_INTERVAL = 10000; + static final BatchTimeOrder sBatchOrder = new BatchTimeOrder(); + final ArrayList mAlarmBatches = new ArrayList(); + + static long convertToElapsed(long when, int type) { + final boolean isRtc = (type == RTC || type == RTC_WAKEUP); + if (isRtc) { + when -= System.currentTimeMillis() - SystemClock.elapsedRealtime(); + } + return when; + } + + // Apply a heuristic to { recurrence interval, futurity of the trigger time } to + // calculate the end of our nominal delivery window for the alarm. + static long maxTriggerTime(long now, long triggerAtTime, long interval) { + // Current heuristic: batchable window is 75% of either the recurrence interval + // [for a periodic alarm] or of the time from now to the desired delivery time, + // with a minimum delay/interval of 10 seconds, under which we will simply not + // defer the alarm. + long futurity = (interval == 0) + ? (triggerAtTime - now) + : interval; + if (futurity < MIN_FUZZABLE_INTERVAL) { + futurity = 0; + } + return triggerAtTime + (long)(.75 * futurity); + } + + // returns true if the batch was added at the head + static boolean addBatchLocked(ArrayList list, Batch newBatch) { + int index = Collections.binarySearch(list, newBatch, sBatchOrder); + if (index < 0) { + index = 0 - index - 1; + } + list.add(index, newBatch); + return (index == 0); + } + + // Return the index of the matching batch, or -1 if none found. + int attemptCoalesceLocked(long whenElapsed, long maxWhen) { + final int N = mAlarmBatches.size(); + for (int i = 0; i < N; i++) { + Batch b = mAlarmBatches.get(i); + if (!b.standalone && b.canHold(whenElapsed, maxWhen)) { + return i; + } + } + return -1; + } + + // The RTC clock has moved arbitrarily, so we need to recalculate all the batching + void rebatchAllAlarms() { + synchronized (mLock) { + rebatchAllAlarmsLocked(true); + } + } + + void rebatchAllAlarmsLocked(boolean doValidate) { + ArrayList oldSet = (ArrayList) mAlarmBatches.clone(); + mAlarmBatches.clear(); + final long nowElapsed = SystemClock.elapsedRealtime(); + final int oldBatches = oldSet.size(); + for (int batchNum = 0; batchNum < oldBatches; batchNum++) { + Batch batch = oldSet.get(batchNum); + final int N = batch.size(); + for (int i = 0; i < N; i++) { + Alarm a = batch.get(i); + long whenElapsed = convertToElapsed(a.when, a.type); + final long maxElapsed; + if (a.whenElapsed == a.maxWhen) { + // Exact + maxElapsed = whenElapsed; + } else { + // Not exact. Preserve any explicit window, otherwise recalculate + // the window based on the alarm's new futurity. Note that this + // reflects a policy of preferring timely to deferred delivery. + maxElapsed = (a.windowLength > 0) + ? (whenElapsed + a.windowLength) + : maxTriggerTime(nowElapsed, whenElapsed, a.repeatInterval); + } + setImplLocked(a.type, a.when, whenElapsed, a.windowLength, maxElapsed, + a.repeatInterval, a.operation, batch.standalone, doValidate, a.workSource); + } + } + } + + static final class InFlight extends Intent { + final PendingIntent mPendingIntent; + final WorkSource mWorkSource; + final Pair mTarget; + final BroadcastStats mBroadcastStats; + final FilterStats mFilterStats; + + InFlight(AlarmManagerService service, PendingIntent pendingIntent, WorkSource workSource) { + mPendingIntent = pendingIntent; + mWorkSource = workSource; + Intent intent = pendingIntent.getIntent(); + mTarget = intent != null + ? new Pair(intent.getAction(), intent.getComponent()) + : null; + mBroadcastStats = service.getStatsLocked(pendingIntent); + FilterStats fs = mBroadcastStats.filterStats.get(mTarget); + if (fs == null) { + fs = new FilterStats(mBroadcastStats, mTarget); + mBroadcastStats.filterStats.put(mTarget, fs); + } + mFilterStats = fs; + } + } + + static final class FilterStats { + final BroadcastStats mBroadcastStats; + final Pair mTarget; + + long aggregateTime; + int count; + int numWakeup; + long startTime; + int nesting; + + FilterStats(BroadcastStats broadcastStats, Pair target) { + mBroadcastStats = broadcastStats; + mTarget = target; + } + } + + static final class BroadcastStats { + final String mPackageName; + + long aggregateTime; + int count; + int numWakeup; + long startTime; + int nesting; + final HashMap, FilterStats> filterStats + = new HashMap, FilterStats>(); + + BroadcastStats(String packageName) { + mPackageName = packageName; + } + } + + final HashMap mBroadcastStats + = new HashMap(); + + @Override + public void onStart() { + mNativeData = init(); + mNextWakeup = mNextNonWakeup = 0; + + // We have to set current TimeZone info to kernel + // because kernel doesn't keep this after reboot + setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY)); + + PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mTimeTickSender = PendingIntent.getBroadcastAsUser(getContext(), 0, + new Intent(Intent.ACTION_TIME_TICK).addFlags( + Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND), 0, + UserHandle.ALL); + Intent intent = new Intent(Intent.ACTION_DATE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + mDateChangeSender = PendingIntent.getBroadcastAsUser(getContext(), 0, intent, + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT, UserHandle.ALL); + + // now that we have initied the driver schedule the alarm + mClockReceiver = new ClockReceiver(); + mClockReceiver.scheduleTimeTickEvent(); + mClockReceiver.scheduleDateChangedEvent(); + mUninstallReceiver = new UninstallReceiver(); + + if (mNativeData != 0) { + AlarmThread waitThread = new AlarmThread(); + waitThread.start(); + } else { + Slog.w(TAG, "Failed to open alarm driver. Falling back to a handler."); + } + + publishBinderService(Context.ALARM_SERVICE, mService); + } + + @Override + protected void finalize() throws Throwable { + try { + close(mNativeData); + } finally { + super.finalize(); + } + } + + void setTimeZoneImpl(String tz) { + if (TextUtils.isEmpty(tz)) { + return; + } + + TimeZone zone = TimeZone.getTimeZone(tz); + // Prevent reentrant calls from stepping on each other when writing + // the time zone property + boolean timeZoneWasChanged = false; + synchronized (this) { + String current = SystemProperties.get(TIMEZONE_PROPERTY); + if (current == null || !current.equals(zone.getID())) { + if (localLOGV) { + Slog.v(TAG, "timezone changed: " + current + ", new=" + zone.getID()); + } + timeZoneWasChanged = true; + SystemProperties.set(TIMEZONE_PROPERTY, zone.getID()); + } + + // Update the kernel timezone information + // Kernel tracks time offsets as 'minutes west of GMT' + int gmtOffset = zone.getOffset(System.currentTimeMillis()); + setKernelTimezone(mNativeData, -(gmtOffset / 60000)); + } + + TimeZone.setDefault(null); + + if (timeZoneWasChanged) { + Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time-zone", zone.getID()); + getContext().sendBroadcastAsUser(intent, UserHandle.ALL); + } + } + + void removeImpl(PendingIntent operation) { + if (operation == null) { + return; + } + synchronized (mLock) { + removeLocked(operation); + } + } + + void setImpl(int type, long triggerAtTime, long windowLength, long interval, + PendingIntent operation, boolean isStandalone, WorkSource workSource) { + if (operation == null) { + Slog.w(TAG, "set/setRepeating ignored because there is no intent"); + return; + } + + // Sanity check the window length. This will catch people mistakenly + // trying to pass an end-of-window timestamp rather than a duration. + if (windowLength > AlarmManager.INTERVAL_HALF_DAY) { + Slog.w(TAG, "Window length " + windowLength + + "ms suspiciously long; limiting to 1 hour"); + windowLength = AlarmManager.INTERVAL_HOUR; + } + + if (type < RTC_WAKEUP || type > ELAPSED_REALTIME) { + throw new IllegalArgumentException("Invalid alarm type " + type); + } + + if (triggerAtTime < 0) { + final long who = Binder.getCallingUid(); + final long what = Binder.getCallingPid(); + Slog.w(TAG, "Invalid alarm trigger time! " + triggerAtTime + " from uid=" + who + + " pid=" + what); + triggerAtTime = 0; + } + + final long nowElapsed = SystemClock.elapsedRealtime(); + final long triggerElapsed = convertToElapsed(triggerAtTime, type); + final long maxElapsed; + if (windowLength == AlarmManager.WINDOW_EXACT) { + maxElapsed = triggerElapsed; + } else if (windowLength < 0) { + maxElapsed = maxTriggerTime(nowElapsed, triggerElapsed, interval); + } else { + maxElapsed = triggerElapsed + windowLength; + } + + synchronized (mLock) { + if (DEBUG_BATCH) { + Slog.v(TAG, "set(" + operation + ") : type=" + type + + " triggerAtTime=" + triggerAtTime + " win=" + windowLength + + " tElapsed=" + triggerElapsed + " maxElapsed=" + maxElapsed + + " interval=" + interval + " standalone=" + isStandalone); + } + setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed, + interval, operation, isStandalone, true, workSource); + } + } + + private void setImplLocked(int type, long when, long whenElapsed, long windowLength, + long maxWhen, long interval, PendingIntent operation, boolean isStandalone, + boolean doValidate, WorkSource workSource) { + Alarm a = new Alarm(type, when, whenElapsed, windowLength, maxWhen, interval, + operation, workSource); + removeLocked(operation); + + int whichBatch = (isStandalone) ? -1 : attemptCoalesceLocked(whenElapsed, maxWhen); + if (whichBatch < 0) { + Batch batch = new Batch(a); + batch.standalone = isStandalone; + addBatchLocked(mAlarmBatches, batch); + } else { + Batch batch = mAlarmBatches.get(whichBatch); + if (batch.add(a)) { + // The start time of this batch advanced, so batch ordering may + // have just been broken. Move it to where it now belongs. + mAlarmBatches.remove(whichBatch); + addBatchLocked(mAlarmBatches, batch); + } + } + + if (DEBUG_VALIDATE) { + if (doValidate && !validateConsistencyLocked()) { + Slog.v(TAG, "Tipping-point operation: type=" + type + " when=" + when + + " when(hex)=" + Long.toHexString(when) + + " whenElapsed=" + whenElapsed + " maxWhen=" + maxWhen + + " interval=" + interval + " op=" + operation + + " standalone=" + isStandalone); + rebatchAllAlarmsLocked(false); + } + } + + rescheduleKernelAlarmsLocked(); + } + + private final IBinder mService = new IAlarmManager.Stub() { + @Override + public void set(int type, long triggerAtTime, long windowLength, long interval, + PendingIntent operation, WorkSource workSource) { + if (workSource != null) { + getContext().enforceCallingPermission( + android.Manifest.permission.UPDATE_DEVICE_STATS, + "AlarmManager.set"); + } + + setImpl(type, triggerAtTime, windowLength, interval, operation, + false, workSource); + } + + @Override + public void setTime(long millis) { + getContext().enforceCallingOrSelfPermission( + "android.permission.SET_TIME", + "setTime"); + + SystemClock.setCurrentTimeMillis(millis); + } + + @Override + public void setTimeZone(String tz) { + getContext().enforceCallingOrSelfPermission( + "android.permission.SET_TIME_ZONE", + "setTimeZone"); + + final long oldId = Binder.clearCallingIdentity(); + try { + setTimeZoneImpl(tz); + } finally { + Binder.restoreCallingIdentity(oldId); + } + } + + @Override + public void remove(PendingIntent operation) { + removeImpl(operation); + + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump AlarmManager from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + dumpImpl(pw); + } + }; + + void dumpImpl(PrintWriter pw) { + synchronized (mLock) { + pw.println("Current Alarm Manager state:"); + final long nowRTC = System.currentTimeMillis(); + final long nowELAPSED = SystemClock.elapsedRealtime(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + pw.print("nowRTC="); pw.print(nowRTC); + pw.print("="); pw.print(sdf.format(new Date(nowRTC))); + pw.print(" nowELAPSED="); pw.println(nowELAPSED); + + long nextWakeupRTC = mNextWakeup + (nowRTC - nowELAPSED); + long nextNonWakeupRTC = mNextNonWakeup + (nowRTC - nowELAPSED); + pw.print("Next alarm: "); pw.print(mNextNonWakeup); + pw.print(" = "); pw.println(sdf.format(new Date(nextNonWakeupRTC))); + pw.print("Next wakeup: "); pw.print(mNextWakeup); + pw.print(" = "); pw.println(sdf.format(new Date(nextWakeupRTC))); + + if (mAlarmBatches.size() > 0) { + pw.println(); + pw.print("Pending alarm batches: "); + pw.println(mAlarmBatches.size()); + for (Batch b : mAlarmBatches) { + pw.print(b); pw.println(':'); + dumpAlarmList(pw, b.alarms, " ", nowELAPSED, nowRTC); + } + } + + pw.println(); + pw.print(" Broadcast ref count: "); pw.println(mBroadcastRefCount); + pw.println(); + + if (mLog.dump(pw, " Recent problems", " ")) { + pw.println(); + } + + final FilterStats[] topFilters = new FilterStats[10]; + final Comparator comparator = new Comparator() { + @Override + public int compare(FilterStats lhs, FilterStats rhs) { + if (lhs.aggregateTime < rhs.aggregateTime) { + return 1; + } else if (lhs.aggregateTime > rhs.aggregateTime) { + return -1; + } + return 0; + } + }; + int len = 0; + for (Map.Entry be : mBroadcastStats.entrySet()) { + BroadcastStats bs = be.getValue(); + for (Map.Entry, FilterStats> fe + : bs.filterStats.entrySet()) { + FilterStats fs = fe.getValue(); + int pos = len > 0 + ? Arrays.binarySearch(topFilters, 0, len, fs, comparator) : 0; + if (pos < 0) { + pos = -pos - 1; + } + if (pos < topFilters.length) { + int copylen = topFilters.length - pos - 1; + if (copylen > 0) { + System.arraycopy(topFilters, pos, topFilters, pos+1, copylen); + } + topFilters[pos] = fs; + if (len < topFilters.length) { + len++; + } + } + } + } + if (len > 0) { + pw.println(" Top Alarms:"); + for (int i=0; i 0) pw.print("*ACTIVE* "); + TimeUtils.formatDuration(fs.aggregateTime, pw); + pw.print(" running, "); pw.print(fs.numWakeup); + pw.print(" wakeups, "); pw.print(fs.count); + pw.print(" alarms: "); pw.print(fs.mBroadcastStats.mPackageName); + pw.println(); + pw.print(" "); + if (fs.mTarget.first != null) { + pw.print(" act="); pw.print(fs.mTarget.first); + } + if (fs.mTarget.second != null) { + pw.print(" cmp="); pw.print(fs.mTarget.second.toShortString()); + } + pw.println(); + } + } + + pw.println(" "); + pw.println(" Alarm Stats:"); + final ArrayList tmpFilters = new ArrayList(); + for (Map.Entry be : mBroadcastStats.entrySet()) { + BroadcastStats bs = be.getValue(); + pw.print(" "); + if (bs.nesting > 0) pw.print("*ACTIVE* "); + pw.print(be.getKey()); + pw.print(" "); TimeUtils.formatDuration(bs.aggregateTime, pw); + pw.print(" running, "); pw.print(bs.numWakeup); + pw.println(" wakeups:"); + tmpFilters.clear(); + for (Map.Entry, FilterStats> fe + : bs.filterStats.entrySet()) { + tmpFilters.add(fe.getValue()); + } + Collections.sort(tmpFilters, comparator); + for (int i=0; i 0) pw.print("*ACTIVE* "); + TimeUtils.formatDuration(fs.aggregateTime, pw); + pw.print(" "); pw.print(fs.numWakeup); + pw.print(" wakes " ); pw.print(fs.count); + pw.print(" alarms:"); + if (fs.mTarget.first != null) { + pw.print(" act="); pw.print(fs.mTarget.first); + } + if (fs.mTarget.second != null) { + pw.print(" cmp="); pw.print(fs.mTarget.second.toShortString()); + } + pw.println(); + } + } + + if (WAKEUP_STATS) { + pw.println(); + pw.println(" Recent Wakeup History:"); + long last = -1; + for (WakeupEvent event : mRecentWakeups) { + pw.print(" "); pw.print(sdf.format(new Date(event.when))); + pw.print('|'); + if (last < 0) { + pw.print('0'); + } else { + pw.print(event.when - last); + } + last = event.when; + pw.print('|'); pw.print(event.uid); + pw.print('|'); pw.print(event.action); + pw.println(); + } + pw.println(); + } + } + } + + private void logBatchesLocked() { + ByteArrayOutputStream bs = new ByteArrayOutputStream(2048); + PrintWriter pw = new PrintWriter(bs); + final long nowRTC = System.currentTimeMillis(); + final long nowELAPSED = SystemClock.elapsedRealtime(); + final int NZ = mAlarmBatches.size(); + for (int iz = 0; iz < NZ; iz++) { + Batch bz = mAlarmBatches.get(iz); + pw.append("Batch "); pw.print(iz); pw.append(": "); pw.println(bz); + dumpAlarmList(pw, bz.alarms, " ", nowELAPSED, nowRTC); + pw.flush(); + Slog.v(TAG, bs.toString()); + bs.reset(); + } + } + + private boolean validateConsistencyLocked() { + if (DEBUG_VALIDATE) { + long lastTime = Long.MIN_VALUE; + final int N = mAlarmBatches.size(); + for (int i = 0; i < N; i++) { + Batch b = mAlarmBatches.get(i); + if (b.start >= lastTime) { + // duplicate start times are okay because of standalone batches + lastTime = b.start; + } else { + Slog.e(TAG, "CONSISTENCY FAILURE: Batch " + i + " is out of order"); + logBatchesLocked(); + return false; + } + } + } + return true; + } + + private Batch findFirstWakeupBatchLocked() { + final int N = mAlarmBatches.size(); + for (int i = 0; i < N; i++) { + Batch b = mAlarmBatches.get(i); + if (b.hasWakeups()) { + return b; + } + } + return null; + } + + void rescheduleKernelAlarmsLocked() { + // Schedule the next upcoming wakeup alarm. If there is a deliverable batch + // prior to that which contains no wakeups, we schedule that as well. + if (mAlarmBatches.size() > 0) { + final Batch firstWakeup = findFirstWakeupBatchLocked(); + final Batch firstBatch = mAlarmBatches.get(0); + if (firstWakeup != null && mNextWakeup != firstWakeup.start) { + mNextWakeup = firstWakeup.start; + setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start); + } + if (firstBatch != firstWakeup && mNextNonWakeup != firstBatch.start) { + mNextNonWakeup = firstBatch.start; + setLocked(ELAPSED_REALTIME, firstBatch.start); + } + } + } + + private void removeLocked(PendingIntent operation) { + boolean didRemove = false; + for (int i = mAlarmBatches.size() - 1; i >= 0; i--) { + Batch b = mAlarmBatches.get(i); + didRemove |= b.remove(operation); + if (b.size() == 0) { + mAlarmBatches.remove(i); + } + } + + if (didRemove) { + if (DEBUG_BATCH) { + Slog.v(TAG, "remove(operation) changed bounds; rebatching"); + } + rebatchAllAlarmsLocked(true); + rescheduleKernelAlarmsLocked(); + } + } + + void removeLocked(String packageName) { + boolean didRemove = false; + for (int i = mAlarmBatches.size() - 1; i >= 0; i--) { + Batch b = mAlarmBatches.get(i); + didRemove |= b.remove(packageName); + if (b.size() == 0) { + mAlarmBatches.remove(i); + } + } + + if (didRemove) { + if (DEBUG_BATCH) { + Slog.v(TAG, "remove(package) changed bounds; rebatching"); + } + rebatchAllAlarmsLocked(true); + rescheduleKernelAlarmsLocked(); + } + } + + void removeUserLocked(int userHandle) { + boolean didRemove = false; + for (int i = mAlarmBatches.size() - 1; i >= 0; i--) { + Batch b = mAlarmBatches.get(i); + didRemove |= b.remove(userHandle); + if (b.size() == 0) { + mAlarmBatches.remove(i); + } + } + + if (didRemove) { + if (DEBUG_BATCH) { + Slog.v(TAG, "remove(user) changed bounds; rebatching"); + } + rebatchAllAlarmsLocked(true); + rescheduleKernelAlarmsLocked(); + } + } + + boolean lookForPackageLocked(String packageName) { + for (int i = 0; i < mAlarmBatches.size(); i++) { + Batch b = mAlarmBatches.get(i); + if (b.hasPackage(packageName)) { + return true; + } + } + return false; + } + + private void setLocked(int type, long when) { + if (mNativeData != 0) { + // The kernel never triggers alarms with negative wakeup times + // so we ensure they are positive. + long alarmSeconds, alarmNanoseconds; + if (when < 0) { + alarmSeconds = 0; + alarmNanoseconds = 0; + } else { + alarmSeconds = when / 1000; + alarmNanoseconds = (when % 1000) * 1000 * 1000; + } + + set(mNativeData, type, alarmSeconds, alarmNanoseconds); + } else { + Message msg = Message.obtain(); + msg.what = ALARM_EVENT; + + mHandler.removeMessages(ALARM_EVENT); + mHandler.sendMessageAtTime(msg, when); + } + } + + private static final void dumpAlarmList(PrintWriter pw, ArrayList list, + String prefix, String label, long now) { + for (int i=list.size()-1; i>=0; i--) { + Alarm a = list.get(i); + pw.print(prefix); pw.print(label); pw.print(" #"); pw.print(i); + pw.print(": "); pw.println(a); + a.dump(pw, prefix + " ", now); + } + } + + private static final String labelForType(int type) { + switch (type) { + case RTC: return "RTC"; + case RTC_WAKEUP : return "RTC_WAKEUP"; + case ELAPSED_REALTIME : return "ELAPSED"; + case ELAPSED_REALTIME_WAKEUP: return "ELAPSED_WAKEUP"; + default: + break; + } + return "--unknown--"; + } + + private static final void dumpAlarmList(PrintWriter pw, ArrayList list, + String prefix, long nowELAPSED, long nowRTC) { + for (int i=list.size()-1; i>=0; i--) { + Alarm a = list.get(i); + final String label = labelForType(a.type); + long now = (a.type <= RTC) ? nowRTC : nowELAPSED; + pw.print(prefix); pw.print(label); pw.print(" #"); pw.print(i); + pw.print(": "); pw.println(a); + a.dump(pw, prefix + " ", now); + } + } + + private native long init(); + private native void close(long nativeData); + private native void set(long nativeData, int type, long seconds, long nanoseconds); + private native int waitForAlarm(long nativeData); + private native int setKernelTimezone(long nativeData, int minuteswest); + + void triggerAlarmsLocked(ArrayList triggerList, long nowELAPSED, long nowRTC) { + // batches are temporally sorted, so we need only pull from the + // start of the list until we either empty it or hit a batch + // that is not yet deliverable + while (mAlarmBatches.size() > 0) { + Batch batch = mAlarmBatches.get(0); + if (batch.start > nowELAPSED) { + // Everything else is scheduled for the future + break; + } + + // We will (re)schedule some alarms now; don't let that interfere + // with delivery of this current batch + mAlarmBatches.remove(0); + + final int N = batch.size(); + for (int i = 0; i < N; i++) { + Alarm alarm = batch.get(i); + alarm.count = 1; + triggerList.add(alarm); + + // Recurring alarms may have passed several alarm intervals while the + // phone was asleep or off, so pass a trigger count when sending them. + if (alarm.repeatInterval > 0) { + // this adjustment will be zero if we're late by + // less than one full repeat interval + alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval; + + // Also schedule its next recurrence + final long delta = alarm.count * alarm.repeatInterval; + final long nextElapsed = alarm.whenElapsed + delta; + setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength, + maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval), + alarm.repeatInterval, alarm.operation, batch.standalone, true, + alarm.workSource); + } + + } + } + } + + /** + * This Comparator sorts Alarms into increasing time order. + */ + public static class IncreasingTimeOrder implements Comparator { + public int compare(Alarm a1, Alarm a2) { + long when1 = a1.when; + long when2 = a2.when; + if (when1 - when2 > 0) { + return 1; + } + if (when1 - when2 < 0) { + return -1; + } + return 0; + } + } + + private static class Alarm { + public int type; + public int count; + public long when; + public long windowLength; + public long whenElapsed; // 'when' in the elapsed time base + public long maxWhen; // also in the elapsed time base + public long repeatInterval; + public PendingIntent operation; + public WorkSource workSource; + + public Alarm(int _type, long _when, long _whenElapsed, long _windowLength, long _maxWhen, + long _interval, PendingIntent _op, WorkSource _ws) { + type = _type; + when = _when; + whenElapsed = _whenElapsed; + windowLength = _windowLength; + maxWhen = _maxWhen; + repeatInterval = _interval; + operation = _op; + workSource = _ws; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(128); + sb.append("Alarm{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" type "); + sb.append(type); + sb.append(" "); + sb.append(operation.getTargetPackage()); + sb.append('}'); + return sb.toString(); + } + + public void dump(PrintWriter pw, String prefix, long now) { + pw.print(prefix); pw.print("type="); pw.print(type); + pw.print(" whenElapsed="); pw.print(whenElapsed); + pw.print(" when="); TimeUtils.formatDuration(when, now, pw); + pw.print(" window="); pw.print(windowLength); + pw.print(" repeatInterval="); pw.print(repeatInterval); + pw.print(" count="); pw.println(count); + pw.print(prefix); pw.print("operation="); pw.println(operation); + } + } + + void recordWakeupAlarms(ArrayList batches, long nowELAPSED, long nowRTC) { + final int numBatches = batches.size(); + for (int nextBatch = 0; nextBatch < numBatches; nextBatch++) { + Batch b = batches.get(nextBatch); + if (b.start > nowELAPSED) { + break; + } + + final int numAlarms = b.alarms.size(); + for (int nextAlarm = 0; nextAlarm < numAlarms; nextAlarm++) { + Alarm a = b.alarms.get(nextAlarm); + WakeupEvent e = new WakeupEvent(nowRTC, + a.operation.getCreatorUid(), + a.operation.getIntent().getAction()); + mRecentWakeups.add(e); + } + } + } + + private class AlarmThread extends Thread + { + public AlarmThread() + { + super("AlarmManager"); + } + + public void run() + { + ArrayList triggerList = new ArrayList(); + + while (true) + { + int result = waitForAlarm(mNativeData); + + triggerList.clear(); + + if ((result & TIME_CHANGED_MASK) != 0) { + if (DEBUG_BATCH) { + Slog.v(TAG, "Time changed notification from kernel; rebatching"); + } + removeImpl(mTimeTickSender); + rebatchAllAlarms(); + mClockReceiver.scheduleTimeTickEvent(); + Intent intent = new Intent(Intent.ACTION_TIME_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING + | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + getContext().sendBroadcastAsUser(intent, UserHandle.ALL); + } + + synchronized (mLock) { + final long nowRTC = System.currentTimeMillis(); + final long nowELAPSED = SystemClock.elapsedRealtime(); + if (localLOGV) Slog.v( + TAG, "Checking for alarms... rtc=" + nowRTC + + ", elapsed=" + nowELAPSED); + + if (WAKEUP_STATS) { + if ((result & IS_WAKEUP_MASK) != 0) { + long newEarliest = nowRTC - RECENT_WAKEUP_PERIOD; + int n = 0; + for (WakeupEvent event : mRecentWakeups) { + if (event.when > newEarliest) break; + n++; // number of now-stale entries at the list head + } + for (int i = 0; i < n; i++) { + mRecentWakeups.remove(); + } + + recordWakeupAlarms(mAlarmBatches, nowELAPSED, nowRTC); + } + } + + triggerAlarmsLocked(triggerList, nowELAPSED, nowRTC); + rescheduleKernelAlarmsLocked(); + + // now deliver the alarm intents + for (int i=0; i 0) { + // This IntentSender is no longer valid, but this + // is a repeating alarm, so toss the hoser. + removeImpl(alarm.operation); + } + } catch (RuntimeException e) { + Slog.w(TAG, "Failure sending alarm.", e); + } + } + } + } + } + } + + /** + * Attribute blame for a WakeLock. + * @param pi PendingIntent to attribute blame to if ws is null. + * @param ws WorkSource to attribute blame. + */ + void setWakelockWorkSource(PendingIntent pi, WorkSource ws) { + try { + if (ws != null) { + mWakeLock.setWorkSource(ws); + return; + } + + final int uid = ActivityManagerNative.getDefault() + .getUidForIntentSender(pi.getTarget()); + if (uid >= 0) { + mWakeLock.setWorkSource(new WorkSource(uid)); + return; + } + } catch (Exception e) { + } + + // Something went wrong; fall back to attributing the lock to the OS + mWakeLock.setWorkSource(null); + } + + private class AlarmHandler extends Handler { + public static final int ALARM_EVENT = 1; + public static final int MINUTE_CHANGE_EVENT = 2; + public static final int DATE_CHANGE_EVENT = 3; + + public AlarmHandler() { + } + + public void handleMessage(Message msg) { + if (msg.what == ALARM_EVENT) { + ArrayList triggerList = new ArrayList(); + synchronized (mLock) { + final long nowRTC = System.currentTimeMillis(); + final long nowELAPSED = SystemClock.elapsedRealtime(); + triggerAlarmsLocked(triggerList, nowELAPSED, nowRTC); + } + + // now trigger the alarms without the lock held + for (int i=0; i 0) { + // This IntentSender is no longer valid, but this + // is a repeating alarm, so toss the hoser. + removeImpl(alarm.operation); + } + } + } + } + } + } + + class ClockReceiver extends BroadcastReceiver { + public ClockReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_DATE_CHANGED); + getContext().registerReceiver(this, filter); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_TIME_TICK)) { + if (DEBUG_BATCH) { + Slog.v(TAG, "Received TIME_TICK alarm; rescheduling"); + } + scheduleTimeTickEvent(); + } else if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED)) { + // Since the kernel does not keep track of DST, we need to + // reset the TZ information at the beginning of each day + // based off of the current Zone gmt offset + userspace tracked + // daylight savings information. + TimeZone zone = TimeZone.getTimeZone(SystemProperties.get(TIMEZONE_PROPERTY)); + int gmtOffset = zone.getOffset(System.currentTimeMillis()); + setKernelTimezone(mNativeData, -(gmtOffset / 60000)); + scheduleDateChangedEvent(); + } + } + + public void scheduleTimeTickEvent() { + final long currentTime = System.currentTimeMillis(); + final long nextTime = 60000 * ((currentTime / 60000) + 1); + + // Schedule this event for the amount of time that it would take to get to + // the top of the next minute. + final long tickEventDelay = nextTime - currentTime; + + final WorkSource workSource = null; // Let system take blame for time tick events. + setImpl(ELAPSED_REALTIME, SystemClock.elapsedRealtime() + tickEventDelay, 0, + 0, mTimeTickSender, true, workSource); + } + + public void scheduleDateChangedEvent() { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + calendar.add(Calendar.DAY_OF_MONTH, 1); + + final WorkSource workSource = null; // Let system take blame for date change events. + setImpl(RTC, calendar.getTimeInMillis(), 0, 0, mDateChangeSender, true, workSource); + } + } + + class UninstallReceiver extends BroadcastReceiver { + public UninstallReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART); + filter.addDataScheme("package"); + getContext().registerReceiver(this, filter); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + sdFilter.addAction(Intent.ACTION_USER_STOPPED); + getContext().registerReceiver(this, sdFilter); + } + + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mLock) { + String action = intent.getAction(); + String pkgList[] = null; + if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); + for (String packageName : pkgList) { + if (lookForPackageLocked(packageName)) { + setResultCode(Activity.RESULT_OK); + return; + } + } + return; + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + } else if (Intent.ACTION_USER_STOPPED.equals(action)) { + int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userHandle >= 0) { + removeUserLocked(userHandle); + } + } else { + if (Intent.ACTION_PACKAGE_REMOVED.equals(action) + && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + // This package is being updated; don't kill its alarms. + return; + } + Uri data = intent.getData(); + if (data != null) { + String pkg = data.getSchemeSpecificPart(); + if (pkg != null) { + pkgList = new String[]{pkg}; + } + } + } + if (pkgList != null && (pkgList.length > 0)) { + for (String pkg : pkgList) { + removeLocked(pkg); + mBroadcastStats.remove(pkg); + } + } + } + } + } + + private final BroadcastStats getStatsLocked(PendingIntent pi) { + String pkg = pi.getTargetPackage(); + BroadcastStats bs = mBroadcastStats.get(pkg); + if (bs == null) { + bs = new BroadcastStats(pkg); + mBroadcastStats.put(pkg, bs); + } + return bs; + } + + class ResultReceiver implements PendingIntent.OnFinished { + public void onSendFinished(PendingIntent pi, Intent intent, int resultCode, + String resultData, Bundle resultExtras) { + synchronized (mLock) { + InFlight inflight = null; + for (int i=0; i 0) { + mLog.w("Finished all broadcasts with " + mInFlight.size() + + " remaining inflights"); + for (int i=0; i 0) { + InFlight inFlight = mInFlight.get(0); + setWakelockWorkSource(inFlight.mPendingIntent, inFlight.mWorkSource); + } else { + // should never happen + mLog.w("Alarm wakelock still held but sent queue empty"); + mWakeLock.setWorkSource(null); + } + } + } + } + } +} diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java new file mode 100644 index 0000000..a1a0d47 --- /dev/null +++ b/services/core/java/com/android/server/AppOpsService.java @@ -0,0 +1,1083 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; +import android.util.Xml; + +import com.android.internal.app.IAppOpsService; +import com.android.internal.app.IAppOpsCallback; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +public class AppOpsService extends IAppOpsService.Stub { + static final String TAG = "AppOps"; + static final boolean DEBUG = false; + + // Write at most every 30 minutes. + static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000; + + Context mContext; + final AtomicFile mFile; + final Handler mHandler; + + boolean mWriteScheduled; + final Runnable mWriteRunner = new Runnable() { + public void run() { + synchronized (AppOpsService.this) { + mWriteScheduled = false; + AsyncTask task = new AsyncTask() { + @Override protected Void doInBackground(Void... params) { + writeState(); + return null; + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null); + } + } + }; + + final SparseArray> mUidOps + = new SparseArray>(); + + public final static class Ops extends SparseArray { + public final String packageName; + public final int uid; + + public Ops(String _packageName, int _uid) { + packageName = _packageName; + uid = _uid; + } + } + + public final static class Op { + public final int uid; + public final String packageName; + public final int op; + public int mode; + public int duration; + public long time; + public long rejectTime; + public int nesting; + + public Op(int _uid, String _packageName, int _op) { + uid = _uid; + packageName = _packageName; + op = _op; + mode = AppOpsManager.opToDefaultMode(op); + } + } + + final SparseArray> mOpModeWatchers + = new SparseArray>(); + final ArrayMap> mPackageModeWatchers + = new ArrayMap>(); + final ArrayMap mModeWatchers + = new ArrayMap(); + + public final class Callback implements DeathRecipient { + final IAppOpsCallback mCallback; + + public Callback(IAppOpsCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + } + } + + public void unlinkToDeath() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + stopWatchingMode(mCallback); + } + } + + final ArrayMap mClients = new ArrayMap(); + + public final class ClientState extends Binder implements DeathRecipient { + final IBinder mAppToken; + final int mPid; + final ArrayList mStartedOps; + + public ClientState(IBinder appToken) { + mAppToken = appToken; + mPid = Binder.getCallingPid(); + if (appToken instanceof Binder) { + // For local clients, there is no reason to track them. + mStartedOps = null; + } else { + mStartedOps = new ArrayList(); + try { + mAppToken.linkToDeath(this, 0); + } catch (RemoteException e) { + } + } + } + + @Override + public String toString() { + return "ClientState{" + + "mAppToken=" + mAppToken + + ", " + (mStartedOps != null ? ("pid=" + mPid) : "local") + + '}'; + } + + @Override + public void binderDied() { + synchronized (AppOpsService.this) { + for (int i=mStartedOps.size()-1; i>=0; i--) { + finishOperationLocked(mStartedOps.get(i)); + } + mClients.remove(mAppToken); + } + } + } + + public AppOpsService(File storagePath) { + mFile = new AtomicFile(storagePath); + mHandler = new Handler(); + readState(); + } + + public void publish(Context context) { + mContext = context; + ServiceManager.addService(Context.APP_OPS_SERVICE, asBinder()); + } + + public void systemReady() { + synchronized (this) { + boolean changed = false; + for (int i=0; i pkgs = mUidOps.valueAt(i); + Iterator it = pkgs.values().iterator(); + while (it.hasNext()) { + Ops ops = it.next(); + int curUid; + try { + curUid = mContext.getPackageManager().getPackageUid(ops.packageName, + UserHandle.getUserId(ops.uid)); + } catch (NameNotFoundException e) { + curUid = -1; + } + if (curUid != ops.uid) { + Slog.i(TAG, "Pruning old package " + ops.packageName + + "/" + ops.uid + ": new uid=" + curUid); + it.remove(); + changed = true; + } + } + if (pkgs.size() <= 0) { + mUidOps.removeAt(i); + } + } + if (changed) { + scheduleWriteLocked(); + } + } + } + + public void packageRemoved(int uid, String packageName) { + synchronized (this) { + HashMap pkgs = mUidOps.get(uid); + if (pkgs != null) { + if (pkgs.remove(packageName) != null) { + if (pkgs.size() <= 0) { + mUidOps.remove(uid); + } + scheduleWriteLocked(); + } + } + } + } + + public void uidRemoved(int uid) { + synchronized (this) { + if (mUidOps.indexOfKey(uid) >= 0) { + mUidOps.remove(uid); + scheduleWriteLocked(); + } + } + } + + public void shutdown() { + Slog.w(TAG, "Writing app ops before shutdown..."); + boolean doWrite = false; + synchronized (this) { + if (mWriteScheduled) { + mWriteScheduled = false; + doWrite = true; + } + } + if (doWrite) { + writeState(); + } + } + + private ArrayList collectOps(Ops pkgOps, int[] ops) { + ArrayList resOps = null; + if (ops == null) { + resOps = new ArrayList(); + for (int j=0; j(); + } + resOps.add(new AppOpsManager.OpEntry(curOp.op, curOp.mode, curOp.time, + curOp.rejectTime, curOp.duration)); + } + } + } + return resOps; + } + + @Override + public List getPackagesForOps(int[] ops) { + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + ArrayList res = null; + synchronized (this) { + for (int i=0; i packages = mUidOps.valueAt(i); + for (Ops pkgOps : packages.values()) { + ArrayList resOps = collectOps(pkgOps, ops); + if (resOps != null) { + if (res == null) { + res = new ArrayList(); + } + AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( + pkgOps.packageName, pkgOps.uid, resOps); + res.add(resPackage); + } + } + } + } + return res; + } + + @Override + public List getOpsForPackage(int uid, String packageName, + int[] ops) { + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + synchronized (this) { + Ops pkgOps = getOpsLocked(uid, packageName, false); + if (pkgOps == null) { + return null; + } + ArrayList resOps = collectOps(pkgOps, ops); + if (resOps == null) { + return null; + } + ArrayList res = new ArrayList(); + AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( + pkgOps.packageName, pkgOps.uid, resOps); + res.add(resPackage); + return res; + } + } + + private void pruneOp(Op op, int uid, String packageName) { + if (op.time == 0 && op.rejectTime == 0) { + Ops ops = getOpsLocked(uid, packageName, false); + if (ops != null) { + ops.remove(op.op); + if (ops.size() <= 0) { + HashMap pkgOps = mUidOps.get(uid); + if (pkgOps != null) { + pkgOps.remove(ops.packageName); + if (pkgOps.size() <= 0) { + mUidOps.remove(uid); + } + } + } + } + } + } + + @Override + public void setMode(int code, int uid, String packageName, int mode) { + verifyIncomingUid(uid); + verifyIncomingOp(code); + ArrayList repCbs = null; + code = AppOpsManager.opToSwitch(code); + synchronized (this) { + Op op = getOpLocked(code, uid, packageName, true); + if (op != null) { + if (op.mode != mode) { + op.mode = mode; + ArrayList cbs = mOpModeWatchers.get(code); + if (cbs != null) { + if (repCbs == null) { + repCbs = new ArrayList(); + } + repCbs.addAll(cbs); + } + cbs = mPackageModeWatchers.get(packageName); + if (cbs != null) { + if (repCbs == null) { + repCbs = new ArrayList(); + } + repCbs.addAll(cbs); + } + if (mode == AppOpsManager.opToDefaultMode(op.op)) { + // If going into the default mode, prune this op + // if there is nothing else interesting in it. + pruneOp(op, uid, packageName); + } + scheduleWriteNowLocked(); + } + } + } + if (repCbs != null) { + for (int i=0; i>> addCallbacks( + HashMap>> callbacks, + String packageName, int op, ArrayList cbs) { + if (cbs == null) { + return callbacks; + } + if (callbacks == null) { + callbacks = new HashMap>>(); + } + for (int i=0; i> reports = callbacks.get(cb); + if (reports == null) { + reports = new ArrayList>(); + callbacks.put(cb, reports); + } + reports.add(new Pair(packageName, op)); + } + return callbacks; + } + + @Override + public void resetAllModes() { + mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + HashMap>> callbacks = null; + synchronized (this) { + boolean changed = false; + for (int i=mUidOps.size()-1; i>=0; i--) { + HashMap packages = mUidOps.valueAt(i); + Iterator> it = packages.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry ent = it.next(); + String packageName = ent.getKey(); + Ops pkgOps = ent.getValue(); + for (int j=pkgOps.size()-1; j>=0; j--) { + Op curOp = pkgOps.valueAt(j); + if (AppOpsManager.opAllowsReset(curOp.op) + && curOp.mode != AppOpsManager.opToDefaultMode(curOp.op)) { + curOp.mode = AppOpsManager.opToDefaultMode(curOp.op); + changed = true; + callbacks = addCallbacks(callbacks, packageName, curOp.op, + mOpModeWatchers.get(curOp.op)); + callbacks = addCallbacks(callbacks, packageName, curOp.op, + mPackageModeWatchers.get(packageName)); + if (curOp.time == 0 && curOp.rejectTime == 0) { + pkgOps.removeAt(j); + } + } + } + if (pkgOps.size() == 0) { + it.remove(); + } + } + if (packages.size() == 0) { + mUidOps.removeAt(i); + } + } + if (changed) { + scheduleWriteNowLocked(); + } + } + if (callbacks != null) { + for (Map.Entry>> ent : callbacks.entrySet()) { + Callback cb = ent.getKey(); + ArrayList> reports = ent.getValue(); + for (int i=0; i rep = reports.get(i); + try { + cb.mCallback.opChanged(rep.second, rep.first); + } catch (RemoteException e) { + } + } + } + } + } + + @Override + public void startWatchingMode(int op, String packageName, IAppOpsCallback callback) { + synchronized (this) { + op = AppOpsManager.opToSwitch(op); + Callback cb = mModeWatchers.get(callback.asBinder()); + if (cb == null) { + cb = new Callback(callback); + mModeWatchers.put(callback.asBinder(), cb); + } + if (op != AppOpsManager.OP_NONE) { + ArrayList cbs = mOpModeWatchers.get(op); + if (cbs == null) { + cbs = new ArrayList(); + mOpModeWatchers.put(op, cbs); + } + cbs.add(cb); + } + if (packageName != null) { + ArrayList cbs = mPackageModeWatchers.get(packageName); + if (cbs == null) { + cbs = new ArrayList(); + mPackageModeWatchers.put(packageName, cbs); + } + cbs.add(cb); + } + } + } + + @Override + public void stopWatchingMode(IAppOpsCallback callback) { + synchronized (this) { + Callback cb = mModeWatchers.remove(callback.asBinder()); + if (cb != null) { + cb.unlinkToDeath(); + for (int i=mOpModeWatchers.size()-1; i>=0; i--) { + ArrayList cbs = mOpModeWatchers.valueAt(i); + cbs.remove(cb); + if (cbs.size() <= 0) { + mOpModeWatchers.removeAt(i); + } + } + for (int i=mPackageModeWatchers.size()-1; i>=0; i--) { + ArrayList cbs = mPackageModeWatchers.valueAt(i); + cbs.remove(cb); + if (cbs.size() <= 0) { + mPackageModeWatchers.removeAt(i); + } + } + } + } + } + + @Override + public IBinder getToken(IBinder clientToken) { + synchronized (this) { + ClientState cs = mClients.get(clientToken); + if (cs == null) { + cs = new ClientState(clientToken); + mClients.put(clientToken, cs); + } + return cs; + } + } + + @Override + public int checkOperation(int code, int uid, String packageName) { + verifyIncomingUid(uid); + verifyIncomingOp(code); + synchronized (this) { + Op op = getOpLocked(AppOpsManager.opToSwitch(code), uid, packageName, false); + if (op == null) { + return AppOpsManager.opToDefaultMode(code); + } + return op.mode; + } + } + + @Override + public int checkPackage(int uid, String packageName) { + synchronized (this) { + if (getOpsLocked(uid, packageName, true) != null) { + return AppOpsManager.MODE_ALLOWED; + } else { + return AppOpsManager.MODE_ERRORED; + } + } + } + + @Override + public int noteOperation(int code, int uid, String packageName) { + verifyIncomingUid(uid); + verifyIncomingOp(code); + synchronized (this) { + Ops ops = getOpsLocked(uid, packageName, true); + if (ops == null) { + if (DEBUG) Log.d(TAG, "noteOperation: no op for code " + code + " uid " + uid + + " package " + packageName); + return AppOpsManager.MODE_ERRORED; + } + Op op = getOpLocked(ops, code, true); + if (op.duration == -1) { + Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + + " code " + code + " time=" + op.time + " duration=" + op.duration); + } + op.duration = 0; + final int switchCode = AppOpsManager.opToSwitch(code); + final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op; + if (switchOp.mode != AppOpsManager.MODE_ALLOWED) { + if (DEBUG) Log.d(TAG, "noteOperation: reject #" + op.mode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + packageName); + op.rejectTime = System.currentTimeMillis(); + return switchOp.mode; + } + if (DEBUG) Log.d(TAG, "noteOperation: allowing code " + code + " uid " + uid + + " package " + packageName); + op.time = System.currentTimeMillis(); + op.rejectTime = 0; + return AppOpsManager.MODE_ALLOWED; + } + } + + @Override + public int startOperation(IBinder token, int code, int uid, String packageName) { + verifyIncomingUid(uid); + verifyIncomingOp(code); + ClientState client = (ClientState)token; + synchronized (this) { + Ops ops = getOpsLocked(uid, packageName, true); + if (ops == null) { + if (DEBUG) Log.d(TAG, "startOperation: no op for code " + code + " uid " + uid + + " package " + packageName); + return AppOpsManager.MODE_ERRORED; + } + Op op = getOpLocked(ops, code, true); + final int switchCode = AppOpsManager.opToSwitch(code); + final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op; + if (switchOp.mode != AppOpsManager.MODE_ALLOWED) { + if (DEBUG) Log.d(TAG, "startOperation: reject #" + op.mode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + packageName); + op.rejectTime = System.currentTimeMillis(); + return switchOp.mode; + } + if (DEBUG) Log.d(TAG, "startOperation: allowing code " + code + " uid " + uid + + " package " + packageName); + if (op.nesting == 0) { + op.time = System.currentTimeMillis(); + op.rejectTime = 0; + op.duration = -1; + } + op.nesting++; + if (client.mStartedOps != null) { + client.mStartedOps.add(op); + } + return AppOpsManager.MODE_ALLOWED; + } + } + + @Override + public void finishOperation(IBinder token, int code, int uid, String packageName) { + verifyIncomingUid(uid); + verifyIncomingOp(code); + ClientState client = (ClientState)token; + synchronized (this) { + Op op = getOpLocked(code, uid, packageName, true); + if (op == null) { + return; + } + if (client.mStartedOps != null) { + if (!client.mStartedOps.remove(op)) { + throw new IllegalStateException("Operation not started: uid" + op.uid + + " pkg=" + op.packageName + " op=" + op.op); + } + } + finishOperationLocked(op); + } + } + + void finishOperationLocked(Op op) { + if (op.nesting <= 1) { + if (op.nesting == 1) { + op.duration = (int)(System.currentTimeMillis() - op.time); + op.time += op.duration; + } else { + Slog.w(TAG, "Finishing op nesting under-run: uid " + op.uid + " pkg " + + op.packageName + " code " + op.op + " time=" + op.time + + " duration=" + op.duration + " nesting=" + op.nesting); + } + op.nesting = 0; + } else { + op.nesting--; + } + } + + private void verifyIncomingUid(int uid) { + if (uid == Binder.getCallingUid()) { + return; + } + if (Binder.getCallingPid() == Process.myPid()) { + return; + } + mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + } + + private void verifyIncomingOp(int op) { + if (op >= 0 && op < AppOpsManager._NUM_OP) { + return; + } + throw new IllegalArgumentException("Bad operation #" + op); + } + + private Ops getOpsLocked(int uid, String packageName, boolean edit) { + HashMap pkgOps = mUidOps.get(uid); + if (pkgOps == null) { + if (!edit) { + return null; + } + pkgOps = new HashMap(); + mUidOps.put(uid, pkgOps); + } + if (uid == 0) { + packageName = "root"; + } else if (uid == Process.SHELL_UID) { + packageName = "com.android.shell"; + } + Ops ops = pkgOps.get(packageName); + if (ops == null) { + if (!edit) { + return null; + } + // This is the first time we have seen this package name under this uid, + // so let's make sure it is valid. + if (uid != 0) { + final long ident = Binder.clearCallingIdentity(); + try { + int pkgUid = -1; + try { + pkgUid = mContext.getPackageManager().getPackageUid(packageName, + UserHandle.getUserId(uid)); + } catch (NameNotFoundException e) { + if ("media".equals(packageName)) { + pkgUid = Process.MEDIA_UID; + } + } + if (pkgUid != uid) { + // Oops! The package name is not valid for the uid they are calling + // under. Abort. + Slog.w(TAG, "Bad call: specified package " + packageName + + " under uid " + uid + " but it is really " + pkgUid); + return null; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + ops = new Ops(packageName, uid); + pkgOps.put(packageName, ops); + } + return ops; + } + + private void scheduleWriteLocked() { + if (!mWriteScheduled) { + mWriteScheduled = true; + mHandler.postDelayed(mWriteRunner, WRITE_DELAY); + } + } + + private void scheduleWriteNowLocked() { + if (!mWriteScheduled) { + mWriteScheduled = true; + } + mHandler.removeCallbacks(mWriteRunner); + mHandler.post(mWriteRunner); + } + + private Op getOpLocked(int code, int uid, String packageName, boolean edit) { + Ops ops = getOpsLocked(uid, packageName, edit); + if (ops == null) { + return null; + } + return getOpLocked(ops, code, edit); + } + + private Op getOpLocked(Ops ops, int code, boolean edit) { + Op op = ops.get(code); + if (op == null) { + if (!edit) { + return null; + } + op = new Op(ops.uid, ops.packageName, code); + ops.put(code, op); + } + if (edit) { + scheduleWriteLocked(); + } + return op; + } + + void readState() { + synchronized (mFile) { + synchronized (this) { + FileInputStream stream; + try { + stream = mFile.openRead(); + } catch (FileNotFoundException e) { + Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty"); + return; + } + boolean success = false; + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, null); + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("pkg")) { + readPackage(parser); + } else { + Slog.w(TAG, "Unknown element under : " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + success = true; + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (NullPointerException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "Failed parsing " + e); + } finally { + if (!success) { + mUidOps.clear(); + } + try { + stream.close(); + } catch (IOException e) { + } + } + } + } + } + + void readPackage(XmlPullParser parser) throws NumberFormatException, + XmlPullParserException, IOException { + String pkgName = parser.getAttributeValue(null, "n"); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("uid")) { + readUid(parser, pkgName); + } else { + Slog.w(TAG, "Unknown element under : " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + } + + void readUid(XmlPullParser parser, String pkgName) throws NumberFormatException, + XmlPullParserException, IOException { + int uid = Integer.parseInt(parser.getAttributeValue(null, "n")); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("op")) { + Op op = new Op(uid, pkgName, Integer.parseInt(parser.getAttributeValue(null, "n"))); + String mode = parser.getAttributeValue(null, "m"); + if (mode != null) { + op.mode = Integer.parseInt(mode); + } + String time = parser.getAttributeValue(null, "t"); + if (time != null) { + op.time = Long.parseLong(time); + } + time = parser.getAttributeValue(null, "r"); + if (time != null) { + op.rejectTime = Long.parseLong(time); + } + String dur = parser.getAttributeValue(null, "d"); + if (dur != null) { + op.duration = Integer.parseInt(dur); + } + HashMap pkgOps = mUidOps.get(uid); + if (pkgOps == null) { + pkgOps = new HashMap(); + mUidOps.put(uid, pkgOps); + } + Ops ops = pkgOps.get(pkgName); + if (ops == null) { + ops = new Ops(pkgName, uid); + pkgOps.put(pkgName, ops); + } + ops.put(op.op, op); + } else { + Slog.w(TAG, "Unknown element under : " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + } + + void writeState() { + synchronized (mFile) { + List allOps = getPackagesForOps(null); + + FileOutputStream stream; + try { + stream = mFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Failed to write state: " + e); + return; + } + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, "utf-8"); + out.startDocument(null, true); + out.startTag(null, "app-ops"); + + if (allOps != null) { + String lastPkg = null; + for (int i=0; i ops = pkg.getOps(); + for (int j=0; j 0) { + needSep = true; + pw.println(" Op mode watchers:"); + for (int i=0; i callbacks = mOpModeWatchers.valueAt(i); + for (int j=0; j 0) { + needSep = true; + pw.println(" Package mode watchers:"); + for (int i=0; i callbacks = mPackageModeWatchers.valueAt(i); + for (int j=0; j 0) { + needSep = true; + pw.println(" All mode watchers:"); + for (int i=0; i "); pw.println(mModeWatchers.valueAt(i)); + } + } + if (mClients.size() > 0) { + needSep = true; + pw.println(" Clients:"); + for (int i=0; i 0) { + pw.println(" Started ops:"); + for (int j=0; j pkgOps = mUidOps.valueAt(i); + for (Ops ops : pkgOps.values()) { + pw.print(" Package "); pw.print(ops.packageName); pw.println(":"); + for (int j=0; jAssetAtlasService. + */ + public static final String ASSET_ATLAS_SERVICE = "assetatlas"; + + private static final String LOG_TAG = "Atlas"; + + // Turns debug logs on/off. Debug logs are kept to a minimum and should + // remain on to diagnose issues + private static final boolean DEBUG_ATLAS = true; + + // When set to true the content of the atlas will be saved to disk + // in /data/system/atlas.png. The shared GraphicBuffer may be empty + private static final boolean DEBUG_ATLAS_TEXTURE = false; + + // Minimum size in pixels to consider for the resulting texture + private static final int MIN_SIZE = 768; + // Maximum size in pixels to consider for the resulting texture + private static final int MAX_SIZE = 2048; + // Increment in number of pixels between size variants when looking + // for the best texture dimensions + private static final int STEP = 64; + + // This percentage of the total number of pixels represents the minimum + // number of pixels we want to be able to pack in the atlas + private static final float PACKING_THRESHOLD = 0.8f; + + // Defines the number of int fields used to represent a single entry + // in the atlas map. This number defines the size of the array returned + // by the getMap(). See the mAtlasMap field for more information + private static final int ATLAS_MAP_ENTRY_FIELD_COUNT = 4; + + // Specifies how our GraphicBuffer will be used. To get proper swizzling + // the buffer will be written to using OpenGL (from JNI) so we can leave + // the software flag set to "never" + private static final int GRAPHIC_BUFFER_USAGE = GraphicBuffer.USAGE_SW_READ_NEVER | + GraphicBuffer.USAGE_SW_WRITE_NEVER | GraphicBuffer.USAGE_HW_TEXTURE; + + // This boolean is set to true if an atlas was successfully + // computed and rendered + private final AtomicBoolean mAtlasReady = new AtomicBoolean(false); + + private final Context mContext; + + // Version name of the current build, used to identify changes to assets list + private final String mVersionName; + + // Holds the atlas' data. This buffer can be mapped to + // OpenGL using an EGLImage + private GraphicBuffer mBuffer; + + // Describes how bitmaps are placed in the atlas. Each bitmap is + // represented by several entries in the array: + // int0: SkBitmap*, the native bitmap object + // int1: x position + // int2: y position + // int3: rotated, 1 if the bitmap must be rotated, 0 otherwise + // NOTE: This will need to be handled differently to support 64 bit pointers + private int[] mAtlasMap; + + /** + * Creates a new service. Upon creating, the service will gather the list of + * assets to consider for packing into the atlas and spawn a new thread to + * start the packing work. + * + * @param context The context giving access to preloaded resources + */ + public AssetAtlasService(Context context) { + mContext = context; + mVersionName = queryVersionName(context); + + ArrayList bitmaps = new ArrayList(300); + int totalPixelCount = 0; + + // We only care about drawables that hold bitmaps + final Resources resources = context.getResources(); + final LongSparseArray drawables = resources.getPreloadedDrawables(); + + final int count = drawables.size(); + for (int i = 0; i < count; i++) { + final Bitmap bitmap = drawables.valueAt(i).getBitmap(); + if (bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888) { + bitmaps.add(bitmap); + totalPixelCount += bitmap.getWidth() * bitmap.getHeight(); + } + } + + // Our algorithms perform better when the bitmaps are first sorted + // The comparator will sort the bitmap by width first, then by height + Collections.sort(bitmaps, new Comparator() { + @Override + public int compare(Bitmap b1, Bitmap b2) { + if (b1.getWidth() == b2.getWidth()) { + return b2.getHeight() - b1.getHeight(); + } + return b2.getWidth() - b1.getWidth(); + } + }); + + // Kick off the packing work on a worker thread + new Thread(new Renderer(bitmaps, totalPixelCount)).start(); + } + + /** + * Queries the version name stored in framework's AndroidManifest. + * The version name can be used to identify possible changes to + * framework resources. + * + * @see #getBuildIdentifier(String) + */ + private static String queryVersionName(Context context) { + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + return info.versionName; + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, "Could not get package info", e); + } + return null; + } + + /** + * Callback invoked by the server thread to indicate we can now run + * 3rd party code. + */ + public void systemRunning() { + } + + /** + * The renderer does all the work: + */ + private class Renderer implements Runnable { + private final ArrayList mBitmaps; + private final int mPixelCount; + + private int mNativeBitmap; + + // Used for debugging only + private Bitmap mAtlasBitmap; + + Renderer(ArrayList bitmaps, int pixelCount) { + mBitmaps = bitmaps; + mPixelCount = pixelCount; + } + + /** + * 1. On first boot or after every update, brute-force through all the + * possible atlas configurations and look for the best one (maximimize + * number of packed assets and minimize texture size) + * a. If a best configuration was computed, write it out to disk for + * future use + * 2. Read best configuration from disk + * 3. Compute the packing using the best configuration + * 4. Allocate a GraphicBuffer + * 5. Render assets in the buffer + */ + @Override + public void run() { + Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName); + if (DEBUG_ATLAS) Log.d(LOG_TAG, "Loaded configuration: " + config); + + if (config != null) { + mBuffer = GraphicBuffer.create(config.width, config.height, + PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE); + + if (mBuffer != null) { + Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags); + if (renderAtlas(mBuffer, atlas, config.count)) { + mAtlasReady.set(true); + } + } + } + } + + /** + * Renders a list of bitmaps into the atlas. The position of each bitmap + * was decided by the packing algorithm and will be honored by this + * method. If need be this method will also rotate bitmaps. + * + * @param buffer The buffer to render the atlas entries into + * @param atlas The atlas to pack the bitmaps into + * @param packCount The number of bitmaps that will be packed in the atlas + * + * @return true if the atlas was rendered, false otherwise + */ + @SuppressWarnings("MismatchedReadAndWriteOfArray") + private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) { + // Use a Source blend mode to improve performance, the target bitmap + // will be zero'd out so there's no need to waste time applying blending + final Paint paint = new Paint(); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + + // We always render the atlas into a bitmap. This bitmap is then + // uploaded into the GraphicBuffer using OpenGL to swizzle the content + final Canvas canvas = acquireCanvas(buffer.getWidth(), buffer.getHeight()); + if (canvas == null) return false; + + final Atlas.Entry entry = new Atlas.Entry(); + + mAtlasMap = new int[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT]; + int[] atlasMap = mAtlasMap; + int mapIndex = 0; + + boolean result = false; + try { + final long startRender = System.nanoTime(); + final int count = mBitmaps.size(); + + for (int i = 0; i < count; i++) { + final Bitmap bitmap = mBitmaps.get(i); + if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { + // We have more bitmaps to pack than the current configuration + // says, we were most likely not able to detect a change in the + // list of preloaded drawables, abort and delete the configuration + if (mapIndex >= mAtlasMap.length) { + deleteDataFile(); + break; + } + + canvas.save(); + canvas.translate(entry.x, entry.y); + if (entry.rotated) { + canvas.translate(bitmap.getHeight(), 0.0f); + canvas.rotate(90.0f); + } + canvas.drawBitmap(bitmap, 0.0f, 0.0f, null); + canvas.restore(); + + atlasMap[mapIndex++] = bitmap.mNativeBitmap; + atlasMap[mapIndex++] = entry.x; + atlasMap[mapIndex++] = entry.y; + atlasMap[mapIndex++] = entry.rotated ? 1 : 0; + } + } + + final long endRender = System.nanoTime(); + if (mNativeBitmap != 0) { + result = nUploadAtlas(buffer, mNativeBitmap); + } + + final long endUpload = System.nanoTime(); + if (DEBUG_ATLAS) { + float renderDuration = (endRender - startRender) / 1000.0f / 1000.0f; + float uploadDuration = (endUpload - endRender) / 1000.0f / 1000.0f; + Log.d(LOG_TAG, String.format("Rendered atlas in %.2fms (%.2f+%.2fms)", + renderDuration + uploadDuration, renderDuration, uploadDuration)); + } + + } finally { + releaseCanvas(canvas); + } + + return result; + } + + /** + * Returns a Canvas for the specified buffer. If {@link #DEBUG_ATLAS_TEXTURE} + * is turned on, the returned Canvas will render into a local bitmap that + * will then be saved out to disk for debugging purposes. + * @param width + * @param height + */ + private Canvas acquireCanvas(int width, int height) { + if (DEBUG_ATLAS_TEXTURE) { + mAtlasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + return new Canvas(mAtlasBitmap); + } else { + Canvas canvas = new Canvas(); + mNativeBitmap = nAcquireAtlasCanvas(canvas, width, height); + return canvas; + } + } + + /** + * Releases the canvas used to render into the buffer. Calling this method + * will release any resource previously acquired. If {@link #DEBUG_ATLAS_TEXTURE} + * is turend on, calling this method will write the content of the atlas + * to disk in /data/system/atlas.png for debugging. + */ + private void releaseCanvas(Canvas canvas) { + if (DEBUG_ATLAS_TEXTURE) { + canvas.setBitmap(null); + + File systemDirectory = new File(Environment.getDataDirectory(), "system"); + File dataFile = new File(systemDirectory, "atlas.png"); + + try { + FileOutputStream out = new FileOutputStream(dataFile); + mAtlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + } catch (FileNotFoundException e) { + // Ignore + } catch (IOException e) { + // Ignore + } + + mAtlasBitmap.recycle(); + mAtlasBitmap = null; + } else { + nReleaseAtlasCanvas(canvas, mNativeBitmap); + } + } + } + + private static native int nAcquireAtlasCanvas(Canvas canvas, int width, int height); + private static native void nReleaseAtlasCanvas(Canvas canvas, int bitmap); + private static native boolean nUploadAtlas(GraphicBuffer buffer, int bitmap); + + @Override + public boolean isCompatible(int ppid) { + return ppid == android.os.Process.myPpid(); + } + + @Override + public GraphicBuffer getBuffer() throws RemoteException { + return mAtlasReady.get() ? mBuffer : null; + } + + @Override + public int[] getMap() throws RemoteException { + return mAtlasReady.get() ? mAtlasMap : null; + } + + /** + * Finds the best atlas configuration to pack the list of supplied bitmaps. + * This method takes advantage of multi-core systems by spawning a number + * of threads equal to the number of available cores. + */ + private static Configuration computeBestConfiguration( + ArrayList bitmaps, int pixelCount) { + if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration..."); + + long begin = System.nanoTime(); + List results = Collections.synchronizedList(new ArrayList()); + + // Don't bother with an extra thread if there's only one processor + int cpuCount = Runtime.getRuntime().availableProcessors(); + if (cpuCount == 1) { + new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run(); + } else { + int start = MIN_SIZE; + int end = MAX_SIZE - (cpuCount - 1) * STEP; + int step = STEP * cpuCount; + + final CountDownLatch signal = new CountDownLatch(cpuCount); + + for (int i = 0; i < cpuCount; i++, start += STEP, end += STEP) { + ComputeWorker worker = new ComputeWorker(start, end, step, + bitmaps, pixelCount, results, signal); + new Thread(worker, "Atlas Worker #" + (i + 1)).start(); + } + + try { + signal.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.w(LOG_TAG, "Could not complete configuration computation"); + return null; + } + } + + // Maximize the number of packed bitmaps, minimize the texture size + Collections.sort(results, new Comparator() { + @Override + public int compare(WorkerResult r1, WorkerResult r2) { + int delta = r2.count - r1.count; + if (delta != 0) return delta; + return r1.width * r1.height - r2.width * r2.height; + } + }); + + if (DEBUG_ATLAS) { + float delay = (System.nanoTime() - begin) / 1000.0f / 1000.0f / 1000.0f; + Log.d(LOG_TAG, String.format("Found best atlas configuration in %.2fs", delay)); + } + + WorkerResult result = results.get(0); + return new Configuration(result.type, result.width, result.height, result.count); + } + + /** + * Returns the path to the file containing the best computed + * atlas configuration. + */ + private static File getDataFile() { + File systemDirectory = new File(Environment.getDataDirectory(), "system"); + return new File(systemDirectory, "framework_atlas.config"); + } + + private static void deleteDataFile() { + Log.w(LOG_TAG, "Current configuration inconsistent with assets list"); + if (!getDataFile().delete()) { + Log.w(LOG_TAG, "Could not delete the current configuration"); + } + } + + private File getFrameworkResourcesFile() { + return new File(mContext.getApplicationInfo().sourceDir); + } + + /** + * Returns the best known atlas configuration. This method will either + * read the configuration from disk or start a brute-force search + * and save the result out to disk. + */ + private Configuration chooseConfiguration(ArrayList bitmaps, int pixelCount, + String versionName) { + Configuration config = null; + + final File dataFile = getDataFile(); + if (dataFile.exists()) { + config = readConfiguration(dataFile, versionName); + } + + if (config == null) { + config = computeBestConfiguration(bitmaps, pixelCount); + if (config != null) writeConfiguration(config, dataFile, versionName); + } + + return config; + } + + /** + * Writes the specified atlas configuration to the specified file. + */ + private void writeConfiguration(Configuration config, File file, String versionName) { + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); + writer.write(getBuildIdentifier(versionName)); + writer.newLine(); + writer.write(config.type.toString()); + writer.newLine(); + writer.write(String.valueOf(config.width)); + writer.newLine(); + writer.write(String.valueOf(config.height)); + writer.newLine(); + writer.write(String.valueOf(config.count)); + writer.newLine(); + writer.write(String.valueOf(config.flags)); + writer.newLine(); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "Could not write " + file, e); + } catch (IOException e) { + Log.w(LOG_TAG, "Could not write " + file, e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + /** + * Reads an atlas configuration from the specified file. This method + * returns null if an error occurs or if the configuration is invalid. + */ + private Configuration readConfiguration(File file, String versionName) { + BufferedReader reader = null; + Configuration config = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); + + if (checkBuildIdentifier(reader, versionName)) { + Atlas.Type type = Atlas.Type.valueOf(reader.readLine()); + int width = readInt(reader, MIN_SIZE, MAX_SIZE); + int height = readInt(reader, MIN_SIZE, MAX_SIZE); + int count = readInt(reader, 0, Integer.MAX_VALUE); + int flags = readInt(reader, Integer.MIN_VALUE, Integer.MAX_VALUE); + + config = new Configuration(type, width, height, count, flags); + } + } catch (IllegalArgumentException e) { + Log.w(LOG_TAG, "Invalid parameter value in " + file, e); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "Could not read " + file, e); + } catch (IOException e) { + Log.w(LOG_TAG, "Could not read " + file, e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // Ignore + } + } + } + return config; + } + + private static int readInt(BufferedReader reader, int min, int max) throws IOException { + return Math.max(min, Math.min(max, Integer.parseInt(reader.readLine()))); + } + + /** + * Compares the next line in the specified buffered reader to the current + * build identifier. Returns whether the two values are equal. + * + * @see #getBuildIdentifier(String) + */ + private boolean checkBuildIdentifier(BufferedReader reader, String versionName) + throws IOException { + String deviceBuildId = getBuildIdentifier(versionName); + String buildId = reader.readLine(); + return deviceBuildId.equals(buildId); + } + + /** + * Returns an identifier for the current build that can be used to detect + * likely changes to framework resources. The build identifier is made of + * several distinct values: + * + * build fingerprint/framework version name/file size of framework resources apk + * + * Only the build fingerprint should be necessary on user builds but + * the other values are useful to detect changes on eng builds during + * development. + * + * This identifier does not attempt to be exact: a new identifier does not + * necessarily mean the preloaded drawables have changed. It is important + * however that whenever the list of preloaded drawables changes, this + * identifier changes as well. + * + * @see #checkBuildIdentifier(java.io.BufferedReader, String) + */ + private String getBuildIdentifier(String versionName) { + return SystemProperties.get("ro.build.fingerprint", "") + '/' + versionName + '/' + + String.valueOf(getFrameworkResourcesFile().length()); + } + + /** + * Atlas configuration. Specifies the algorithm, dimensions and flags to use. + */ + private static class Configuration { + final Atlas.Type type; + final int width; + final int height; + final int count; + final int flags; + + Configuration(Atlas.Type type, int width, int height, int count) { + this(type, width, height, count, Atlas.FLAG_DEFAULTS); + } + + Configuration(Atlas.Type type, int width, int height, int count, int flags) { + this.type = type; + this.width = width; + this.height = height; + this.count = count; + this.flags = flags; + } + + @Override + public String toString() { + return type.toString() + " (" + width + "x" + height + ") flags=0x" + + Integer.toHexString(flags) + " count=" + count; + } + } + + /** + * Used during the brute-force search to gather information about each + * variant of the packing algorithm. + */ + private static class WorkerResult { + Atlas.Type type; + int width; + int height; + int count; + + WorkerResult(Atlas.Type type, int width, int height, int count) { + this.type = type; + this.width = width; + this.height = height; + this.count = count; + } + + @Override + public String toString() { + return String.format("%s %dx%d", type.toString(), width, height); + } + } + + /** + * A compute worker will try a finite number of variations of the packing + * algorithms and save the results in a supplied list. + */ + private static class ComputeWorker implements Runnable { + private final int mStart; + private final int mEnd; + private final int mStep; + private final List mBitmaps; + private final List mResults; + private final CountDownLatch mSignal; + private final int mThreshold; + + /** + * Creates a new compute worker to brute-force through a range of + * packing algorithms variants. + * + * @param start The minimum texture width to try + * @param end The maximum texture width to try + * @param step The number of pixels to increment the texture width by at each step + * @param bitmaps The list of bitmaps to pack in the atlas + * @param pixelCount The total number of pixels occupied by the list of bitmaps + * @param results The list of results in which to save the brute-force search results + * @param signal Latch to decrement when this worker is done, may be null + */ + ComputeWorker(int start, int end, int step, List bitmaps, int pixelCount, + List results, CountDownLatch signal) { + mStart = start; + mEnd = end; + mStep = step; + mBitmaps = bitmaps; + mResults = results; + mSignal = signal; + + // Minimum number of pixels we want to be able to pack + int threshold = (int) (pixelCount * PACKING_THRESHOLD); + // Make sure we can find at least one configuration + while (threshold > MAX_SIZE * MAX_SIZE) { + threshold >>= 1; + } + mThreshold = threshold; + } + + @Override + public void run() { + if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName()); + + Atlas.Entry entry = new Atlas.Entry(); + for (Atlas.Type type : Atlas.Type.values()) { + for (int width = mStart; width < mEnd; width += mStep) { + for (int height = MIN_SIZE; height < MAX_SIZE; height += STEP) { + // If the atlas is not big enough, skip it + if (width * height <= mThreshold) continue; + + final int count = packBitmaps(type, width, height, entry); + if (count > 0) { + mResults.add(new WorkerResult(type, width, height, count)); + // If we were able to pack everything let's stop here + // Increasing the height further won't make things better + if (count == mBitmaps.size()) { + break; + } + } + } + } + } + + if (mSignal != null) { + mSignal.countDown(); + } + } + + private int packBitmaps(Atlas.Type type, int width, int height, Atlas.Entry entry) { + int total = 0; + Atlas atlas = new Atlas(type, width, height); + + final int count = mBitmaps.size(); + for (int i = 0; i < count; i++) { + final Bitmap bitmap = mBitmaps.get(i); + if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { + total++; + } + } + + return total; + } + } +} diff --git a/services/core/java/com/android/server/AttributeCache.java b/services/core/java/com/android/server/AttributeCache.java new file mode 100644 index 0000000..427dbc0 --- /dev/null +++ b/services/core/java/com/android/server/AttributeCache.java @@ -0,0 +1,143 @@ +/* +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.server; + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.UserHandle; +import android.util.SparseArray; + +import java.util.HashMap; +import java.util.WeakHashMap; + +/** + * TODO: This should be better integrated into the system so it doesn't need + * special calls from the activity manager to clear it. + */ +public final class AttributeCache { + private static AttributeCache sInstance = null; + + private final Context mContext; + private final WeakHashMap mPackages = + new WeakHashMap(); + private final Configuration mConfiguration = new Configuration(); + + public final static class Package { + public final Context context; + private final SparseArray> mMap + = new SparseArray>(); + + public Package(Context c) { + context = c; + } + } + + public final static class Entry { + public final Context context; + public final TypedArray array; + + public Entry(Context c, TypedArray ta) { + context = c; + array = ta; + } + } + + public static void init(Context context) { + if (sInstance == null) { + sInstance = new AttributeCache(context); + } + } + + public static AttributeCache instance() { + return sInstance; + } + + public AttributeCache(Context context) { + mContext = context; + } + + public void removePackage(String packageName) { + synchronized (this) { + mPackages.remove(packageName); + } + } + + public void updateConfiguration(Configuration config) { + synchronized (this) { + int changes = mConfiguration.updateFrom(config); + if ((changes & ~(ActivityInfo.CONFIG_FONT_SCALE | + ActivityInfo.CONFIG_KEYBOARD_HIDDEN | + ActivityInfo.CONFIG_ORIENTATION)) != 0) { + // The configurations being masked out are ones that commonly + // change so we don't want flushing the cache... all others + // will flush the cache. + mPackages.clear(); + } + } + } + + public Entry get(String packageName, int resId, int[] styleable, int userId) { + synchronized (this) { + Package pkg = mPackages.get(packageName); + HashMap map = null; + Entry ent = null; + if (pkg != null) { + map = pkg.mMap.get(resId); + if (map != null) { + ent = map.get(styleable); + if (ent != null) { + return ent; + } + } + } else { + Context context; + try { + context = mContext.createPackageContextAsUser(packageName, 0, + new UserHandle(userId)); + if (context == null) { + return null; + } + } catch (PackageManager.NameNotFoundException e) { + return null; + } + pkg = new Package(context); + mPackages.put(packageName, pkg); + } + + if (map == null) { + map = new HashMap(); + pkg.mMap.put(resId, map); + } + + try { + ent = new Entry(pkg.context, + pkg.context.obtainStyledAttributes(resId, styleable)); + map.put(styleable, ent); + } catch (Resources.NotFoundException e) { + return null; + } + + return ent; + } + } +} + diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java new file mode 100644 index 0000000..cc9055d --- /dev/null +++ b/services/core/java/com/android/server/BatteryService.java @@ -0,0 +1,753 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.os.BatteryStats; +import com.android.internal.app.IBatteryStats; +import com.android.server.am.BatteryStatsService; +import com.android.server.lights.Light; +import com.android.server.lights.LightsManager; + +import android.app.ActivityManagerNative; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.BatteryManager; +import android.os.BatteryProperties; +import android.os.Binder; +import android.os.FileUtils; +import android.os.Handler; +import android.os.IBatteryPropertiesListener; +import android.os.IBatteryPropertiesRegistrar; +import android.os.IBinder; +import android.os.DropBoxManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UEventObserver; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.EventLog; +import android.util.Slog; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + + +/** + *

      BatteryService monitors the charging status, and charge level of the device + * battery. When these values change this service broadcasts the new values + * to all {@link android.content.BroadcastReceiver IntentReceivers} that are + * watching the {@link android.content.Intent#ACTION_BATTERY_CHANGED + * BATTERY_CHANGED} action.

      + *

      The new values are stored in the Intent data and can be retrieved by + * calling {@link android.content.Intent#getExtra Intent.getExtra} with the + * following keys:

      + *

      "scale" - int, the maximum value for the charge level

      + *

      "level" - int, charge level, from 0 through "scale" inclusive

      + *

      "status" - String, the current charging status.
      + *

      "health" - String, the current battery health.
      + *

      "present" - boolean, true if the battery is present
      + *

      "icon-small" - int, suggested small icon to use for this state

      + *

      "plugged" - int, 0 if the device is not plugged in; 1 if plugged + * into an AC power adapter; 2 if plugged in via USB.

      + *

      "voltage" - int, current battery voltage in millivolts

      + *

      "temperature" - int, current battery temperature in tenths of + * a degree Centigrade

      + *

      "technology" - String, the type of battery installed, e.g. "Li-ion"

      + * + *

      + * The battery service may be called by the power manager while holding its locks so + * we take care to post all outcalls into the activity manager to a handler. + * + * FIXME: Ideally the power manager would perform all of its calls into the battery + * service asynchronously itself. + *

      + */ +public final class BatteryService extends Binder { + private static final String TAG = BatteryService.class.getSimpleName(); + + private static final boolean DEBUG = false; + + private static final int BATTERY_SCALE = 100; // battery capacity is a percentage + + // Used locally for determining when to make a last ditch effort to log + // discharge stats before the device dies. + private int mCriticalBatteryLevel; + + private static final int DUMP_MAX_LENGTH = 24 * 1024; + private static final String[] DUMPSYS_ARGS = new String[] { "--checkin", "--unplugged" }; + + private static final String DUMPSYS_DATA_PATH = "/data/system/"; + + // This should probably be exposed in the API, though it's not critical + private static final int BATTERY_PLUGGED_NONE = 0; + + private final Context mContext; + private final IBatteryStats mBatteryStats; + private final Handler mHandler; + + private final Object mLock = new Object(); + + private BatteryProperties mBatteryProps; + private boolean mBatteryLevelCritical; + private int mLastBatteryStatus; + private int mLastBatteryHealth; + private boolean mLastBatteryPresent; + private int mLastBatteryLevel; + private int mLastBatteryVoltage; + private int mLastBatteryTemperature; + private boolean mLastBatteryLevelCritical; + + private int mInvalidCharger; + private int mLastInvalidCharger; + + private int mLowBatteryWarningLevel; + private int mLowBatteryCloseWarningLevel; + private int mShutdownBatteryTemperature; + + private int mPlugType; + private int mLastPlugType = -1; // Extra state so we can detect first run + + private long mDischargeStartTime; + private int mDischargeStartLevel; + + private boolean mUpdatesStopped; + + private Led mLed; + + private boolean mSentLowBatteryBroadcast = false; + + public BatteryService(Context context, LightsManager lightsManager) { + mContext = context; + mHandler = new Handler(true /*async*/); + mLed = new Led(context, lightsManager); + mBatteryStats = BatteryStatsService.getService(); + + mCriticalBatteryLevel = mContext.getResources().getInteger( + com.android.internal.R.integer.config_criticalBatteryWarningLevel); + mLowBatteryWarningLevel = mContext.getResources().getInteger( + com.android.internal.R.integer.config_lowBatteryWarningLevel); + mLowBatteryCloseWarningLevel = mContext.getResources().getInteger( + com.android.internal.R.integer.config_lowBatteryCloseWarningLevel); + mShutdownBatteryTemperature = mContext.getResources().getInteger( + com.android.internal.R.integer.config_shutdownBatteryTemperature); + + // watch for invalid charger messages if the invalid_charger switch exists + if (new File("/sys/devices/virtual/switch/invalid_charger/state").exists()) { + mInvalidChargerObserver.startObserving( + "DEVPATH=/devices/virtual/switch/invalid_charger"); + } + + IBinder b = ServiceManager.getService("batterypropreg"); + final IBatteryPropertiesRegistrar batteryPropertiesRegistrar = + IBatteryPropertiesRegistrar.Stub.asInterface(b); + try { + batteryPropertiesRegistrar.registerListener(new BatteryListener()); + } catch (RemoteException e) { + // Should never happen. + } + } + + void systemReady() { + // check our power situation now that it is safe to display the shutdown dialog. + synchronized (mLock) { + shutdownIfNoPowerLocked(); + shutdownIfOverTempLocked(); + } + } + + /** + * Returns true if the device is plugged into any of the specified plug types. + */ + public boolean isPowered(int plugTypeSet) { + synchronized (mLock) { + return isPoweredLocked(plugTypeSet); + } + } + + private boolean isPoweredLocked(int plugTypeSet) { + // assume we are powered if battery state is unknown so + // the "stay on while plugged in" option will work. + if (mBatteryProps.batteryStatus == BatteryManager.BATTERY_STATUS_UNKNOWN) { + return true; + } + if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_AC) != 0 && mBatteryProps.chargerAcOnline) { + return true; + } + if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_USB) != 0 && mBatteryProps.chargerUsbOnline) { + return true; + } + if ((plugTypeSet & BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0 && mBatteryProps.chargerWirelessOnline) { + return true; + } + return false; + } + + /** + * Returns the current plug type. + */ + public int getPlugType() { + synchronized (mLock) { + return mPlugType; + } + } + + /** + * Returns battery level as a percentage. + */ + public int getBatteryLevel() { + synchronized (mLock) { + return mBatteryProps.batteryLevel; + } + } + + /** + * Returns true if battery level is below the first warning threshold. + */ + public boolean isBatteryLow() { + synchronized (mLock) { + return mBatteryProps.batteryPresent && mBatteryProps.batteryLevel <= mLowBatteryWarningLevel; + } + } + + /** + * Returns a non-zero value if an unsupported charger is attached. + */ + public int getInvalidCharger() { + synchronized (mLock) { + return mInvalidCharger; + } + } + + private void shutdownIfNoPowerLocked() { + // shut down gracefully if our battery is critically low and we are not powered. + // wait until the system has booted before attempting to display the shutdown dialog. + if (mBatteryProps.batteryLevel == 0 && !isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY)) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (ActivityManagerNative.isSystemReady()) { + Intent intent = new Intent(Intent.ACTION_REQUEST_SHUTDOWN); + intent.putExtra(Intent.EXTRA_KEY_CONFIRM, false); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } + } + }); + } + } + + private void shutdownIfOverTempLocked() { + // shut down gracefully if temperature is too high (> 68.0C by default) + // wait until the system has booted before attempting to display the + // shutdown dialog. + if (mBatteryProps.batteryTemperature > mShutdownBatteryTemperature) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (ActivityManagerNative.isSystemReady()) { + Intent intent = new Intent(Intent.ACTION_REQUEST_SHUTDOWN); + intent.putExtra(Intent.EXTRA_KEY_CONFIRM, false); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } + } + }); + } + } + + private void update(BatteryProperties props) { + synchronized (mLock) { + if (!mUpdatesStopped) { + mBatteryProps = props; + // Process the new values. + processValuesLocked(); + } + } + } + + private void processValuesLocked() { + boolean logOutlier = false; + long dischargeDuration = 0; + + mBatteryLevelCritical = (mBatteryProps.batteryLevel <= mCriticalBatteryLevel); + if (mBatteryProps.chargerAcOnline) { + mPlugType = BatteryManager.BATTERY_PLUGGED_AC; + } else if (mBatteryProps.chargerUsbOnline) { + mPlugType = BatteryManager.BATTERY_PLUGGED_USB; + } else if (mBatteryProps.chargerWirelessOnline) { + mPlugType = BatteryManager.BATTERY_PLUGGED_WIRELESS; + } else { + mPlugType = BATTERY_PLUGGED_NONE; + } + + if (DEBUG) { + Slog.d(TAG, "Processing new values: " + + "chargerAcOnline=" + mBatteryProps.chargerAcOnline + + ", chargerUsbOnline=" + mBatteryProps.chargerUsbOnline + + ", chargerWirelessOnline=" + mBatteryProps.chargerWirelessOnline + + ", batteryStatus=" + mBatteryProps.batteryStatus + + ", batteryHealth=" + mBatteryProps.batteryHealth + + ", batteryPresent=" + mBatteryProps.batteryPresent + + ", batteryLevel=" + mBatteryProps.batteryLevel + + ", batteryTechnology=" + mBatteryProps.batteryTechnology + + ", batteryVoltage=" + mBatteryProps.batteryVoltage + + ", batteryCurrentNow=" + mBatteryProps.batteryCurrentNow + + ", batteryChargeCounter=" + mBatteryProps.batteryChargeCounter + + ", batteryTemperature=" + mBatteryProps.batteryTemperature + + ", mBatteryLevelCritical=" + mBatteryLevelCritical + + ", mPlugType=" + mPlugType); + } + + // Let the battery stats keep track of the current level. + try { + mBatteryStats.setBatteryState(mBatteryProps.batteryStatus, mBatteryProps.batteryHealth, + mPlugType, mBatteryProps.batteryLevel, mBatteryProps.batteryTemperature, + mBatteryProps.batteryVoltage); + } catch (RemoteException e) { + // Should never happen. + } + + shutdownIfNoPowerLocked(); + shutdownIfOverTempLocked(); + + if (mBatteryProps.batteryStatus != mLastBatteryStatus || + mBatteryProps.batteryHealth != mLastBatteryHealth || + mBatteryProps.batteryPresent != mLastBatteryPresent || + mBatteryProps.batteryLevel != mLastBatteryLevel || + mPlugType != mLastPlugType || + mBatteryProps.batteryVoltage != mLastBatteryVoltage || + mBatteryProps.batteryTemperature != mLastBatteryTemperature || + mInvalidCharger != mLastInvalidCharger) { + + if (mPlugType != mLastPlugType) { + if (mLastPlugType == BATTERY_PLUGGED_NONE) { + // discharging -> charging + + // There's no value in this data unless we've discharged at least once and the + // battery level has changed; so don't log until it does. + if (mDischargeStartTime != 0 && mDischargeStartLevel != mBatteryProps.batteryLevel) { + dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime; + logOutlier = true; + EventLog.writeEvent(EventLogTags.BATTERY_DISCHARGE, dischargeDuration, + mDischargeStartLevel, mBatteryProps.batteryLevel); + // make sure we see a discharge event before logging again + mDischargeStartTime = 0; + } + } else if (mPlugType == BATTERY_PLUGGED_NONE) { + // charging -> discharging or we just powered up + mDischargeStartTime = SystemClock.elapsedRealtime(); + mDischargeStartLevel = mBatteryProps.batteryLevel; + } + } + if (mBatteryProps.batteryStatus != mLastBatteryStatus || + mBatteryProps.batteryHealth != mLastBatteryHealth || + mBatteryProps.batteryPresent != mLastBatteryPresent || + mPlugType != mLastPlugType) { + EventLog.writeEvent(EventLogTags.BATTERY_STATUS, + mBatteryProps.batteryStatus, mBatteryProps.batteryHealth, mBatteryProps.batteryPresent ? 1 : 0, + mPlugType, mBatteryProps.batteryTechnology); + } + if (mBatteryProps.batteryLevel != mLastBatteryLevel) { + // Don't do this just from voltage or temperature changes, that is + // too noisy. + EventLog.writeEvent(EventLogTags.BATTERY_LEVEL, + mBatteryProps.batteryLevel, mBatteryProps.batteryVoltage, mBatteryProps.batteryTemperature); + } + if (mBatteryLevelCritical && !mLastBatteryLevelCritical && + mPlugType == BATTERY_PLUGGED_NONE) { + // We want to make sure we log discharge cycle outliers + // if the battery is about to die. + dischargeDuration = SystemClock.elapsedRealtime() - mDischargeStartTime; + logOutlier = true; + } + + final boolean plugged = mPlugType != BATTERY_PLUGGED_NONE; + final boolean oldPlugged = mLastPlugType != BATTERY_PLUGGED_NONE; + + /* The ACTION_BATTERY_LOW broadcast is sent in these situations: + * - is just un-plugged (previously was plugged) and battery level is + * less than or equal to WARNING, or + * - is not plugged and battery level falls to WARNING boundary + * (becomes <= mLowBatteryWarningLevel). + */ + final boolean sendBatteryLow = !plugged + && mBatteryProps.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN + && mBatteryProps.batteryLevel <= mLowBatteryWarningLevel + && (oldPlugged || mLastBatteryLevel > mLowBatteryWarningLevel); + + sendIntentLocked(); + + // Separate broadcast is sent for power connected / not connected + // since the standard intent will not wake any applications and some + // applications may want to have smart behavior based on this. + if (mPlugType != 0 && mLastPlugType == 0) { + mHandler.post(new Runnable() { + @Override + public void run() { + Intent statusIntent = new Intent(Intent.ACTION_POWER_CONNECTED); + statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL); + } + }); + } + else if (mPlugType == 0 && mLastPlugType != 0) { + mHandler.post(new Runnable() { + @Override + public void run() { + Intent statusIntent = new Intent(Intent.ACTION_POWER_DISCONNECTED); + statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL); + } + }); + } + + if (sendBatteryLow) { + mSentLowBatteryBroadcast = true; + mHandler.post(new Runnable() { + @Override + public void run() { + Intent statusIntent = new Intent(Intent.ACTION_BATTERY_LOW); + statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL); + } + }); + } else if (mSentLowBatteryBroadcast && mLastBatteryLevel >= mLowBatteryCloseWarningLevel) { + mSentLowBatteryBroadcast = false; + mHandler.post(new Runnable() { + @Override + public void run() { + Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY); + statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL); + } + }); + } + + // Update the battery LED + mLed.updateLightsLocked(); + + // This needs to be done after sendIntent() so that we get the lastest battery stats. + if (logOutlier && dischargeDuration != 0) { + logOutlierLocked(dischargeDuration); + } + + mLastBatteryStatus = mBatteryProps.batteryStatus; + mLastBatteryHealth = mBatteryProps.batteryHealth; + mLastBatteryPresent = mBatteryProps.batteryPresent; + mLastBatteryLevel = mBatteryProps.batteryLevel; + mLastPlugType = mPlugType; + mLastBatteryVoltage = mBatteryProps.batteryVoltage; + mLastBatteryTemperature = mBatteryProps.batteryTemperature; + mLastBatteryLevelCritical = mBatteryLevelCritical; + mLastInvalidCharger = mInvalidCharger; + } + } + + private void sendIntentLocked() { + // Pack up the values and broadcast them to everyone + final Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_REPLACE_PENDING); + + int icon = getIconLocked(mBatteryProps.batteryLevel); + + intent.putExtra(BatteryManager.EXTRA_STATUS, mBatteryProps.batteryStatus); + intent.putExtra(BatteryManager.EXTRA_HEALTH, mBatteryProps.batteryHealth); + intent.putExtra(BatteryManager.EXTRA_PRESENT, mBatteryProps.batteryPresent); + intent.putExtra(BatteryManager.EXTRA_LEVEL, mBatteryProps.batteryLevel); + intent.putExtra(BatteryManager.EXTRA_SCALE, BATTERY_SCALE); + intent.putExtra(BatteryManager.EXTRA_ICON_SMALL, icon); + intent.putExtra(BatteryManager.EXTRA_PLUGGED, mPlugType); + intent.putExtra(BatteryManager.EXTRA_VOLTAGE, mBatteryProps.batteryVoltage); + intent.putExtra(BatteryManager.EXTRA_TEMPERATURE, mBatteryProps.batteryTemperature); + intent.putExtra(BatteryManager.EXTRA_TECHNOLOGY, mBatteryProps.batteryTechnology); + intent.putExtra(BatteryManager.EXTRA_INVALID_CHARGER, mInvalidCharger); + + if (DEBUG) { + Slog.d(TAG, "Sending ACTION_BATTERY_CHANGED. level:" + mBatteryProps.batteryLevel + + ", scale:" + BATTERY_SCALE + ", status:" + mBatteryProps.batteryStatus + + ", health:" + mBatteryProps.batteryHealth + ", present:" + mBatteryProps.batteryPresent + + ", voltage: " + mBatteryProps.batteryVoltage + + ", temperature: " + mBatteryProps.batteryTemperature + + ", technology: " + mBatteryProps.batteryTechnology + + ", AC powered:" + mBatteryProps.chargerAcOnline + ", USB powered:" + mBatteryProps.chargerUsbOnline + + ", Wireless powered:" + mBatteryProps.chargerWirelessOnline + + ", icon:" + icon + ", invalid charger:" + mInvalidCharger); + } + + mHandler.post(new Runnable() { + @Override + public void run() { + ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL); + } + }); + } + + private void logBatteryStatsLocked() { + IBinder batteryInfoService = ServiceManager.getService(BatteryStats.SERVICE_NAME); + if (batteryInfoService == null) return; + + DropBoxManager db = (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE); + if (db == null || !db.isTagEnabled("BATTERY_DISCHARGE_INFO")) return; + + File dumpFile = null; + FileOutputStream dumpStream = null; + try { + // dump the service to a file + dumpFile = new File(DUMPSYS_DATA_PATH + BatteryStats.SERVICE_NAME + ".dump"); + dumpStream = new FileOutputStream(dumpFile); + batteryInfoService.dump(dumpStream.getFD(), DUMPSYS_ARGS); + FileUtils.sync(dumpStream); + + // add dump file to drop box + db.addFile("BATTERY_DISCHARGE_INFO", dumpFile, DropBoxManager.IS_TEXT); + } catch (RemoteException e) { + Slog.e(TAG, "failed to dump battery service", e); + } catch (IOException e) { + Slog.e(TAG, "failed to write dumpsys file", e); + } finally { + // make sure we clean up + if (dumpStream != null) { + try { + dumpStream.close(); + } catch (IOException e) { + Slog.e(TAG, "failed to close dumpsys output stream"); + } + } + if (dumpFile != null && !dumpFile.delete()) { + Slog.e(TAG, "failed to delete temporary dumpsys file: " + + dumpFile.getAbsolutePath()); + } + } + } + + private void logOutlierLocked(long duration) { + ContentResolver cr = mContext.getContentResolver(); + String dischargeThresholdString = Settings.Global.getString(cr, + Settings.Global.BATTERY_DISCHARGE_THRESHOLD); + String durationThresholdString = Settings.Global.getString(cr, + Settings.Global.BATTERY_DISCHARGE_DURATION_THRESHOLD); + + if (dischargeThresholdString != null && durationThresholdString != null) { + try { + long durationThreshold = Long.parseLong(durationThresholdString); + int dischargeThreshold = Integer.parseInt(dischargeThresholdString); + if (duration <= durationThreshold && + mDischargeStartLevel - mBatteryProps.batteryLevel >= dischargeThreshold) { + // If the discharge cycle is bad enough we want to know about it. + logBatteryStatsLocked(); + } + if (DEBUG) Slog.v(TAG, "duration threshold: " + durationThreshold + + " discharge threshold: " + dischargeThreshold); + if (DEBUG) Slog.v(TAG, "duration: " + duration + " discharge: " + + (mDischargeStartLevel - mBatteryProps.batteryLevel)); + } catch (NumberFormatException e) { + Slog.e(TAG, "Invalid DischargeThresholds GService string: " + + durationThresholdString + " or " + dischargeThresholdString); + return; + } + } + } + + private int getIconLocked(int level) { + if (mBatteryProps.batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) { + return com.android.internal.R.drawable.stat_sys_battery_charge; + } else if (mBatteryProps.batteryStatus == BatteryManager.BATTERY_STATUS_DISCHARGING) { + return com.android.internal.R.drawable.stat_sys_battery; + } else if (mBatteryProps.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING + || mBatteryProps.batteryStatus == BatteryManager.BATTERY_STATUS_FULL) { + if (isPoweredLocked(BatteryManager.BATTERY_PLUGGED_ANY) + && mBatteryProps.batteryLevel >= 100) { + return com.android.internal.R.drawable.stat_sys_battery_charge; + } else { + return com.android.internal.R.drawable.stat_sys_battery; + } + } else { + return com.android.internal.R.drawable.stat_sys_battery_unknown; + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + pw.println("Permission Denial: can't dump Battery service from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mLock) { + if (args == null || args.length == 0 || "-a".equals(args[0])) { + pw.println("Current Battery Service state:"); + if (mUpdatesStopped) { + pw.println(" (UPDATES STOPPED -- use 'reset' to restart)"); + } + pw.println(" AC powered: " + mBatteryProps.chargerAcOnline); + pw.println(" USB powered: " + mBatteryProps.chargerUsbOnline); + pw.println(" Wireless powered: " + mBatteryProps.chargerWirelessOnline); + pw.println(" status: " + mBatteryProps.batteryStatus); + pw.println(" health: " + mBatteryProps.batteryHealth); + pw.println(" present: " + mBatteryProps.batteryPresent); + pw.println(" level: " + mBatteryProps.batteryLevel); + pw.println(" scale: " + BATTERY_SCALE); + pw.println(" voltage: " + mBatteryProps.batteryVoltage); + + if (mBatteryProps.batteryCurrentNow != Integer.MIN_VALUE) { + pw.println(" current now: " + mBatteryProps.batteryCurrentNow); + } + + if (mBatteryProps.batteryChargeCounter != Integer.MIN_VALUE) { + pw.println(" charge counter: " + mBatteryProps.batteryChargeCounter); + } + + pw.println(" temperature: " + mBatteryProps.batteryTemperature); + pw.println(" technology: " + mBatteryProps.batteryTechnology); + } else if (args.length == 3 && "set".equals(args[0])) { + String key = args[1]; + String value = args[2]; + try { + boolean update = true; + if ("ac".equals(key)) { + mBatteryProps.chargerAcOnline = Integer.parseInt(value) != 0; + } else if ("usb".equals(key)) { + mBatteryProps.chargerUsbOnline = Integer.parseInt(value) != 0; + } else if ("wireless".equals(key)) { + mBatteryProps.chargerWirelessOnline = Integer.parseInt(value) != 0; + } else if ("status".equals(key)) { + mBatteryProps.batteryStatus = Integer.parseInt(value); + } else if ("level".equals(key)) { + mBatteryProps.batteryLevel = Integer.parseInt(value); + } else if ("invalid".equals(key)) { + mInvalidCharger = Integer.parseInt(value); + } else { + pw.println("Unknown set option: " + key); + update = false; + } + if (update) { + long ident = Binder.clearCallingIdentity(); + try { + mUpdatesStopped = true; + processValuesLocked(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } catch (NumberFormatException ex) { + pw.println("Bad value: " + value); + } + } else if (args.length == 1 && "reset".equals(args[0])) { + long ident = Binder.clearCallingIdentity(); + try { + mUpdatesStopped = false; + } finally { + Binder.restoreCallingIdentity(ident); + } + } else { + pw.println("Dump current battery state, or:"); + pw.println(" set ac|usb|wireless|status|level|invalid "); + pw.println(" reset"); + } + } + } + + private final UEventObserver mInvalidChargerObserver = new UEventObserver() { + @Override + public void onUEvent(UEventObserver.UEvent event) { + final int invalidCharger = "1".equals(event.get("SWITCH_STATE")) ? 1 : 0; + synchronized (mLock) { + if (mInvalidCharger != invalidCharger) { + mInvalidCharger = invalidCharger; + } + } + } + }; + + private final class Led { + private final Light mBatteryLight; + + private final int mBatteryLowARGB; + private final int mBatteryMediumARGB; + private final int mBatteryFullARGB; + private final int mBatteryLedOn; + private final int mBatteryLedOff; + + public Led(Context context, LightsManager lights) { + mBatteryLight = lights.getLight(LightsManager.LIGHT_ID_BATTERY); + + mBatteryLowARGB = context.getResources().getInteger( + com.android.internal.R.integer.config_notificationsBatteryLowARGB); + mBatteryMediumARGB = context.getResources().getInteger( + com.android.internal.R.integer.config_notificationsBatteryMediumARGB); + mBatteryFullARGB = context.getResources().getInteger( + com.android.internal.R.integer.config_notificationsBatteryFullARGB); + mBatteryLedOn = context.getResources().getInteger( + com.android.internal.R.integer.config_notificationsBatteryLedOn); + mBatteryLedOff = context.getResources().getInteger( + com.android.internal.R.integer.config_notificationsBatteryLedOff); + } + + /** + * Synchronize on BatteryService. + */ + public void updateLightsLocked() { + final int level = mBatteryProps.batteryLevel; + final int status = mBatteryProps.batteryStatus; + if (level < mLowBatteryWarningLevel) { + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + // Solid red when battery is charging + mBatteryLight.setColor(mBatteryLowARGB); + } else { + // Flash red when battery is low and not charging + mBatteryLight.setFlashing(mBatteryLowARGB, Light.LIGHT_FLASH_TIMED, + mBatteryLedOn, mBatteryLedOff); + } + } else if (status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL) { + if (status == BatteryManager.BATTERY_STATUS_FULL || level >= 90) { + // Solid green when full or charging and nearly full + mBatteryLight.setColor(mBatteryFullARGB); + } else { + // Solid orange when charging and halfway full + mBatteryLight.setColor(mBatteryMediumARGB); + } + } else { + // No lights if not charging and not low + mBatteryLight.turnOff(); + } + } + } + + private final class BatteryListener extends IBatteryPropertiesListener.Stub { + @Override + public void batteryPropertiesChanged(BatteryProperties props) { + final long identity = Binder.clearCallingIdentity(); + try { + BatteryService.this.update(props); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } +} diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java new file mode 100644 index 0000000..546324a --- /dev/null +++ b/services/core/java/com/android/server/BluetoothManagerService.java @@ -0,0 +1,1262 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.ActivityManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothGatt; +import android.bluetooth.IBluetoothCallback; +import android.bluetooth.IBluetoothManager; +import android.bluetooth.IBluetoothManagerCallback; +import android.bluetooth.IBluetoothStateChangeCallback; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +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.RemoteCallbackList; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +class BluetoothManagerService extends IBluetoothManager.Stub { + private static final String TAG = "BluetoothManagerService"; + private static final boolean DBG = true; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + private static final String ACTION_SERVICE_STATE_CHANGED="com.android.bluetooth.btservice.action.STATE_CHANGED"; + private static final String EXTRA_ACTION="action"; + private static final String SECURE_SETTINGS_BLUETOOTH_ADDR_VALID="bluetooth_addr_valid"; + private static final String SECURE_SETTINGS_BLUETOOTH_ADDRESS="bluetooth_address"; + private static final String SECURE_SETTINGS_BLUETOOTH_NAME="bluetooth_name"; + private static final int TIMEOUT_BIND_MS = 3000; //Maximum msec to wait for a bind + private static final int TIMEOUT_SAVE_MS = 500; //Maximum msec to wait for a save + //Maximum msec to wait for service restart + private static final int SERVICE_RESTART_TIME_MS = 200; + //Maximum msec to wait for restart due to error + private static final int ERROR_RESTART_TIME_MS = 3000; + //Maximum msec to delay MESSAGE_USER_SWITCHED + private static final int USER_SWITCHED_TIME_MS = 200; + + private static final int MESSAGE_ENABLE = 1; + private static final int MESSAGE_DISABLE = 2; + private static final int MESSAGE_REGISTER_ADAPTER = 20; + private static final int MESSAGE_UNREGISTER_ADAPTER = 21; + private static final int MESSAGE_REGISTER_STATE_CHANGE_CALLBACK = 30; + private static final int MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK = 31; + private static final int MESSAGE_BLUETOOTH_SERVICE_CONNECTED = 40; + private static final int MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED = 41; + private static final int MESSAGE_RESTART_BLUETOOTH_SERVICE = 42; + private static final int MESSAGE_BLUETOOTH_STATE_CHANGE=60; + private static final int MESSAGE_TIMEOUT_BIND =100; + private static final int MESSAGE_TIMEOUT_UNBIND =101; + private static final int MESSAGE_GET_NAME_AND_ADDRESS=200; + private static final int MESSAGE_SAVE_NAME_AND_ADDRESS=201; + private static final int MESSAGE_USER_SWITCHED = 300; + private static final int MAX_SAVE_RETRIES=3; + private static final int MAX_ERROR_RESTART_RETRIES=6; + + // Bluetooth persisted setting is off + private static final int BLUETOOTH_OFF=0; + // Bluetooth persisted setting is on + // and Airplane mode won't affect Bluetooth state at start up + private static final int BLUETOOTH_ON_BLUETOOTH=1; + // Bluetooth persisted setting is on + // but Airplane mode will affect Bluetooth state at start up + // and Airplane mode will have higher priority. + private static final int BLUETOOTH_ON_AIRPLANE=2; + + private static final int SERVICE_IBLUETOOTH = 1; + private static final int SERVICE_IBLUETOOTHGATT = 2; + + private final Context mContext; + + // Locks are not provided for mName and mAddress. + // They are accessed in handler or broadcast receiver, same thread context. + private String mAddress; + private String mName; + private final ContentResolver mContentResolver; + private final RemoteCallbackList mCallbacks; + private final RemoteCallbackList mStateChangeCallbacks; + private IBluetooth mBluetooth; + private IBluetoothGatt mBluetoothGatt; + private boolean mBinding; + private boolean mUnbinding; + // used inside handler thread + private boolean mQuietEnable = false; + // configuarion from external IBinder call which is used to + // synchronize with broadcast receiver. + private boolean mQuietEnableExternal; + // configuarion from external IBinder call which is used to + // synchronize with broadcast receiver. + private boolean mEnableExternal; + // used inside handler thread + private boolean mEnable; + private int mState; + private final BluetoothHandler mHandler; + private int mErrorRecoveryRetryCounter; + + private void registerForAirplaneMode(IntentFilter filter) { + final ContentResolver resolver = mContext.getContentResolver(); + final String airplaneModeRadios = Settings.Global.getString(resolver, + Settings.Global.AIRPLANE_MODE_RADIOS); + final String toggleableRadios = Settings.Global.getString(resolver, + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); + boolean mIsAirplaneSensitive = airplaneModeRadios == null ? true : + airplaneModeRadios.contains(Settings.Global.RADIO_BLUETOOTH); + if (mIsAirplaneSensitive) { + filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); + } + } + + private final IBluetoothCallback mBluetoothCallback = new IBluetoothCallback.Stub() { + @Override + public void onBluetoothStateChange(int prevState, int newState) throws RemoteException { + Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_STATE_CHANGE,prevState,newState); + mHandler.sendMessage(msg); + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED.equals(action)) { + String newName = intent.getStringExtra(BluetoothAdapter.EXTRA_LOCAL_NAME); + if (DBG) Log.d(TAG, "Bluetooth Adapter name changed to " + newName); + if (newName != null) { + storeNameAndAddress(newName, null); + } + } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) { + synchronized(mReceiver) { + if (isBluetoothPersistedStateOn()) { + if (isAirplaneModeOn()) { + persistBluetoothSetting(BLUETOOTH_ON_AIRPLANE); + } else { + persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH); + } + } + if (isAirplaneModeOn()) { + // disable without persisting the setting + sendDisableMsg(); + } else if (mEnableExternal) { + // enable without persisting the setting + sendEnableMsg(mQuietEnableExternal); + } + } + } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_USER_SWITCHED, + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0), 0)); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + synchronized(mReceiver) { + if (mEnableExternal && isBluetoothPersistedStateOnBluetooth()) { + //Enable + if (DBG) Log.d(TAG, "Auto-enabling Bluetooth."); + sendEnableMsg(mQuietEnableExternal); + } + } + + if (!isNameAndAddressSet()) { + //Sync the Bluetooth name and address from the Bluetooth Adapter + if (DBG) Log.d(TAG,"Retrieving Bluetooth Adapter name and address..."); + getNameAndAddress(); + } + } + } + }; + + BluetoothManagerService(Context context) { + mHandler = new BluetoothHandler(IoThread.get().getLooper()); + + mContext = context; + mBluetooth = null; + mBinding = false; + mUnbinding = false; + mEnable = false; + mState = BluetoothAdapter.STATE_OFF; + mQuietEnableExternal = false; + mEnableExternal = false; + mAddress = null; + mName = null; + mErrorRecoveryRetryCounter = 0; + mContentResolver = context.getContentResolver(); + mCallbacks = new RemoteCallbackList(); + mStateChangeCallbacks = new RemoteCallbackList(); + IntentFilter filter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); + filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); + filter.addAction(Intent.ACTION_USER_SWITCHED); + registerForAirplaneMode(filter); + mContext.registerReceiver(mReceiver, filter); + loadStoredNameAndAddress(); + if (isBluetoothPersistedStateOn()) { + mEnableExternal = true; + } + } + + /** + * Returns true if airplane mode is currently on + */ + private final boolean isAirplaneModeOn() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) == 1; + } + + /** + * Returns true if the Bluetooth saved state is "on" + */ + private final boolean isBluetoothPersistedStateOn() { + return Settings.Global.getInt(mContentResolver, + Settings.Global.BLUETOOTH_ON, 0) != BLUETOOTH_OFF; + } + + /** + * Returns true if the Bluetooth saved state is BLUETOOTH_ON_BLUETOOTH + */ + private final boolean isBluetoothPersistedStateOnBluetooth() { + return Settings.Global.getInt(mContentResolver, + Settings.Global.BLUETOOTH_ON, 0) == BLUETOOTH_ON_BLUETOOTH; + } + + /** + * Save the Bluetooth on/off state + * + */ + private void persistBluetoothSetting(int value) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.BLUETOOTH_ON, + value); + } + + /** + * Returns true if the Bluetooth Adapter's name and address is + * locally cached + * @return + */ + private boolean isNameAndAddressSet() { + return mName !=null && mAddress!= null && mName.length()>0 && mAddress.length()>0; + } + + /** + * Retrieve the Bluetooth Adapter's name and address and save it in + * in the local cache + */ + private void loadStoredNameAndAddress() { + if (DBG) Log.d(TAG, "Loading stored name and address"); + if (mContext.getResources().getBoolean + (com.android.internal.R.bool.config_bluetooth_address_validation) && + Settings.Secure.getInt(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDR_VALID, 0) == 0) { + // if the valid flag is not set, don't load the address and name + if (DBG) Log.d(TAG, "invalid bluetooth name and address stored"); + return; + } + mName = Settings.Secure.getString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_NAME); + mAddress = Settings.Secure.getString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDRESS); + if (DBG) Log.d(TAG, "Stored bluetooth Name=" + mName + ",Address=" + mAddress); + } + + /** + * Save the Bluetooth name and address in the persistent store. + * Only non-null values will be saved. + * @param name + * @param address + */ + private void storeNameAndAddress(String name, String address) { + if (name != null) { + Settings.Secure.putString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_NAME, name); + mName = name; + if (DBG) Log.d(TAG,"Stored Bluetooth name: " + + Settings.Secure.getString(mContentResolver,SECURE_SETTINGS_BLUETOOTH_NAME)); + } + + if (address != null) { + Settings.Secure.putString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDRESS, address); + mAddress=address; + if (DBG) Log.d(TAG,"Stored Bluetoothaddress: " + + Settings.Secure.getString(mContentResolver,SECURE_SETTINGS_BLUETOOTH_ADDRESS)); + } + + if ((name != null) && (address != null)) { + Settings.Secure.putInt(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDR_VALID, 1); + } + } + + public IBluetooth registerAdapter(IBluetoothManagerCallback callback){ + Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_ADAPTER); + msg.obj = callback; + mHandler.sendMessage(msg); + synchronized(mConnection) { + return mBluetooth; + } + } + + public void unregisterAdapter(IBluetoothManagerCallback callback) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_ADAPTER); + msg.obj = callback; + mHandler.sendMessage(msg); + } + + public void registerStateChangeCallback(IBluetoothStateChangeCallback callback) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK); + msg.obj = callback; + mHandler.sendMessage(msg); + } + + public void unregisterStateChangeCallback(IBluetoothStateChangeCallback callback) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK); + msg.obj = callback; + mHandler.sendMessage(msg); + } + + public boolean isEnabled() { + if ((Binder.getCallingUid() != Process.SYSTEM_UID) && + (!checkIfCallerIsForegroundUser())) { + Log.w(TAG,"isEnabled(): not allowed for non-active and non system user"); + return false; + } + + synchronized(mConnection) { + try { + return (mBluetooth != null && mBluetooth.isEnabled()); + } catch (RemoteException e) { + Log.e(TAG, "isEnabled()", e); + } + } + return false; + } + + public void getNameAndAddress() { + if (DBG) { + Log.d(TAG,"getNameAndAddress(): mBluetooth = " + mBluetooth + + " mBinding = " + mBinding); + } + Message msg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS); + mHandler.sendMessage(msg); + } + public boolean enableNoAutoConnect() + { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH ADMIN permission"); + + if (DBG) { + Log.d(TAG,"enableNoAutoConnect(): mBluetooth =" + mBluetooth + + " mBinding = " + mBinding); + } + int callingAppId = UserHandle.getAppId(Binder.getCallingUid()); + + if (callingAppId != Process.NFC_UID) { + throw new SecurityException("no permission to enable Bluetooth quietly"); + } + + synchronized(mReceiver) { + mQuietEnableExternal = true; + mEnableExternal = true; + sendEnableMsg(true); + } + return true; + + } + public boolean enable() { + if ((Binder.getCallingUid() != Process.SYSTEM_UID) && + (!checkIfCallerIsForegroundUser())) { + Log.w(TAG,"enable(): not allowed for non-active and non system user"); + return false; + } + + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH ADMIN permission"); + if (DBG) { + Log.d(TAG,"enable(): mBluetooth =" + mBluetooth + + " mBinding = " + mBinding); + } + + synchronized(mReceiver) { + mQuietEnableExternal = false; + mEnableExternal = true; + // waive WRITE_SECURE_SETTINGS permission check + long callingIdentity = Binder.clearCallingIdentity(); + persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH); + Binder.restoreCallingIdentity(callingIdentity); + sendEnableMsg(false); + } + return true; + } + + public boolean disable(boolean persist) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH ADMIN permissicacheNameAndAddresson"); + + if ((Binder.getCallingUid() != Process.SYSTEM_UID) && + (!checkIfCallerIsForegroundUser())) { + Log.w(TAG,"disable(): not allowed for non-active and non system user"); + return false; + } + + if (DBG) { + Log.d(TAG,"disable(): mBluetooth = " + mBluetooth + + " mBinding = " + mBinding); + } + + synchronized(mReceiver) { + if (persist) { + // waive WRITE_SECURE_SETTINGS permission check + long callingIdentity = Binder.clearCallingIdentity(); + persistBluetoothSetting(BLUETOOTH_OFF); + Binder.restoreCallingIdentity(callingIdentity); + } + mEnableExternal = false; + sendDisableMsg(); + } + return true; + } + + public void unbindAndFinish() { + if (DBG) { + Log.d(TAG,"unbindAndFinish(): " + mBluetooth + + " mBinding = " + mBinding); + } + + synchronized (mConnection) { + if (mUnbinding) return; + mUnbinding = true; + if (mBluetooth != null) { + if (!mConnection.isGetNameAddressOnly()) { + //Unregister callback object + try { + mBluetooth.unregisterCallback(mBluetoothCallback); + } catch (RemoteException re) { + Log.e(TAG, "Unable to unregister BluetoothCallback",re); + } + } + if (DBG) Log.d(TAG, "Sending unbind request."); + mBluetooth = null; + //Unbind + mContext.unbindService(mConnection); + mUnbinding = false; + mBinding = false; + } else { + mUnbinding=false; + } + } + } + + public IBluetoothGatt getBluetoothGatt() { + // sync protection + return mBluetoothGatt; + } + + private void sendBluetoothStateCallback(boolean isUp) { + int n = mStateChangeCallbacks.beginBroadcast(); + if (DBG) Log.d(TAG,"Broadcasting onBluetoothStateChange("+isUp+") to " + n + " receivers."); + for (int i=0; i " + newState); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL, + BLUETOOTH_PERM); + } + } + + /** + * if on is true, wait for state become ON + * if off is true, wait for state become OFF + * if both on and off are false, wait for state not ON + */ + private boolean waitForOnOff(boolean on, boolean off) { + int i = 0; + while (i < 10) { + synchronized(mConnection) { + try { + if (mBluetooth == null) break; + if (on) { + if (mBluetooth.getState() == BluetoothAdapter.STATE_ON) return true; + } else if (off) { + if (mBluetooth.getState() == BluetoothAdapter.STATE_OFF) return true; + } else { + if (mBluetooth.getState() != BluetoothAdapter.STATE_ON) return true; + } + } catch (RemoteException e) { + Log.e(TAG, "getState()", e); + break; + } + } + if (on || off) { + SystemClock.sleep(300); + } else { + SystemClock.sleep(50); + } + i++; + } + Log.e(TAG,"waitForOnOff time out"); + return false; + } + + private void sendDisableMsg() { + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISABLE)); + } + + private void sendEnableMsg(boolean quietMode) { + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE, + quietMode ? 1 : 0, 0)); + } + + private boolean canUnbindBluetoothService() { + synchronized(mConnection) { + //Only unbind with mEnable flag not set + //For race condition: disable and enable back-to-back + //Avoid unbind right after enable due to callback from disable + //Only unbind with Bluetooth at OFF state + //Only unbind without any MESSAGE_BLUETOOTH_STATE_CHANGE message + try { + if (mEnable || (mBluetooth == null)) return false; + if (mHandler.hasMessages(MESSAGE_BLUETOOTH_STATE_CHANGE)) return false; + return (mBluetooth.getState() == BluetoothAdapter.STATE_OFF); + } catch (RemoteException e) { + Log.e(TAG, "getState()", e); + } + } + return false; + } + + private void recoverBluetoothServiceFromError() { + Log.e(TAG,"recoverBluetoothServiceFromError"); + synchronized (mConnection) { + if (mBluetooth != null) { + //Unregister callback object + try { + mBluetooth.unregisterCallback(mBluetoothCallback); + } catch (RemoteException re) { + Log.e(TAG, "Unable to unregister",re); + } + } + } + + SystemClock.sleep(500); + + // disable + handleDisable(); + + waitForOnOff(false, true); + + sendBluetoothServiceDownCallback(); + synchronized (mConnection) { + if (mBluetooth != null) { + mBluetooth = null; + //Unbind + mContext.unbindService(mConnection); + } + } + + mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE); + mState = BluetoothAdapter.STATE_OFF; + + mEnable = false; + + if (mErrorRecoveryRetryCounter++ < MAX_ERROR_RESTART_RETRIES) { + // Send a Bluetooth Restart message to reenable bluetooth + Message restartMsg = mHandler.obtainMessage( + MESSAGE_RESTART_BLUETOOTH_SERVICE); + mHandler.sendMessageDelayed(restartMsg, ERROR_RESTART_TIME_MS); + } else { + // todo: notify user to power down and power up phone to make bluetooth work. + } + } +} diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java new file mode 100644 index 0000000..bce85ce --- /dev/null +++ b/services/core/java/com/android/server/BootReceiver.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.IPackageManager; +import android.os.Build; +import android.os.DropBoxManager; +import android.os.FileObserver; +import android.os.FileUtils; +import android.os.RecoverySystem; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.provider.Downloads; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; + +/** + * Performs a number of miscellaneous, non-system-critical actions + * after the system has finished booting. + */ +public class BootReceiver extends BroadcastReceiver { + private static final String TAG = "BootReceiver"; + + // Maximum size of a logged event (files get truncated if they're longer). + // Give userdebug builds a larger max to capture extra debug, esp. for last_kmsg. + private static final int LOG_SIZE = + SystemProperties.getInt("ro.debuggable", 0) == 1 ? 98304 : 65536; + + private static final File TOMBSTONE_DIR = new File("/data/tombstones"); + + // The pre-froyo package and class of the system updater, which + // ran in the system process. We need to remove its packages here + // in order to clean up after a pre-froyo-to-froyo update. + private static final String OLD_UPDATER_PACKAGE = + "com.google.android.systemupdater"; + private static final String OLD_UPDATER_CLASS = + "com.google.android.systemupdater.SystemUpdateReceiver"; + + // Keep a reference to the observer so the finalizer doesn't disable it. + private static FileObserver sTombstoneObserver = null; + + @Override + public void onReceive(final Context context, Intent intent) { + // Log boot events in the background to avoid blocking the main thread with I/O + new Thread() { + @Override + public void run() { + try { + logBootEvents(context); + } catch (Exception e) { + Slog.e(TAG, "Can't log boot events", e); + } + try { + boolean onlyCore = false; + try { + onlyCore = IPackageManager.Stub.asInterface(ServiceManager.getService( + "package")).isOnlyCoreApps(); + } catch (RemoteException e) { + } + if (!onlyCore) { + removeOldUpdatePackages(context); + } + } catch (Exception e) { + Slog.e(TAG, "Can't remove old update packages", e); + } + + } + }.start(); + } + + private void removeOldUpdatePackages(Context context) { + Downloads.removeAllDownloadsByPackage(context, OLD_UPDATER_PACKAGE, OLD_UPDATER_CLASS); + } + + private void logBootEvents(Context ctx) throws IOException { + final DropBoxManager db = (DropBoxManager) ctx.getSystemService(Context.DROPBOX_SERVICE); + final SharedPreferences prefs = ctx.getSharedPreferences("log_files", Context.MODE_PRIVATE); + final String headers = new StringBuilder(512) + .append("Build: ").append(Build.FINGERPRINT).append("\n") + .append("Hardware: ").append(Build.BOARD).append("\n") + .append("Revision: ") + .append(SystemProperties.get("ro.revision", "")).append("\n") + .append("Bootloader: ").append(Build.BOOTLOADER).append("\n") + .append("Radio: ").append(Build.RADIO).append("\n") + .append("Kernel: ") + .append(FileUtils.readTextFile(new File("/proc/version"), 1024, "...\n")) + .append("\n").toString(); + + String recovery = RecoverySystem.handleAftermath(); + if (recovery != null && db != null) { + db.addText("SYSTEM_RECOVERY_LOG", headers + recovery); + } + + if (SystemProperties.getLong("ro.runtime.firstboot", 0) == 0) { + String now = Long.toString(System.currentTimeMillis()); + SystemProperties.set("ro.runtime.firstboot", now); + if (db != null) db.addText("SYSTEM_BOOT", headers); + + // Negative sizes mean to take the *tail* of the file (see FileUtils.readTextFile()) + addFileToDropBox(db, prefs, headers, "/proc/last_kmsg", + -LOG_SIZE, "SYSTEM_LAST_KMSG"); + addFileToDropBox(db, prefs, headers, "/sys/fs/pstore/console-ramoops", + -LOG_SIZE, "SYSTEM_LAST_KMSG"); + addFileToDropBox(db, prefs, headers, "/cache/recovery/log", + -LOG_SIZE, "SYSTEM_RECOVERY_LOG"); + addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_console", + -LOG_SIZE, "APANIC_CONSOLE"); + addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_threads", + -LOG_SIZE, "APANIC_THREADS"); + addAuditErrorsToDropBox(db, prefs, headers, -LOG_SIZE, "SYSTEM_AUDIT"); + addFsckErrorsToDropBox(db, prefs, headers, -LOG_SIZE, "SYSTEM_FSCK"); + } else { + if (db != null) db.addText("SYSTEM_RESTART", headers); + } + + // Scan existing tombstones (in case any new ones appeared) + File[] tombstoneFiles = TOMBSTONE_DIR.listFiles(); + for (int i = 0; tombstoneFiles != null && i < tombstoneFiles.length; i++) { + addFileToDropBox(db, prefs, headers, tombstoneFiles[i].getPath(), + LOG_SIZE, "SYSTEM_TOMBSTONE"); + } + + // Start watching for new tombstone files; will record them as they occur. + // This gets registered with the singleton file observer thread. + sTombstoneObserver = new FileObserver(TOMBSTONE_DIR.getPath(), FileObserver.CLOSE_WRITE) { + @Override + public void onEvent(int event, String path) { + try { + String filename = new File(TOMBSTONE_DIR, path).getPath(); + addFileToDropBox(db, prefs, headers, filename, LOG_SIZE, "SYSTEM_TOMBSTONE"); + } catch (IOException e) { + Slog.e(TAG, "Can't log tombstone", e); + } + } + }; + + sTombstoneObserver.startWatching(); + } + + private static void addFileToDropBox( + DropBoxManager db, SharedPreferences prefs, + String headers, String filename, int maxSize, String tag) throws IOException { + if (db == null || !db.isTagEnabled(tag)) return; // Logging disabled + + File file = new File(filename); + long fileTime = file.lastModified(); + if (fileTime <= 0) return; // File does not exist + + if (prefs != null) { + long lastTime = prefs.getLong(filename, 0); + if (lastTime == fileTime) return; // Already logged this particular file + // TODO: move all these SharedPreferences Editor commits + // outside this function to the end of logBootEvents + prefs.edit().putLong(filename, fileTime).apply(); + } + + Slog.i(TAG, "Copying " + filename + " to DropBox (" + tag + ")"); + db.addText(tag, headers + FileUtils.readTextFile(file, maxSize, "[[TRUNCATED]]\n")); + } + + private static void addAuditErrorsToDropBox(DropBoxManager db, SharedPreferences prefs, + String headers, int maxSize, String tag) throws IOException { + if (db == null || !db.isTagEnabled(tag)) return; // Logging disabled + Slog.i(TAG, "Copying audit failures to DropBox"); + + File file = new File("/proc/last_kmsg"); + long fileTime = file.lastModified(); + if (fileTime <= 0) { + file = new File("/sys/fs/pstore/console-ramoops"); + fileTime = file.lastModified(); + } + + if (fileTime <= 0) return; // File does not exist + + if (prefs != null) { + long lastTime = prefs.getLong(tag, 0); + if (lastTime == fileTime) return; // Already logged this particular file + // TODO: move all these SharedPreferences Editor commits + // outside this function to the end of logBootEvents + prefs.edit().putLong(tag, fileTime).apply(); + } + + String log = FileUtils.readTextFile(file, maxSize, "[[TRUNCATED]]\n"); + StringBuilder sb = new StringBuilder(); + for (String line : log.split("\n")) { + if (line.contains("audit")) { + sb.append(line + "\n"); + } + } + Slog.i(TAG, "Copied " + sb.toString().length() + " worth of audits to DropBox"); + db.addText(tag, headers + sb.toString()); + } + + private static void addFsckErrorsToDropBox(DropBoxManager db, SharedPreferences prefs, + String headers, int maxSize, String tag) throws IOException { + boolean upload_needed = false; + if (db == null || !db.isTagEnabled(tag)) return; // Logging disabled + Slog.i(TAG, "Checking for fsck errors"); + + File file = new File("/dev/fscklogs/log"); + long fileTime = file.lastModified(); + if (fileTime <= 0) return; // File does not exist + + String log = FileUtils.readTextFile(file, maxSize, "[[TRUNCATED]]\n"); + StringBuilder sb = new StringBuilder(); + for (String line : log.split("\n")) { + if (line.contains("FILE SYSTEM WAS MODIFIED")) { + upload_needed = true; + break; + } + } + + if (upload_needed) { + addFileToDropBox(db, prefs, headers, "/dev/fscklogs/log", maxSize, tag); + } + + // Remove the file so we don't re-upload if the runtime restarts. + file.delete(); + } +} diff --git a/services/core/java/com/android/server/BrickReceiver.java b/services/core/java/com/android/server/BrickReceiver.java new file mode 100644 index 0000000..cff3805 --- /dev/null +++ b/services/core/java/com/android/server/BrickReceiver.java @@ -0,0 +1,31 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.content.Intent; +import android.content.BroadcastReceiver; +import android.os.SystemService; +import android.util.Slog; + +public class BrickReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Slog.w("BrickReceiver", "!!! BRICKING DEVICE !!!"); + SystemService.start("brick"); + } +} diff --git a/services/core/java/com/android/server/CertBlacklister.java b/services/core/java/com/android/server/CertBlacklister.java new file mode 100644 index 0000000..8b167d7 --- /dev/null +++ b/services/core/java/com/android/server/CertBlacklister.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.os.Binder; +import android.os.FileUtils; +import android.provider.Settings; +import android.util.Slog; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import libcore.io.IoUtils; + +/** + *

      CertBlacklister provides a simple mechanism for updating the platform blacklists for SSL + * certificate public keys and serial numbers. + */ +public class CertBlacklister extends Binder { + + private static final String TAG = "CertBlacklister"; + + private static final String BLACKLIST_ROOT = System.getenv("ANDROID_DATA") + "/misc/keychain/"; + + public static final String PUBKEY_PATH = BLACKLIST_ROOT + "pubkey_blacklist.txt"; + public static final String SERIAL_PATH = BLACKLIST_ROOT + "serial_blacklist.txt"; + + public static final String PUBKEY_BLACKLIST_KEY = "pubkey_blacklist"; + public static final String SERIAL_BLACKLIST_KEY = "serial_blacklist"; + + private static class BlacklistObserver extends ContentObserver { + + private final String mKey; + private final String mName; + private final String mPath; + private final File mTmpDir; + private final ContentResolver mContentResolver; + + public BlacklistObserver(String key, String name, String path, ContentResolver cr) { + super(null); + mKey = key; + mName = name; + mPath = path; + mTmpDir = new File(mPath).getParentFile(); + mContentResolver = cr; + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + writeBlacklist(); + } + + public String getValue() { + return Settings.Secure.getString(mContentResolver, mKey); + } + + private void writeBlacklist() { + new Thread("BlacklistUpdater") { + public void run() { + synchronized(mTmpDir) { + String blacklist = getValue(); + if (blacklist != null) { + Slog.i(TAG, "Certificate blacklist changed, updating..."); + FileOutputStream out = null; + try { + // create a temporary file + File tmp = File.createTempFile("journal", "", mTmpDir); + // mark it -rw-r--r-- + tmp.setReadable(true, false); + // write to it + out = new FileOutputStream(tmp); + out.write(blacklist.getBytes()); + // sync to disk + FileUtils.sync(out); + // atomic rename + tmp.renameTo(new File(mPath)); + Slog.i(TAG, "Certificate blacklist updated"); + } catch (IOException e) { + Slog.e(TAG, "Failed to write blacklist", e); + } finally { + IoUtils.closeQuietly(out); + } + } + } + } + }.start(); + } + } + + public CertBlacklister(Context context) { + registerObservers(context.getContentResolver()); + } + + private BlacklistObserver buildPubkeyObserver(ContentResolver cr) { + return new BlacklistObserver(PUBKEY_BLACKLIST_KEY, + "pubkey", + PUBKEY_PATH, + cr); + } + + private BlacklistObserver buildSerialObserver(ContentResolver cr) { + return new BlacklistObserver(SERIAL_BLACKLIST_KEY, + "serial", + SERIAL_PATH, + cr); + } + + private void registerObservers(ContentResolver cr) { + // set up the public key blacklist observer + cr.registerContentObserver( + Settings.Secure.getUriFor(PUBKEY_BLACKLIST_KEY), + true, + buildPubkeyObserver(cr) + ); + + // set up the serial number blacklist observer + cr.registerContentObserver( + Settings.Secure.getUriFor(SERIAL_BLACKLIST_KEY), + true, + buildSerialObserver(cr) + ); + } +} diff --git a/services/core/java/com/android/server/CommonTimeManagementService.java b/services/core/java/com/android/server/CommonTimeManagementService.java new file mode 100644 index 0000000..710fb9d --- /dev/null +++ b/services/core/java/com/android/server/CommonTimeManagementService.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.InetAddress; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.net.INetworkManagementEventObserver; +import android.net.InterfaceConfiguration; +import android.net.NetworkInfo; +import android.os.Binder; +import android.os.CommonTimeConfig; +import android.os.Handler; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.server.net.BaseNetworkObserver; + +/** + * @hide + *

      CommonTimeManagementService manages the configuration of the native Common Time service, + * reconfiguring the native service as appropriate in response to changes in network configuration. + */ +class CommonTimeManagementService extends Binder { + /* + * Constants and globals. + */ + private static final String TAG = CommonTimeManagementService.class.getSimpleName(); + private static final int NATIVE_SERVICE_RECONNECT_TIMEOUT = 5000; + private static final String AUTO_DISABLE_PROP = "ro.common_time.auto_disable"; + private static final String ALLOW_WIFI_PROP = "ro.common_time.allow_wifi"; + private static final String SERVER_PRIO_PROP = "ro.common_time.server_prio"; + private static final String NO_INTERFACE_TIMEOUT_PROP = "ro.common_time.no_iface_timeout"; + private static final boolean AUTO_DISABLE; + private static final boolean ALLOW_WIFI; + private static final byte BASE_SERVER_PRIO; + private static final int NO_INTERFACE_TIMEOUT; + private static final InterfaceScoreRule[] IFACE_SCORE_RULES; + + static { + int tmp; + AUTO_DISABLE = (0 != SystemProperties.getInt(AUTO_DISABLE_PROP, 1)); + ALLOW_WIFI = (0 != SystemProperties.getInt(ALLOW_WIFI_PROP, 0)); + tmp = SystemProperties.getInt(SERVER_PRIO_PROP, 1); + NO_INTERFACE_TIMEOUT = SystemProperties.getInt(NO_INTERFACE_TIMEOUT_PROP, 60000); + + if (tmp < 1) + BASE_SERVER_PRIO = 1; + else + if (tmp > 30) + BASE_SERVER_PRIO = 30; + else + BASE_SERVER_PRIO = (byte)tmp; + + if (ALLOW_WIFI) { + IFACE_SCORE_RULES = new InterfaceScoreRule[] { + new InterfaceScoreRule("wlan", (byte)1), + new InterfaceScoreRule("eth", (byte)2), + }; + } else { + IFACE_SCORE_RULES = new InterfaceScoreRule[] { + new InterfaceScoreRule("eth", (byte)2), + }; + } + }; + + /* + * Internal state + */ + private final Context mContext; + private INetworkManagementService mNetMgr; + private CommonTimeConfig mCTConfig; + private String mCurIface; + private Handler mReconnectHandler = new Handler(); + private Handler mNoInterfaceHandler = new Handler(); + private Object mLock = new Object(); + private boolean mDetectedAtStartup = false; + private byte mEffectivePrio = BASE_SERVER_PRIO; + + /* + * Callback handler implementations. + */ + private INetworkManagementEventObserver mIfaceObserver = new BaseNetworkObserver() { + public void interfaceStatusChanged(String iface, boolean up) { + reevaluateServiceState(); + } + public void interfaceLinkStateChanged(String iface, boolean up) { + reevaluateServiceState(); + } + public void interfaceAdded(String iface) { + reevaluateServiceState(); + } + public void interfaceRemoved(String iface) { + reevaluateServiceState(); + } + }; + + private BroadcastReceiver mConnectivityMangerObserver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reevaluateServiceState(); + } + }; + + private CommonTimeConfig.OnServerDiedListener mCTServerDiedListener = + new CommonTimeConfig.OnServerDiedListener() { + public void onServerDied() { + scheduleTimeConfigReconnect(); + } + }; + + private Runnable mReconnectRunnable = new Runnable() { + public void run() { connectToTimeConfig(); } + }; + + private Runnable mNoInterfaceRunnable = new Runnable() { + public void run() { handleNoInterfaceTimeout(); } + }; + + /* + * Public interface (constructor, systemReady and dump) + */ + public CommonTimeManagementService(Context context) { + mContext = context; + } + + void systemRunning() { + if (ServiceManager.checkService(CommonTimeConfig.SERVICE_NAME) == null) { + Log.i(TAG, "No common time service detected on this platform. " + + "Common time services will be unavailable."); + return; + } + + mDetectedAtStartup = true; + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + mNetMgr = INetworkManagementService.Stub.asInterface(b); + + // Network manager is running along-side us, so we should never receiver a remote exception + // while trying to register this observer. + try { + mNetMgr.registerObserver(mIfaceObserver); + } + catch (RemoteException e) { } + + // Register with the connectivity manager for connectivity changed intents. + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + mContext.registerReceiver(mConnectivityMangerObserver, filter); + + // Connect to the common time config service and apply the initial configuration. + connectToTimeConfig(); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println(String.format( + "Permission Denial: can't dump CommonTimeManagement service from from " + + "pid=%d, uid=%d", Binder.getCallingPid(), Binder.getCallingUid())); + return; + } + + if (!mDetectedAtStartup) { + pw.println("Native Common Time service was not detected at startup. " + + "Service is unavailable"); + return; + } + + synchronized (mLock) { + pw.println("Current Common Time Management Service Config:"); + pw.println(String.format(" Native service : %s", + (null == mCTConfig) ? "reconnecting" + : "alive")); + pw.println(String.format(" Bound interface : %s", + (null == mCurIface ? "unbound" : mCurIface))); + pw.println(String.format(" Allow WiFi : %s", ALLOW_WIFI ? "yes" : "no")); + pw.println(String.format(" Allow Auto Disable : %s", AUTO_DISABLE ? "yes" : "no")); + pw.println(String.format(" Server Priority : %d", mEffectivePrio)); + pw.println(String.format(" No iface timeout : %d", NO_INTERFACE_TIMEOUT)); + } + } + + /* + * Inner helper classes + */ + private static class InterfaceScoreRule { + public final String mPrefix; + public final byte mScore; + public InterfaceScoreRule(String prefix, byte score) { + mPrefix = prefix; + mScore = score; + } + }; + + /* + * Internal implementation + */ + private void cleanupTimeConfig() { + mReconnectHandler.removeCallbacks(mReconnectRunnable); + mNoInterfaceHandler.removeCallbacks(mNoInterfaceRunnable); + if (null != mCTConfig) { + mCTConfig.release(); + mCTConfig = null; + } + } + + private void connectToTimeConfig() { + // Get access to the common time service configuration interface. If we catch a remote + // exception in the process (service crashed or no running for w/e reason), schedule an + // attempt to reconnect in the future. + cleanupTimeConfig(); + try { + synchronized (mLock) { + mCTConfig = new CommonTimeConfig(); + mCTConfig.setServerDiedListener(mCTServerDiedListener); + mCurIface = mCTConfig.getInterfaceBinding(); + mCTConfig.setAutoDisable(AUTO_DISABLE); + mCTConfig.setMasterElectionPriority(mEffectivePrio); + } + + if (NO_INTERFACE_TIMEOUT >= 0) + mNoInterfaceHandler.postDelayed(mNoInterfaceRunnable, NO_INTERFACE_TIMEOUT); + + reevaluateServiceState(); + } + catch (RemoteException e) { + scheduleTimeConfigReconnect(); + } + } + + private void scheduleTimeConfigReconnect() { + cleanupTimeConfig(); + Log.w(TAG, String.format("Native service died, will reconnect in %d mSec", + NATIVE_SERVICE_RECONNECT_TIMEOUT)); + mReconnectHandler.postDelayed(mReconnectRunnable, + NATIVE_SERVICE_RECONNECT_TIMEOUT); + } + + private void handleNoInterfaceTimeout() { + if (null != mCTConfig) { + Log.i(TAG, "Timeout waiting for interface to come up. " + + "Forcing networkless master mode."); + if (CommonTimeConfig.ERROR_DEAD_OBJECT == mCTConfig.forceNetworklessMasterMode()) + scheduleTimeConfigReconnect(); + } + } + + private void reevaluateServiceState() { + String bindIface = null; + byte bestScore = -1; + try { + // Check to see if this interface is suitable to use for time synchronization. + // + // TODO : This selection algorithm needs to be enhanced for use with mobile devices. In + // particular, the choice of whether to a wireless interface or not should not be an all + // or nothing thing controlled by properties. It would probably be better if the + // platform had some concept of public wireless networks vs. home or friendly wireless + // networks (something a user would configure in settings or when a new interface is + // added). Then this algorithm could pick only wireless interfaces which were flagged + // as friendly, and be dormant when on public wireless networks. + // + // Another issue which needs to be dealt with is the use of driver supplied interface + // name to determine the network type. The fact that the wireless interface on a device + // is named "wlan0" is just a matter of convention; its not a 100% rule. For example, + // there are devices out there where the wireless is name "tiwlan0", not "wlan0". The + // internal network management interfaces in Android have all of the information needed + // to make a proper classification, there is just no way (currently) to fetch an + // interface's type (available from the ConnectionManager) as well as its address + // (available from either the java.net interfaces or from the NetworkManagment service). + // Both can enumerate interfaces, but that is no way to correlate their results (no + // common shared key; although using the interface name in the connection manager would + // be a good start). Until this gets resolved, we resort to substring searching for + // tags like wlan and eth. + // + String ifaceList[] = mNetMgr.listInterfaces(); + if (null != ifaceList) { + for (String iface : ifaceList) { + + byte thisScore = -1; + for (InterfaceScoreRule r : IFACE_SCORE_RULES) { + if (iface.contains(r.mPrefix)) { + thisScore = r.mScore; + break; + } + } + + if (thisScore <= bestScore) + continue; + + InterfaceConfiguration config = mNetMgr.getInterfaceConfig(iface); + if (null == config) + continue; + + if (config.isActive()) { + bindIface = iface; + bestScore = thisScore; + } + } + } + } + catch (RemoteException e) { + // Bad news; we should not be getting remote exceptions from the connectivity manager + // since it is running in SystemServer along side of us. It probably does not matter + // what we do here, but go ahead and unbind the common time service in this case, just + // so we have some defined behavior. + bindIface = null; + } + + boolean doRebind = true; + synchronized (mLock) { + if ((null != bindIface) && (null == mCurIface)) { + Log.e(TAG, String.format("Binding common time service to %s.", bindIface)); + mCurIface = bindIface; + } else + if ((null == bindIface) && (null != mCurIface)) { + Log.e(TAG, "Unbinding common time service."); + mCurIface = null; + } else + if ((null != bindIface) && (null != mCurIface) && !bindIface.equals(mCurIface)) { + Log.e(TAG, String.format("Switching common time service binding from %s to %s.", + mCurIface, bindIface)); + mCurIface = bindIface; + } else { + doRebind = false; + } + } + + if (doRebind && (null != mCTConfig)) { + byte newPrio = (bestScore > 0) + ? (byte)(bestScore * BASE_SERVER_PRIO) + : BASE_SERVER_PRIO; + if (newPrio != mEffectivePrio) { + mEffectivePrio = newPrio; + mCTConfig.setMasterElectionPriority(mEffectivePrio); + } + + int res = mCTConfig.setNetworkBinding(mCurIface); + if (res != CommonTimeConfig.SUCCESS) + scheduleTimeConfigReconnect(); + + else if (NO_INTERFACE_TIMEOUT >= 0) { + mNoInterfaceHandler.removeCallbacks(mNoInterfaceRunnable); + if (null == mCurIface) + mNoInterfaceHandler.postDelayed(mNoInterfaceRunnable, NO_INTERFACE_TIMEOUT); + } + } + } +} diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java new file mode 100644 index 0000000..d42ae3a --- /dev/null +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -0,0 +1,4930 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.Manifest.permission.MANAGE_NETWORK_POLICY; +import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE; +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_DUMMY; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIMAX; +import static android.net.ConnectivityManager.getNetworkTypeName; +import static android.net.ConnectivityManager.isNetworkTypeValid; +import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL; +import static android.net.NetworkPolicyManager.RULE_REJECT_METERED; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothTetheringDataTracker; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.CaptivePortalTracker; +import android.net.ConnectivityManager; +import android.net.DummyDataStateTracker; +import android.net.EthernetDataTracker; +import android.net.IConnectivityManager; +import android.net.INetworkManagementEventObserver; +import android.net.INetworkPolicyListener; +import android.net.INetworkPolicyManager; +import android.net.INetworkStatsService; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.LinkProperties.CompareResult; +import android.net.LinkQualityInfo; +import android.net.MobileDataStateTracker; +import android.net.NetworkConfig; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkQuotaInfo; +import android.net.NetworkState; +import android.net.NetworkStateTracker; +import android.net.NetworkUtils; +import android.net.Proxy; +import android.net.ProxyProperties; +import android.net.RouteInfo; +import android.net.SamplingDataTracker; +import android.net.Uri; +import android.net.wifi.WifiStateTracker; +import android.net.wimax.WimaxManagerConstants; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.Settings; +import android.security.Credentials; +import android.security.KeyStore; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.Xml; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.internal.telephony.DctConstants; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.XmlUtils; +import com.android.server.am.BatteryStatsService; +import com.android.server.connectivity.DataConnectionStats; +import com.android.server.connectivity.Nat464Xlat; +import com.android.server.connectivity.PacManager; +import com.android.server.connectivity.Tethering; +import com.android.server.connectivity.Vpn; +import com.android.server.net.BaseNetworkObserver; +import com.android.server.net.LockdownVpnTracker; +import com.google.android.collect.Lists; +import com.google.android.collect.Sets; + +import dalvik.system.DexClassLoader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; + +/** + * @hide + */ +public class ConnectivityService extends IConnectivityManager.Stub { + private static final String TAG = "ConnectivityService"; + + private static final boolean DBG = true; + private static final boolean VDBG = false; + + private static final boolean LOGD_RULES = false; + + // TODO: create better separation between radio types and network types + + // how long to wait before switching back to a radio's default network + private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000; + // system property that can override the above value + private static final String NETWORK_RESTORE_DELAY_PROP_NAME = + "android.telephony.apn-restore"; + + // Default value if FAIL_FAST_TIME_MS is not set + private static final int DEFAULT_FAIL_FAST_TIME_MS = 1 * 60 * 1000; + // system property that can override DEFAULT_FAIL_FAST_TIME_MS + private static final String FAIL_FAST_TIME_MS = + "persist.radio.fail_fast_time_ms"; + + private static final String ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED = + "android.net.ConnectivityService.action.PKT_CNT_SAMPLE_INTERVAL_ELAPSED"; + + private static final int SAMPLE_INTERVAL_ELAPSED_REQUEST_CODE = 0; + + private PendingIntent mSampleIntervalElapsedIntent; + + // Set network sampling interval at 12 minutes, this way, even if the timers get + // aggregated, it will fire at around 15 minutes, which should allow us to + // aggregate this timer with other timers (specially the socket keep alive timers) + private static final int DEFAULT_SAMPLING_INTERVAL_IN_SECONDS = (VDBG ? 30 : 12 * 60); + + // start network sampling a minute after booting ... + private static final int DEFAULT_START_SAMPLING_INTERVAL_IN_SECONDS = (VDBG ? 30 : 60); + + AlarmManager mAlarmManager; + + // used in recursive route setting to add gateways for the host for which + // a host route was requested. + private static final int MAX_HOSTROUTE_CYCLE_COUNT = 10; + + private Tethering mTethering; + + private KeyStore mKeyStore; + + @GuardedBy("mVpns") + private final SparseArray mVpns = new SparseArray(); + private VpnCallback mVpnCallback = new VpnCallback(); + + private boolean mLockdownEnabled; + private LockdownVpnTracker mLockdownTracker; + + private Nat464Xlat mClat; + + /** Lock around {@link #mUidRules} and {@link #mMeteredIfaces}. */ + private Object mRulesLock = new Object(); + /** Currently active network rules by UID. */ + private SparseIntArray mUidRules = new SparseIntArray(); + /** Set of ifaces that are costly. */ + private HashSet mMeteredIfaces = Sets.newHashSet(); + + /** + * Sometimes we want to refer to the individual network state + * trackers separately, and sometimes we just want to treat them + * abstractly. + */ + private NetworkStateTracker mNetTrackers[]; + + /* Handles captive portal check on a network */ + private CaptivePortalTracker mCaptivePortalTracker; + + /** + * The link properties that define the current links + */ + private LinkProperties mCurrentLinkProperties[]; + + /** + * A per Net list of the PID's that requested access to the net + * used both as a refcount and for per-PID DNS selection + */ + private List mNetRequestersPids[]; + + // priority order of the nettrackers + // (excluding dynamically set mNetworkPreference) + // TODO - move mNetworkTypePreference into this + private int[] mPriorityList; + + private Context mContext; + private int mNetworkPreference; + private int mActiveDefaultNetwork = -1; + // 0 is full bad, 100 is full good + private int mDefaultInetCondition = 0; + private int mDefaultInetConditionPublished = 0; + private boolean mInetConditionChangeInFlight = false; + private int mDefaultConnectionSequence = 0; + + private Object mDnsLock = new Object(); + private int mNumDnsEntries; + + private boolean mTestMode; + private static ConnectivityService sServiceInstance; + + private INetworkManagementService mNetd; + private INetworkPolicyManager mPolicyManager; + + private static final int ENABLED = 1; + private static final int DISABLED = 0; + + private static final boolean ADD = true; + private static final boolean REMOVE = false; + + private static final boolean TO_DEFAULT_TABLE = true; + private static final boolean TO_SECONDARY_TABLE = false; + + private static final boolean EXEMPT = true; + private static final boolean UNEXEMPT = false; + + /** + * used internally as a delayed event to make us switch back to the + * default network + */ + private static final int EVENT_RESTORE_DEFAULT_NETWORK = 1; + + /** + * used internally to change our mobile data enabled flag + */ + private static final int EVENT_CHANGE_MOBILE_DATA_ENABLED = 2; + + /** + * used internally to change our network preference setting + * arg1 = networkType to prefer + */ + private static final int EVENT_SET_NETWORK_PREFERENCE = 3; + + /** + * used internally to synchronize inet condition reports + * arg1 = networkType + * arg2 = condition (0 bad, 100 good) + */ + private static final int EVENT_INET_CONDITION_CHANGE = 4; + + /** + * used internally to mark the end of inet condition hold periods + * arg1 = networkType + */ + private static final int EVENT_INET_CONDITION_HOLD_END = 5; + + /** + * used internally to set enable/disable cellular data + * arg1 = ENBALED or DISABLED + */ + private static final int EVENT_SET_MOBILE_DATA = 7; + + /** + * used internally to clear a wakelock when transitioning + * from one net to another + */ + private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8; + + /** + * used internally to reload global proxy settings + */ + private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9; + + /** + * used internally to set external dependency met/unmet + * arg1 = ENABLED (met) or DISABLED (unmet) + * arg2 = NetworkType + */ + private static final int EVENT_SET_DEPENDENCY_MET = 10; + + /** + * used internally to send a sticky broadcast delayed. + */ + private static final int EVENT_SEND_STICKY_BROADCAST_INTENT = 11; + + /** + * Used internally to + * {@link NetworkStateTracker#setPolicyDataEnable(boolean)}. + */ + private static final int EVENT_SET_POLICY_DATA_ENABLE = 12; + + private static final int EVENT_VPN_STATE_CHANGED = 13; + + /** + * Used internally to disable fail fast of mobile data + */ + private static final int EVENT_ENABLE_FAIL_FAST_MOBILE_DATA = 14; + + /** + * user internally to indicate that data sampling interval is up + */ + private static final int EVENT_SAMPLE_INTERVAL_ELAPSED = 15; + + /** + * PAC manager has received new port. + */ + private static final int EVENT_PROXY_HAS_CHANGED = 16; + + /** Handler used for internal events. */ + private InternalHandler mHandler; + /** Handler used for incoming {@link NetworkStateTracker} events. */ + private NetworkStateTrackerHandler mTrackerHandler; + + // list of DeathRecipients used to make sure features are turned off when + // a process dies + private List mFeatureUsers; + + private boolean mSystemReady; + private Intent mInitialBroadcast; + + private PowerManager.WakeLock mNetTransitionWakeLock; + private String mNetTransitionWakeLockCausedBy = ""; + private int mNetTransitionWakeLockSerialNumber; + private int mNetTransitionWakeLockTimeout; + + private InetAddress mDefaultDns; + + // Lock for protecting access to mAddedRoutes and mExemptAddresses + private final Object mRoutesLock = new Object(); + + // this collection is used to refcount the added routes - if there are none left + // it's time to remove the route from the route table + @GuardedBy("mRoutesLock") + private Collection mAddedRoutes = new ArrayList(); + + // this collection corresponds to the entries of mAddedRoutes that have routing exemptions + // used to handle cleanup of exempt rules + @GuardedBy("mRoutesLock") + private Collection mExemptAddresses = new ArrayList(); + + // used in DBG mode to track inet condition reports + private static final int INET_CONDITION_LOG_MAX_SIZE = 15; + private ArrayList mInetLog; + + // track the current default http proxy - tell the world if we get a new one (real change) + private ProxyProperties mDefaultProxy = null; + private Object mProxyLock = new Object(); + private boolean mDefaultProxyDisabled = false; + + // track the global proxy. + private ProxyProperties mGlobalProxy = null; + + private PacManager mPacManager = null; + + private SettingsObserver mSettingsObserver; + + NetworkConfig[] mNetConfigs; + int mNetworksDefined; + + private static class RadioAttributes { + public int mSimultaneity; + public int mType; + public RadioAttributes(String init) { + String fragments[] = init.split(","); + mType = Integer.parseInt(fragments[0]); + mSimultaneity = Integer.parseInt(fragments[1]); + } + } + RadioAttributes[] mRadioAttributes; + + // the set of network types that can only be enabled by system/sig apps + List mProtectedNetworks; + + private DataConnectionStats mDataConnectionStats; + + private AtomicInteger mEnableFailFastMobileDataTag = new AtomicInteger(0); + + TelephonyManager mTelephonyManager; + + public ConnectivityService(Context context, INetworkManagementService netd, + INetworkStatsService statsService, INetworkPolicyManager policyManager) { + // Currently, omitting a NetworkFactory will create one internally + // TODO: create here when we have cleaner WiMAX support + this(context, netd, statsService, policyManager, null); + } + + public ConnectivityService(Context context, INetworkManagementService netManager, + INetworkStatsService statsService, INetworkPolicyManager policyManager, + NetworkFactory netFactory) { + if (DBG) log("ConnectivityService starting up"); + + HandlerThread handlerThread = new HandlerThread("ConnectivityServiceThread"); + handlerThread.start(); + mHandler = new InternalHandler(handlerThread.getLooper()); + mTrackerHandler = new NetworkStateTrackerHandler(handlerThread.getLooper()); + + if (netFactory == null) { + netFactory = new DefaultNetworkFactory(context, mTrackerHandler); + } + + // setup our unique device name + if (TextUtils.isEmpty(SystemProperties.get("net.hostname"))) { + String id = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.ANDROID_ID); + if (id != null && id.length() > 0) { + String name = new String("android-").concat(id); + SystemProperties.set("net.hostname", name); + } + } + + // read our default dns server ip + String dns = Settings.Global.getString(context.getContentResolver(), + Settings.Global.DEFAULT_DNS_SERVER); + if (dns == null || dns.length() == 0) { + dns = context.getResources().getString( + com.android.internal.R.string.config_default_dns_server); + } + try { + mDefaultDns = NetworkUtils.numericToInetAddress(dns); + } catch (IllegalArgumentException e) { + loge("Error setting defaultDns using " + dns); + } + + mContext = checkNotNull(context, "missing Context"); + mNetd = checkNotNull(netManager, "missing INetworkManagementService"); + mPolicyManager = checkNotNull(policyManager, "missing INetworkPolicyManager"); + mKeyStore = KeyStore.getInstance(); + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + + try { + mPolicyManager.registerListener(mPolicyListener); + } catch (RemoteException e) { + // ouch, no rules updates means some processes may never get network + loge("unable to register INetworkPolicyListener" + e.toString()); + } + + final PowerManager powerManager = (PowerManager) context.getSystemService( + Context.POWER_SERVICE); + mNetTransitionWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mNetTransitionWakeLockTimeout = mContext.getResources().getInteger( + com.android.internal.R.integer.config_networkTransitionTimeout); + + mNetTrackers = new NetworkStateTracker[ + ConnectivityManager.MAX_NETWORK_TYPE+1]; + mCurrentLinkProperties = new LinkProperties[ConnectivityManager.MAX_NETWORK_TYPE+1]; + + mRadioAttributes = new RadioAttributes[ConnectivityManager.MAX_RADIO_TYPE+1]; + mNetConfigs = new NetworkConfig[ConnectivityManager.MAX_NETWORK_TYPE+1]; + + // Load device network attributes from resources + String[] raStrings = context.getResources().getStringArray( + com.android.internal.R.array.radioAttributes); + for (String raString : raStrings) { + RadioAttributes r = new RadioAttributes(raString); + if (VDBG) log("raString=" + raString + " r=" + r); + if (r.mType > ConnectivityManager.MAX_RADIO_TYPE) { + loge("Error in radioAttributes - ignoring attempt to define type " + r.mType); + continue; + } + if (mRadioAttributes[r.mType] != null) { + loge("Error in radioAttributes - ignoring attempt to redefine type " + + r.mType); + continue; + } + mRadioAttributes[r.mType] = r; + } + + // TODO: What is the "correct" way to do determine if this is a wifi only device? + boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false); + log("wifiOnly=" + wifiOnly); + String[] naStrings = context.getResources().getStringArray( + com.android.internal.R.array.networkAttributes); + for (String naString : naStrings) { + try { + NetworkConfig n = new NetworkConfig(naString); + if (VDBG) log("naString=" + naString + " config=" + n); + if (n.type > ConnectivityManager.MAX_NETWORK_TYPE) { + loge("Error in networkAttributes - ignoring attempt to define type " + + n.type); + continue; + } + if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) { + log("networkAttributes - ignoring mobile as this dev is wifiOnly " + + n.type); + continue; + } + if (mNetConfigs[n.type] != null) { + loge("Error in networkAttributes - ignoring attempt to redefine type " + + n.type); + continue; + } + if (mRadioAttributes[n.radio] == null) { + loge("Error in networkAttributes - ignoring attempt to use undefined " + + "radio " + n.radio + " in network type " + n.type); + continue; + } + mNetConfigs[n.type] = n; + mNetworksDefined++; + } catch(Exception e) { + // ignore it - leave the entry null + } + } + if (VDBG) log("mNetworksDefined=" + mNetworksDefined); + + mProtectedNetworks = new ArrayList(); + int[] protectedNetworks = context.getResources().getIntArray( + com.android.internal.R.array.config_protectedNetworks); + for (int p : protectedNetworks) { + if ((mNetConfigs[p] != null) && (mProtectedNetworks.contains(p) == false)) { + mProtectedNetworks.add(p); + } else { + if (DBG) loge("Ignoring protectedNetwork " + p); + } + } + + // high priority first + mPriorityList = new int[mNetworksDefined]; + { + int insertionPoint = mNetworksDefined-1; + int currentLowest = 0; + int nextLowest = 0; + while (insertionPoint > -1) { + for (NetworkConfig na : mNetConfigs) { + if (na == null) continue; + if (na.priority < currentLowest) continue; + if (na.priority > currentLowest) { + if (na.priority < nextLowest || nextLowest == 0) { + nextLowest = na.priority; + } + continue; + } + mPriorityList[insertionPoint--] = na.type; + } + currentLowest = nextLowest; + nextLowest = 0; + } + } + + // Update mNetworkPreference according to user mannually first then overlay config.xml + mNetworkPreference = getPersistedNetworkPreference(); + if (mNetworkPreference == -1) { + for (int n : mPriorityList) { + if (mNetConfigs[n].isDefault() && ConnectivityManager.isNetworkTypeValid(n)) { + mNetworkPreference = n; + break; + } + } + if (mNetworkPreference == -1) { + throw new IllegalStateException( + "You should set at least one default Network in config.xml!"); + } + } + + mNetRequestersPids = + (List [])new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE+1]; + for (int i : mPriorityList) { + mNetRequestersPids[i] = new ArrayList(); + } + + mFeatureUsers = new ArrayList(); + + mTestMode = SystemProperties.get("cm.test.mode").equals("true") + && SystemProperties.get("ro.build.type").equals("eng"); + + // Create and start trackers for hard-coded networks + for (int targetNetworkType : mPriorityList) { + final NetworkConfig config = mNetConfigs[targetNetworkType]; + final NetworkStateTracker tracker; + try { + tracker = netFactory.createTracker(targetNetworkType, config); + mNetTrackers[targetNetworkType] = tracker; + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Problem creating " + getNetworkTypeName(targetNetworkType) + + " tracker: " + e); + continue; + } + + tracker.startMonitoring(context, mTrackerHandler); + if (config.isDefault()) { + tracker.reconnect(); + } + } + + mTethering = new Tethering(mContext, mNetd, statsService, this, mHandler.getLooper()); + + //set up the listener for user state for creating user VPNs + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_STARTING); + intentFilter.addAction(Intent.ACTION_USER_STOPPING); + mContext.registerReceiverAsUser( + mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null); + mClat = new Nat464Xlat(mContext, mNetd, this, mTrackerHandler); + + try { + mNetd.registerObserver(mTethering); + mNetd.registerObserver(mDataActivityObserver); + mNetd.registerObserver(mClat); + } catch (RemoteException e) { + loge("Error registering observer :" + e); + } + + if (DBG) { + mInetLog = new ArrayList(); + } + + mSettingsObserver = new SettingsObserver(mHandler, EVENT_APPLY_GLOBAL_HTTP_PROXY); + mSettingsObserver.observe(mContext); + + mDataConnectionStats = new DataConnectionStats(mContext); + mDataConnectionStats.startMonitoring(); + + // start network sampling .. + Intent intent = new Intent(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED, null); + mSampleIntervalElapsedIntent = PendingIntent.getBroadcast(mContext, + SAMPLE_INTERVAL_ELAPSED_REQUEST_CODE, intent, 0); + + mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); + setAlarm(DEFAULT_START_SAMPLING_INTERVAL_IN_SECONDS * 1000, mSampleIntervalElapsedIntent); + + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED); + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED)) { + mHandler.sendMessage(mHandler.obtainMessage + (EVENT_SAMPLE_INTERVAL_ELAPSED)); + } + } + }, + new IntentFilter(filter)); + + mPacManager = new PacManager(mContext, mHandler, EVENT_PROXY_HAS_CHANGED); + + filter = new IntentFilter(); + filter.addAction(CONNECTED_TO_PROVISIONING_NETWORK_ACTION); + mContext.registerReceiver(mProvisioningReceiver, filter); + } + + /** + * Factory that creates {@link NetworkStateTracker} instances using given + * {@link NetworkConfig}. + */ + public interface NetworkFactory { + public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config); + } + + private static class DefaultNetworkFactory implements NetworkFactory { + private final Context mContext; + private final Handler mTrackerHandler; + + public DefaultNetworkFactory(Context context, Handler trackerHandler) { + mContext = context; + mTrackerHandler = trackerHandler; + } + + @Override + public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config) { + switch (config.radio) { + case TYPE_WIFI: + return new WifiStateTracker(targetNetworkType, config.name); + case TYPE_MOBILE: + return new MobileDataStateTracker(targetNetworkType, config.name); + case TYPE_DUMMY: + return new DummyDataStateTracker(targetNetworkType, config.name); + case TYPE_BLUETOOTH: + return BluetoothTetheringDataTracker.getInstance(); + case TYPE_WIMAX: + return makeWimaxStateTracker(mContext, mTrackerHandler); + case TYPE_ETHERNET: + return EthernetDataTracker.getInstance(); + default: + throw new IllegalArgumentException( + "Trying to create a NetworkStateTracker for an unknown radio type: " + + config.radio); + } + } + } + + /** + * Loads external WiMAX library and registers as system service, returning a + * {@link NetworkStateTracker} for WiMAX. Caller is still responsible for + * invoking {@link NetworkStateTracker#startMonitoring(Context, Handler)}. + */ + private static NetworkStateTracker makeWimaxStateTracker( + Context context, Handler trackerHandler) { + // Initialize Wimax + DexClassLoader wimaxClassLoader; + Class wimaxStateTrackerClass = null; + Class wimaxServiceClass = null; + Class wimaxManagerClass; + String wimaxJarLocation; + String wimaxLibLocation; + String wimaxManagerClassName; + String wimaxServiceClassName; + String wimaxStateTrackerClassName; + + NetworkStateTracker wimaxStateTracker = null; + + boolean isWimaxEnabled = context.getResources().getBoolean( + com.android.internal.R.bool.config_wimaxEnabled); + + if (isWimaxEnabled) { + try { + wimaxJarLocation = context.getResources().getString( + com.android.internal.R.string.config_wimaxServiceJarLocation); + wimaxLibLocation = context.getResources().getString( + com.android.internal.R.string.config_wimaxNativeLibLocation); + wimaxManagerClassName = context.getResources().getString( + com.android.internal.R.string.config_wimaxManagerClassname); + wimaxServiceClassName = context.getResources().getString( + com.android.internal.R.string.config_wimaxServiceClassname); + wimaxStateTrackerClassName = context.getResources().getString( + com.android.internal.R.string.config_wimaxStateTrackerClassname); + + if (DBG) log("wimaxJarLocation: " + wimaxJarLocation); + wimaxClassLoader = new DexClassLoader(wimaxJarLocation, + new ContextWrapper(context).getCacheDir().getAbsolutePath(), + wimaxLibLocation, ClassLoader.getSystemClassLoader()); + + try { + wimaxManagerClass = wimaxClassLoader.loadClass(wimaxManagerClassName); + wimaxStateTrackerClass = wimaxClassLoader.loadClass(wimaxStateTrackerClassName); + wimaxServiceClass = wimaxClassLoader.loadClass(wimaxServiceClassName); + } catch (ClassNotFoundException ex) { + loge("Exception finding Wimax classes: " + ex.toString()); + return null; + } + } catch(Resources.NotFoundException ex) { + loge("Wimax Resources does not exist!!! "); + return null; + } + + try { + if (DBG) log("Starting Wimax Service... "); + + Constructor wmxStTrkrConst = wimaxStateTrackerClass.getConstructor + (new Class[] {Context.class, Handler.class}); + wimaxStateTracker = (NetworkStateTracker) wmxStTrkrConst.newInstance( + context, trackerHandler); + + Constructor wmxSrvConst = wimaxServiceClass.getDeclaredConstructor + (new Class[] {Context.class, wimaxStateTrackerClass}); + wmxSrvConst.setAccessible(true); + IBinder svcInvoker = (IBinder)wmxSrvConst.newInstance(context, wimaxStateTracker); + wmxSrvConst.setAccessible(false); + + ServiceManager.addService(WimaxManagerConstants.WIMAX_SERVICE, svcInvoker); + + } catch(Exception ex) { + loge("Exception creating Wimax classes: " + ex.toString()); + return null; + } + } else { + loge("Wimax is not enabled or not added to the network attributes!!! "); + return null; + } + + return wimaxStateTracker; + } + + /** + * Sets the preferred network. + * @param preference the new preference + */ + public void setNetworkPreference(int preference) { + enforceChangePermission(); + + mHandler.sendMessage( + mHandler.obtainMessage(EVENT_SET_NETWORK_PREFERENCE, preference, 0)); + } + + public int getNetworkPreference() { + enforceAccessPermission(); + int preference; + synchronized(this) { + preference = mNetworkPreference; + } + return preference; + } + + private void handleSetNetworkPreference(int preference) { + if (ConnectivityManager.isNetworkTypeValid(preference) && + mNetConfigs[preference] != null && + mNetConfigs[preference].isDefault()) { + if (mNetworkPreference != preference) { + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.NETWORK_PREFERENCE, preference); + synchronized(this) { + mNetworkPreference = preference; + } + enforcePreference(); + } + } + } + + private int getConnectivityChangeDelay() { + final ContentResolver cr = mContext.getContentResolver(); + + /** Check system properties for the default value then use secure settings value, if any. */ + int defaultDelay = SystemProperties.getInt( + "conn." + Settings.Global.CONNECTIVITY_CHANGE_DELAY, + ConnectivityManager.CONNECTIVITY_CHANGE_DELAY_DEFAULT); + return Settings.Global.getInt(cr, Settings.Global.CONNECTIVITY_CHANGE_DELAY, + defaultDelay); + } + + private int getPersistedNetworkPreference() { + final ContentResolver cr = mContext.getContentResolver(); + + final int networkPrefSetting = Settings.Global + .getInt(cr, Settings.Global.NETWORK_PREFERENCE, -1); + + return networkPrefSetting; + } + + /** + * Make the state of network connectivity conform to the preference settings + * In this method, we only tear down a non-preferred network. Establishing + * a connection to the preferred network is taken care of when we handle + * the disconnect event from the non-preferred network + * (see {@link #handleDisconnect(NetworkInfo)}). + */ + private void enforcePreference() { + if (mNetTrackers[mNetworkPreference].getNetworkInfo().isConnected()) + return; + + if (!mNetTrackers[mNetworkPreference].isAvailable()) + return; + + for (int t=0; t <= ConnectivityManager.MAX_RADIO_TYPE; t++) { + if (t != mNetworkPreference && mNetTrackers[t] != null && + mNetTrackers[t].getNetworkInfo().isConnected()) { + if (DBG) { + log("tearing down " + mNetTrackers[t].getNetworkInfo() + + " in enforcePreference"); + } + teardown(mNetTrackers[t]); + } + } + } + + private boolean teardown(NetworkStateTracker netTracker) { + if (netTracker.teardown()) { + netTracker.setTeardownRequested(true); + return true; + } else { + return false; + } + } + + /** + * Check if UID should be blocked from using the network represented by the + * given {@link NetworkStateTracker}. + */ + private boolean isNetworkBlocked(NetworkStateTracker tracker, int uid) { + final String iface = tracker.getLinkProperties().getInterfaceName(); + + final boolean networkCostly; + final int uidRules; + synchronized (mRulesLock) { + networkCostly = mMeteredIfaces.contains(iface); + uidRules = mUidRules.get(uid, RULE_ALLOW_ALL); + } + + if (networkCostly && (uidRules & RULE_REJECT_METERED) != 0) { + return true; + } + + // no restrictive rules; network is visible + return false; + } + + /** + * Return a filtered {@link NetworkInfo}, potentially marked + * {@link DetailedState#BLOCKED} based on + * {@link #isNetworkBlocked(NetworkStateTracker, int)}. + */ + private NetworkInfo getFilteredNetworkInfo(NetworkStateTracker tracker, int uid) { + NetworkInfo info = tracker.getNetworkInfo(); + if (isNetworkBlocked(tracker, uid)) { + // network is blocked; clone and override state + info = new NetworkInfo(info); + info.setDetailedState(DetailedState.BLOCKED, null, null); + } + if (mLockdownTracker != null) { + info = mLockdownTracker.augmentNetworkInfo(info); + } + return info; + } + + /** + * Return NetworkInfo for the active (i.e., connected) network interface. + * It is assumed that at most one network is active at a time. If more + * than one is active, it is indeterminate which will be returned. + * @return the info for the active network, or {@code null} if none is + * active + */ + @Override + public NetworkInfo getActiveNetworkInfo() { + enforceAccessPermission(); + final int uid = Binder.getCallingUid(); + return getNetworkInfo(mActiveDefaultNetwork, uid); + } + + /** + * Find the first Provisioning network. + * + * @return NetworkInfo or null if none. + */ + private NetworkInfo getProvisioningNetworkInfo() { + enforceAccessPermission(); + + // Find the first Provisioning Network + NetworkInfo provNi = null; + for (NetworkInfo ni : getAllNetworkInfo()) { + if (ni.isConnectedToProvisioningNetwork()) { + provNi = ni; + break; + } + } + if (DBG) log("getProvisioningNetworkInfo: X provNi=" + provNi); + return provNi; + } + + /** + * Find the first Provisioning network or the ActiveDefaultNetwork + * if there is no Provisioning network + * + * @return NetworkInfo or null if none. + */ + @Override + public NetworkInfo getProvisioningOrActiveNetworkInfo() { + enforceAccessPermission(); + + NetworkInfo provNi = getProvisioningNetworkInfo(); + if (provNi == null) { + final int uid = Binder.getCallingUid(); + provNi = getNetworkInfo(mActiveDefaultNetwork, uid); + } + if (DBG) log("getProvisioningOrActiveNetworkInfo: X provNi=" + provNi); + return provNi; + } + + public NetworkInfo getActiveNetworkInfoUnfiltered() { + enforceAccessPermission(); + if (isNetworkTypeValid(mActiveDefaultNetwork)) { + final NetworkStateTracker tracker = mNetTrackers[mActiveDefaultNetwork]; + if (tracker != null) { + return tracker.getNetworkInfo(); + } + } + return null; + } + + @Override + public NetworkInfo getActiveNetworkInfoForUid(int uid) { + enforceConnectivityInternalPermission(); + return getNetworkInfo(mActiveDefaultNetwork, uid); + } + + @Override + public NetworkInfo getNetworkInfo(int networkType) { + enforceAccessPermission(); + final int uid = Binder.getCallingUid(); + return getNetworkInfo(networkType, uid); + } + + private NetworkInfo getNetworkInfo(int networkType, int uid) { + NetworkInfo info = null; + if (isNetworkTypeValid(networkType)) { + final NetworkStateTracker tracker = mNetTrackers[networkType]; + if (tracker != null) { + info = getFilteredNetworkInfo(tracker, uid); + } + } + return info; + } + + @Override + public NetworkInfo[] getAllNetworkInfo() { + enforceAccessPermission(); + final int uid = Binder.getCallingUid(); + final ArrayList result = Lists.newArrayList(); + synchronized (mRulesLock) { + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + result.add(getFilteredNetworkInfo(tracker, uid)); + } + } + } + return result.toArray(new NetworkInfo[result.size()]); + } + + @Override + public boolean isNetworkSupported(int networkType) { + enforceAccessPermission(); + return (isNetworkTypeValid(networkType) && (mNetTrackers[networkType] != null)); + } + + /** + * Return LinkProperties for the active (i.e., connected) default + * network interface. It is assumed that at most one default network + * is active at a time. If more than one is active, it is indeterminate + * which will be returned. + * @return the ip properties for the active network, or {@code null} if + * none is active + */ + @Override + public LinkProperties getActiveLinkProperties() { + return getLinkProperties(mActiveDefaultNetwork); + } + + @Override + public LinkProperties getLinkProperties(int networkType) { + enforceAccessPermission(); + if (isNetworkTypeValid(networkType)) { + final NetworkStateTracker tracker = mNetTrackers[networkType]; + if (tracker != null) { + return tracker.getLinkProperties(); + } + } + return null; + } + + @Override + public NetworkState[] getAllNetworkState() { + enforceAccessPermission(); + final int uid = Binder.getCallingUid(); + final ArrayList result = Lists.newArrayList(); + synchronized (mRulesLock) { + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + final NetworkInfo info = getFilteredNetworkInfo(tracker, uid); + result.add(new NetworkState( + info, tracker.getLinkProperties(), tracker.getLinkCapabilities())); + } + } + } + return result.toArray(new NetworkState[result.size()]); + } + + private NetworkState getNetworkStateUnchecked(int networkType) { + if (isNetworkTypeValid(networkType)) { + final NetworkStateTracker tracker = mNetTrackers[networkType]; + if (tracker != null) { + return new NetworkState(tracker.getNetworkInfo(), tracker.getLinkProperties(), + tracker.getLinkCapabilities()); + } + } + return null; + } + + @Override + public NetworkQuotaInfo getActiveNetworkQuotaInfo() { + enforceAccessPermission(); + + final long token = Binder.clearCallingIdentity(); + try { + final NetworkState state = getNetworkStateUnchecked(mActiveDefaultNetwork); + if (state != null) { + try { + return mPolicyManager.getNetworkQuotaInfo(state); + } catch (RemoteException e) { + } + } + return null; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public boolean isActiveNetworkMetered() { + enforceAccessPermission(); + final long token = Binder.clearCallingIdentity(); + try { + return isNetworkMeteredUnchecked(mActiveDefaultNetwork); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean isNetworkMeteredUnchecked(int networkType) { + final NetworkState state = getNetworkStateUnchecked(networkType); + if (state != null) { + try { + return mPolicyManager.isNetworkMetered(state); + } catch (RemoteException e) { + } + } + return false; + } + + public boolean setRadios(boolean turnOn) { + boolean result = true; + enforceChangePermission(); + for (NetworkStateTracker t : mNetTrackers) { + if (t != null) result = t.setRadio(turnOn) && result; + } + return result; + } + + public boolean setRadio(int netType, boolean turnOn) { + enforceChangePermission(); + if (!ConnectivityManager.isNetworkTypeValid(netType)) { + return false; + } + NetworkStateTracker tracker = mNetTrackers[netType]; + return tracker != null && tracker.setRadio(turnOn); + } + + private INetworkManagementEventObserver mDataActivityObserver = new BaseNetworkObserver() { + @Override + public void interfaceClassDataActivityChanged(String label, boolean active) { + int deviceType = Integer.parseInt(label); + sendDataActivityBroadcast(deviceType, active); + } + }; + + /** + * Used to notice when the calling process dies so we can self-expire + * + * Also used to know if the process has cleaned up after itself when + * our auto-expire timer goes off. The timer has a link to an object. + * + */ + private class FeatureUser implements IBinder.DeathRecipient { + int mNetworkType; + String mFeature; + IBinder mBinder; + int mPid; + int mUid; + long mCreateTime; + + FeatureUser(int type, String feature, IBinder binder) { + super(); + mNetworkType = type; + mFeature = feature; + mBinder = binder; + mPid = getCallingPid(); + mUid = getCallingUid(); + mCreateTime = System.currentTimeMillis(); + + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + + void unlinkDeathRecipient() { + mBinder.unlinkToDeath(this, 0); + } + + public void binderDied() { + log("ConnectivityService FeatureUser binderDied(" + + mNetworkType + ", " + mFeature + ", " + mBinder + "), created " + + (System.currentTimeMillis() - mCreateTime) + " mSec ago"); + stopUsingNetworkFeature(this, false); + } + + public void expire() { + if (VDBG) { + log("ConnectivityService FeatureUser expire(" + + mNetworkType + ", " + mFeature + ", " + mBinder +"), created " + + (System.currentTimeMillis() - mCreateTime) + " mSec ago"); + } + stopUsingNetworkFeature(this, false); + } + + public boolean isSameUser(FeatureUser u) { + if (u == null) return false; + + return isSameUser(u.mPid, u.mUid, u.mNetworkType, u.mFeature); + } + + public boolean isSameUser(int pid, int uid, int networkType, String feature) { + if ((mPid == pid) && (mUid == uid) && (mNetworkType == networkType) && + TextUtils.equals(mFeature, feature)) { + return true; + } + return false; + } + + public String toString() { + return "FeatureUser("+mNetworkType+","+mFeature+","+mPid+","+mUid+"), created " + + (System.currentTimeMillis() - mCreateTime) + " mSec ago"; + } + } + + // javadoc from interface + public int startUsingNetworkFeature(int networkType, String feature, + IBinder binder) { + long startTime = 0; + if (DBG) { + startTime = SystemClock.elapsedRealtime(); + } + if (VDBG) { + log("startUsingNetworkFeature for net " + networkType + ": " + feature + ", uid=" + + Binder.getCallingUid()); + } + enforceChangePermission(); + try { + if (!ConnectivityManager.isNetworkTypeValid(networkType) || + mNetConfigs[networkType] == null) { + return PhoneConstants.APN_REQUEST_FAILED; + } + + FeatureUser f = new FeatureUser(networkType, feature, binder); + + // TODO - move this into individual networktrackers + int usedNetworkType = convertFeatureToNetworkType(networkType, feature); + + if (mLockdownEnabled) { + // Since carrier APNs usually aren't available from VPN + // endpoint, mark them as unavailable. + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } + + if (mProtectedNetworks.contains(usedNetworkType)) { + enforceConnectivityInternalPermission(); + } + + // if UID is restricted, don't allow them to bring up metered APNs + final boolean networkMetered = isNetworkMeteredUnchecked(usedNetworkType); + final int uidRules; + synchronized (mRulesLock) { + uidRules = mUidRules.get(Binder.getCallingUid(), RULE_ALLOW_ALL); + } + if (networkMetered && (uidRules & RULE_REJECT_METERED) != 0) { + return PhoneConstants.APN_REQUEST_FAILED; + } + + NetworkStateTracker network = mNetTrackers[usedNetworkType]; + if (network != null) { + Integer currentPid = new Integer(getCallingPid()); + if (usedNetworkType != networkType) { + NetworkInfo ni = network.getNetworkInfo(); + + if (ni.isAvailable() == false) { + if (!TextUtils.equals(feature,Phone.FEATURE_ENABLE_DUN_ALWAYS)) { + if (DBG) log("special network not available ni=" + ni.getTypeName()); + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } else { + // else make the attempt anyway - probably giving REQUEST_STARTED below + if (DBG) { + log("special network not available, but try anyway ni=" + + ni.getTypeName()); + } + } + } + + int restoreTimer = getRestoreDefaultNetworkDelay(usedNetworkType); + + synchronized(this) { + boolean addToList = true; + if (restoreTimer < 0) { + // In case there is no timer is specified for the feature, + // make sure we don't add duplicate entry with the same request. + for (FeatureUser u : mFeatureUsers) { + if (u.isSameUser(f)) { + // Duplicate user is found. Do not add. + addToList = false; + break; + } + } + } + + if (addToList) mFeatureUsers.add(f); + if (!mNetRequestersPids[usedNetworkType].contains(currentPid)) { + // this gets used for per-pid dns when connected + mNetRequestersPids[usedNetworkType].add(currentPid); + } + } + + if (restoreTimer >= 0) { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + EVENT_RESTORE_DEFAULT_NETWORK, f), restoreTimer); + } + + if ((ni.isConnectedOrConnecting() == true) && + !network.isTeardownRequested()) { + if (ni.isConnected() == true) { + final long token = Binder.clearCallingIdentity(); + try { + // add the pid-specific dns + handleDnsConfigurationChange(usedNetworkType); + if (VDBG) log("special network already active"); + } finally { + Binder.restoreCallingIdentity(token); + } + return PhoneConstants.APN_ALREADY_ACTIVE; + } + if (VDBG) log("special network already connecting"); + return PhoneConstants.APN_REQUEST_STARTED; + } + + // check if the radio in play can make another contact + // assume if cannot for now + + if (DBG) { + log("startUsingNetworkFeature reconnecting to " + networkType + ": " + + feature); + } + if (network.reconnect()) { + if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_STARTED"); + return PhoneConstants.APN_REQUEST_STARTED; + } else { + if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_FAILED"); + return PhoneConstants.APN_REQUEST_FAILED; + } + } else { + // need to remember this unsupported request so we respond appropriately on stop + synchronized(this) { + mFeatureUsers.add(f); + if (!mNetRequestersPids[usedNetworkType].contains(currentPid)) { + // this gets used for per-pid dns when connected + mNetRequestersPids[usedNetworkType].add(currentPid); + } + } + if (DBG) log("startUsingNetworkFeature X: return -1 unsupported feature."); + return -1; + } + } + if (DBG) log("startUsingNetworkFeature X: return APN_TYPE_NOT_AVAILABLE"); + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } finally { + if (DBG) { + final long execTime = SystemClock.elapsedRealtime() - startTime; + if (execTime > 250) { + loge("startUsingNetworkFeature took too long: " + execTime + "ms"); + } else { + if (VDBG) log("startUsingNetworkFeature took " + execTime + "ms"); + } + } + } + } + + // javadoc from interface + public int stopUsingNetworkFeature(int networkType, String feature) { + enforceChangePermission(); + + int pid = getCallingPid(); + int uid = getCallingUid(); + + FeatureUser u = null; + boolean found = false; + + synchronized(this) { + for (FeatureUser x : mFeatureUsers) { + if (x.isSameUser(pid, uid, networkType, feature)) { + u = x; + found = true; + break; + } + } + } + if (found && u != null) { + if (VDBG) log("stopUsingNetworkFeature: X"); + // stop regardless of how many other time this proc had called start + return stopUsingNetworkFeature(u, true); + } else { + // none found! + if (VDBG) log("stopUsingNetworkFeature: X not a live request, ignoring"); + return 1; + } + } + + private int stopUsingNetworkFeature(FeatureUser u, boolean ignoreDups) { + int networkType = u.mNetworkType; + String feature = u.mFeature; + int pid = u.mPid; + int uid = u.mUid; + + NetworkStateTracker tracker = null; + boolean callTeardown = false; // used to carry our decision outside of sync block + + if (VDBG) { + log("stopUsingNetworkFeature: net " + networkType + ": " + feature); + } + + if (!ConnectivityManager.isNetworkTypeValid(networkType)) { + if (DBG) { + log("stopUsingNetworkFeature: net " + networkType + ": " + feature + + ", net is invalid"); + } + return -1; + } + + // need to link the mFeatureUsers list with the mNetRequestersPids state in this + // sync block + synchronized(this) { + // check if this process still has an outstanding start request + if (!mFeatureUsers.contains(u)) { + if (VDBG) { + log("stopUsingNetworkFeature: this process has no outstanding requests" + + ", ignoring"); + } + return 1; + } + u.unlinkDeathRecipient(); + mFeatureUsers.remove(mFeatureUsers.indexOf(u)); + // If we care about duplicate requests, check for that here. + // + // This is done to support the extension of a request - the app + // can request we start the network feature again and renew the + // auto-shutoff delay. Normal "stop" calls from the app though + // do not pay attention to duplicate requests - in effect the + // API does not refcount and a single stop will counter multiple starts. + if (ignoreDups == false) { + for (FeatureUser x : mFeatureUsers) { + if (x.isSameUser(u)) { + if (VDBG) log("stopUsingNetworkFeature: dup is found, ignoring"); + return 1; + } + } + } + + // TODO - move to individual network trackers + int usedNetworkType = convertFeatureToNetworkType(networkType, feature); + + tracker = mNetTrackers[usedNetworkType]; + if (tracker == null) { + if (DBG) { + log("stopUsingNetworkFeature: net " + networkType + ": " + feature + + " no known tracker for used net type " + usedNetworkType); + } + return -1; + } + if (usedNetworkType != networkType) { + Integer currentPid = new Integer(pid); + mNetRequestersPids[usedNetworkType].remove(currentPid); + + final long token = Binder.clearCallingIdentity(); + try { + reassessPidDns(pid, true); + } finally { + Binder.restoreCallingIdentity(token); + } + flushVmDnsCache(); + if (mNetRequestersPids[usedNetworkType].size() != 0) { + if (VDBG) { + log("stopUsingNetworkFeature: net " + networkType + ": " + feature + + " others still using it"); + } + return 1; + } + callTeardown = true; + } else { + if (DBG) { + log("stopUsingNetworkFeature: net " + networkType + ": " + feature + + " not a known feature - dropping"); + } + } + } + + if (callTeardown) { + if (DBG) { + log("stopUsingNetworkFeature: teardown net " + networkType + ": " + feature); + } + tracker.teardown(); + return 1; + } else { + return -1; + } + } + + /** + * @deprecated use requestRouteToHostAddress instead + * + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. + * @param networkType the type of the network over which traffic to the + * specified host is to be routed + * @param hostAddress the IP address of the host to which the route is + * desired + * @return {@code true} on success, {@code false} on failure + */ + public boolean requestRouteToHost(int networkType, int hostAddress) { + InetAddress inetAddress = NetworkUtils.intToInetAddress(hostAddress); + + if (inetAddress == null) { + return false; + } + + return requestRouteToHostAddress(networkType, inetAddress.getAddress()); + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. + * @param networkType the type of the network over which traffic to the + * specified host is to be routed + * @param hostAddress the IP address of the host to which the route is + * desired + * @return {@code true} on success, {@code false} on failure + */ + public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress) { + enforceChangePermission(); + if (mProtectedNetworks.contains(networkType)) { + enforceConnectivityInternalPermission(); + } + + if (!ConnectivityManager.isNetworkTypeValid(networkType)) { + if (DBG) log("requestRouteToHostAddress on invalid network: " + networkType); + return false; + } + NetworkStateTracker tracker = mNetTrackers[networkType]; + DetailedState netState = DetailedState.DISCONNECTED; + if (tracker != null) { + netState = tracker.getNetworkInfo().getDetailedState(); + } + + if ((netState != DetailedState.CONNECTED && + netState != DetailedState.CAPTIVE_PORTAL_CHECK) || + tracker.isTeardownRequested()) { + if (VDBG) { + log("requestRouteToHostAddress on down network " + + "(" + networkType + ") - dropped" + + " tracker=" + tracker + + " netState=" + netState + + " isTeardownRequested=" + + ((tracker != null) ? tracker.isTeardownRequested() : "tracker:null")); + } + return false; + } + final long token = Binder.clearCallingIdentity(); + try { + InetAddress addr = InetAddress.getByAddress(hostAddress); + LinkProperties lp = tracker.getLinkProperties(); + boolean ok = addRouteToAddress(lp, addr, EXEMPT); + if (DBG) log("requestRouteToHostAddress ok=" + ok); + return ok; + } catch (UnknownHostException e) { + if (DBG) log("requestRouteToHostAddress got " + e.toString()); + } finally { + Binder.restoreCallingIdentity(token); + } + if (DBG) log("requestRouteToHostAddress X bottom return false"); + return false; + } + + private boolean addRoute(LinkProperties p, RouteInfo r, boolean toDefaultTable, + boolean exempt) { + return modifyRoute(p, r, 0, ADD, toDefaultTable, exempt); + } + + private boolean removeRoute(LinkProperties p, RouteInfo r, boolean toDefaultTable) { + return modifyRoute(p, r, 0, REMOVE, toDefaultTable, UNEXEMPT); + } + + private boolean addRouteToAddress(LinkProperties lp, InetAddress addr, boolean exempt) { + return modifyRouteToAddress(lp, addr, ADD, TO_DEFAULT_TABLE, exempt); + } + + private boolean removeRouteToAddress(LinkProperties lp, InetAddress addr) { + return modifyRouteToAddress(lp, addr, REMOVE, TO_DEFAULT_TABLE, UNEXEMPT); + } + + private boolean modifyRouteToAddress(LinkProperties lp, InetAddress addr, boolean doAdd, + boolean toDefaultTable, boolean exempt) { + RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), addr); + if (bestRoute == null) { + bestRoute = RouteInfo.makeHostRoute(addr, lp.getInterfaceName()); + } else { + String iface = bestRoute.getInterface(); + if (bestRoute.getGateway().equals(addr)) { + // if there is no better route, add the implied hostroute for our gateway + bestRoute = RouteInfo.makeHostRoute(addr, iface); + } else { + // if we will connect to this through another route, add a direct route + // to it's gateway + bestRoute = RouteInfo.makeHostRoute(addr, bestRoute.getGateway(), iface); + } + } + return modifyRoute(lp, bestRoute, 0, doAdd, toDefaultTable, exempt); + } + + private boolean modifyRoute(LinkProperties lp, RouteInfo r, int cycleCount, boolean doAdd, + boolean toDefaultTable, boolean exempt) { + if ((lp == null) || (r == null)) { + if (DBG) log("modifyRoute got unexpected null: " + lp + ", " + r); + return false; + } + + if (cycleCount > MAX_HOSTROUTE_CYCLE_COUNT) { + loge("Error modifying route - too much recursion"); + return false; + } + + String ifaceName = r.getInterface(); + if(ifaceName == null) { + loge("Error modifying route - no interface name"); + return false; + } + if (r.hasGateway()) { + RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), r.getGateway()); + if (bestRoute != null) { + if (bestRoute.getGateway().equals(r.getGateway())) { + // if there is no better route, add the implied hostroute for our gateway + bestRoute = RouteInfo.makeHostRoute(r.getGateway(), ifaceName); + } else { + // if we will connect to our gateway through another route, add a direct + // route to it's gateway + bestRoute = RouteInfo.makeHostRoute(r.getGateway(), + bestRoute.getGateway(), + ifaceName); + } + modifyRoute(lp, bestRoute, cycleCount+1, doAdd, toDefaultTable, exempt); + } + } + if (doAdd) { + if (VDBG) log("Adding " + r + " for interface " + ifaceName); + try { + if (toDefaultTable) { + synchronized (mRoutesLock) { + // only track default table - only one apps can effect + mAddedRoutes.add(r); + mNetd.addRoute(ifaceName, r); + if (exempt) { + LinkAddress dest = r.getDestination(); + if (!mExemptAddresses.contains(dest)) { + mNetd.setHostExemption(dest); + mExemptAddresses.add(dest); + } + } + } + } else { + mNetd.addSecondaryRoute(ifaceName, r); + } + } catch (Exception e) { + // never crash - catch them all + if (DBG) loge("Exception trying to add a route: " + e); + return false; + } + } else { + // if we remove this one and there are no more like it, then refcount==0 and + // we can remove it from the table + if (toDefaultTable) { + synchronized (mRoutesLock) { + mAddedRoutes.remove(r); + if (mAddedRoutes.contains(r) == false) { + if (VDBG) log("Removing " + r + " for interface " + ifaceName); + try { + mNetd.removeRoute(ifaceName, r); + LinkAddress dest = r.getDestination(); + if (mExemptAddresses.contains(dest)) { + mNetd.clearHostExemption(dest); + mExemptAddresses.remove(dest); + } + } catch (Exception e) { + // never crash - catch them all + if (VDBG) loge("Exception trying to remove a route: " + e); + return false; + } + } else { + if (VDBG) log("not removing " + r + " as it's still in use"); + } + } + } else { + if (VDBG) log("Removing " + r + " for interface " + ifaceName); + try { + mNetd.removeSecondaryRoute(ifaceName, r); + } catch (Exception e) { + // never crash - catch them all + if (VDBG) loge("Exception trying to remove a route: " + e); + return false; + } + } + } + return true; + } + + /** + * @see ConnectivityManager#getMobileDataEnabled() + */ + public boolean getMobileDataEnabled() { + // TODO: This detail should probably be in DataConnectionTracker's + // which is where we store the value and maybe make this + // asynchronous. + enforceAccessPermission(); + boolean retVal = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.MOBILE_DATA, 1) == 1; + if (VDBG) log("getMobileDataEnabled returning " + retVal); + return retVal; + } + + public void setDataDependency(int networkType, boolean met) { + enforceConnectivityInternalPermission(); + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_DEPENDENCY_MET, + (met ? ENABLED : DISABLED), networkType)); + } + + private void handleSetDependencyMet(int networkType, boolean met) { + if (mNetTrackers[networkType] != null) { + if (DBG) { + log("handleSetDependencyMet(" + networkType + ", " + met + ")"); + } + mNetTrackers[networkType].setDependencyMet(met); + } + } + + private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() { + @Override + public void onUidRulesChanged(int uid, int uidRules) { + // caller is NPMS, since we only register with them + if (LOGD_RULES) { + log("onUidRulesChanged(uid=" + uid + ", uidRules=" + uidRules + ")"); + } + + synchronized (mRulesLock) { + // skip update when we've already applied rules + final int oldRules = mUidRules.get(uid, RULE_ALLOW_ALL); + if (oldRules == uidRules) return; + + mUidRules.put(uid, uidRules); + } + + // TODO: notify UID when it has requested targeted updates + } + + @Override + public void onMeteredIfacesChanged(String[] meteredIfaces) { + // caller is NPMS, since we only register with them + if (LOGD_RULES) { + log("onMeteredIfacesChanged(ifaces=" + Arrays.toString(meteredIfaces) + ")"); + } + + synchronized (mRulesLock) { + mMeteredIfaces.clear(); + for (String iface : meteredIfaces) { + mMeteredIfaces.add(iface); + } + } + } + + @Override + public void onRestrictBackgroundChanged(boolean restrictBackground) { + // caller is NPMS, since we only register with them + if (LOGD_RULES) { + log("onRestrictBackgroundChanged(restrictBackground=" + restrictBackground + ")"); + } + + // kick off connectivity change broadcast for active network, since + // global background policy change is radical. + final int networkType = mActiveDefaultNetwork; + if (isNetworkTypeValid(networkType)) { + final NetworkStateTracker tracker = mNetTrackers[networkType]; + if (tracker != null) { + final NetworkInfo info = tracker.getNetworkInfo(); + if (info != null && info.isConnected()) { + sendConnectedBroadcast(info); + } + } + } + } + }; + + /** + * @see ConnectivityManager#setMobileDataEnabled(boolean) + */ + public void setMobileDataEnabled(boolean enabled) { + enforceChangePermission(); + if (DBG) log("setMobileDataEnabled(" + enabled + ")"); + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_MOBILE_DATA, + (enabled ? ENABLED : DISABLED), 0)); + } + + private void handleSetMobileData(boolean enabled) { + if (mNetTrackers[ConnectivityManager.TYPE_MOBILE] != null) { + if (VDBG) { + log(mNetTrackers[ConnectivityManager.TYPE_MOBILE].toString() + enabled); + } + mNetTrackers[ConnectivityManager.TYPE_MOBILE].setUserDataEnable(enabled); + } + if (mNetTrackers[ConnectivityManager.TYPE_WIMAX] != null) { + if (VDBG) { + log(mNetTrackers[ConnectivityManager.TYPE_WIMAX].toString() + enabled); + } + mNetTrackers[ConnectivityManager.TYPE_WIMAX].setUserDataEnable(enabled); + } + } + + @Override + public void setPolicyDataEnable(int networkType, boolean enabled) { + // only someone like NPMS should only be calling us + mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG); + + mHandler.sendMessage(mHandler.obtainMessage( + EVENT_SET_POLICY_DATA_ENABLE, networkType, (enabled ? ENABLED : DISABLED))); + } + + private void handleSetPolicyDataEnable(int networkType, boolean enabled) { + if (isNetworkTypeValid(networkType)) { + final NetworkStateTracker tracker = mNetTrackers[networkType]; + if (tracker != null) { + tracker.setPolicyDataEnable(enabled); + } + } + } + + private void enforceAccessPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_NETWORK_STATE, + "ConnectivityService"); + } + + private void enforceChangePermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CHANGE_NETWORK_STATE, + "ConnectivityService"); + } + + // TODO Make this a special check when it goes public + private void enforceTetherChangePermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CHANGE_NETWORK_STATE, + "ConnectivityService"); + } + + private void enforceTetherAccessPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_NETWORK_STATE, + "ConnectivityService"); + } + + private void enforceConnectivityInternalPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_INTERNAL, + "ConnectivityService"); + } + + private void enforceMarkNetworkSocketPermission() { + //Media server special case + if (Binder.getCallingUid() == Process.MEDIA_UID) { + return; + } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MARK_NETWORK_SOCKET, + "ConnectivityService"); + } + + /** + * Handle a {@code DISCONNECTED} event. If this pertains to the non-active + * network, we ignore it. If it is for the active network, we send out a + * broadcast. But first, we check whether it might be possible to connect + * to a different network. + * @param info the {@code NetworkInfo} for the network + */ + private void handleDisconnect(NetworkInfo info) { + + int prevNetType = info.getType(); + + mNetTrackers[prevNetType].setTeardownRequested(false); + + // Remove idletimer previously setup in {@code handleConnect} + removeDataActivityTracking(prevNetType); + + /* + * If the disconnected network is not the active one, then don't report + * this as a loss of connectivity. What probably happened is that we're + * getting the disconnect for a network that we explicitly disabled + * in accordance with network preference policies. + */ + if (!mNetConfigs[prevNetType].isDefault()) { + List pids = mNetRequestersPids[prevNetType]; + for (Integer pid : pids) { + // will remove them because the net's no longer connected + // need to do this now as only now do we know the pids and + // can properly null things that are no longer referenced. + reassessPidDns(pid.intValue(), false); + } + } + + Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info)); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); + if (info.isFailover()) { + intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true); + info.setFailover(false); + } + if (info.getReason() != null) { + intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason()); + } + if (info.getExtraInfo() != null) { + intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, + info.getExtraInfo()); + } + + if (mNetConfigs[prevNetType].isDefault()) { + tryFailover(prevNetType); + if (mActiveDefaultNetwork != -1) { + NetworkInfo switchTo = mNetTrackers[mActiveDefaultNetwork].getNetworkInfo(); + intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO, switchTo); + } else { + mDefaultInetConditionPublished = 0; // we're not connected anymore + intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true); + } + } + intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished); + + // Reset interface if no other connections are using the same interface + boolean doReset = true; + LinkProperties linkProperties = mNetTrackers[prevNetType].getLinkProperties(); + if (linkProperties != null) { + String oldIface = linkProperties.getInterfaceName(); + if (TextUtils.isEmpty(oldIface) == false) { + for (NetworkStateTracker networkStateTracker : mNetTrackers) { + if (networkStateTracker == null) continue; + NetworkInfo networkInfo = networkStateTracker.getNetworkInfo(); + if (networkInfo.isConnected() && networkInfo.getType() != prevNetType) { + LinkProperties l = networkStateTracker.getLinkProperties(); + if (l == null) continue; + if (oldIface.equals(l.getInterfaceName())) { + doReset = false; + break; + } + } + } + } + } + + // do this before we broadcast the change + handleConnectivityChange(prevNetType, doReset); + + final Intent immediateIntent = new Intent(intent); + immediateIntent.setAction(CONNECTIVITY_ACTION_IMMEDIATE); + sendStickyBroadcast(immediateIntent); + sendStickyBroadcastDelayed(intent, getConnectivityChangeDelay()); + /* + * If the failover network is already connected, then immediately send + * out a followup broadcast indicating successful failover + */ + if (mActiveDefaultNetwork != -1) { + sendConnectedBroadcastDelayed(mNetTrackers[mActiveDefaultNetwork].getNetworkInfo(), + getConnectivityChangeDelay()); + } + } + + private void tryFailover(int prevNetType) { + /* + * If this is a default network, check if other defaults are available. + * Try to reconnect on all available and let them hash it out when + * more than one connects. + */ + if (mNetConfigs[prevNetType].isDefault()) { + if (mActiveDefaultNetwork == prevNetType) { + if (DBG) { + log("tryFailover: set mActiveDefaultNetwork=-1, prevNetType=" + prevNetType); + } + mActiveDefaultNetwork = -1; + } + + // don't signal a reconnect for anything lower or equal priority than our + // current connected default + // TODO - don't filter by priority now - nice optimization but risky +// int currentPriority = -1; +// if (mActiveDefaultNetwork != -1) { +// currentPriority = mNetConfigs[mActiveDefaultNetwork].mPriority; +// } + + for (int checkType=0; checkType <= ConnectivityManager.MAX_NETWORK_TYPE; checkType++) { + if (checkType == prevNetType) continue; + if (mNetConfigs[checkType] == null) continue; + if (!mNetConfigs[checkType].isDefault()) continue; + if (mNetTrackers[checkType] == null) continue; + +// Enabling the isAvailable() optimization caused mobile to not get +// selected if it was in the middle of error handling. Specifically +// a moble connection that took 30 seconds to complete the DEACTIVATE_DATA_CALL +// would not be available and we wouldn't get connected to anything. +// So removing the isAvailable() optimization below for now. TODO: This +// optimization should work and we need to investigate why it doesn't work. +// This could be related to how DEACTIVATE_DATA_CALL is reporting its +// complete before it is really complete. + +// if (!mNetTrackers[checkType].isAvailable()) continue; + +// if (currentPriority >= mNetConfigs[checkType].mPriority) continue; + + NetworkStateTracker checkTracker = mNetTrackers[checkType]; + NetworkInfo checkInfo = checkTracker.getNetworkInfo(); + if (!checkInfo.isConnectedOrConnecting() || checkTracker.isTeardownRequested()) { + checkInfo.setFailover(true); + checkTracker.reconnect(); + } + if (DBG) log("Attempting to switch to " + checkInfo.getTypeName()); + } + } + } + + public void sendConnectedBroadcast(NetworkInfo info) { + enforceConnectivityInternalPermission(); + sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE); + sendGeneralBroadcast(info, CONNECTIVITY_ACTION); + } + + private void sendConnectedBroadcastDelayed(NetworkInfo info, int delayMs) { + sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE); + sendGeneralBroadcastDelayed(info, CONNECTIVITY_ACTION, delayMs); + } + + private void sendInetConditionBroadcast(NetworkInfo info) { + sendGeneralBroadcast(info, ConnectivityManager.INET_CONDITION_ACTION); + } + + private Intent makeGeneralIntent(NetworkInfo info, String bcastType) { + if (mLockdownTracker != null) { + info = mLockdownTracker.augmentNetworkInfo(info); + } + + Intent intent = new Intent(bcastType); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info)); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); + if (info.isFailover()) { + intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true); + info.setFailover(false); + } + if (info.getReason() != null) { + intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason()); + } + if (info.getExtraInfo() != null) { + intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, + info.getExtraInfo()); + } + intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished); + return intent; + } + + private void sendGeneralBroadcast(NetworkInfo info, String bcastType) { + sendStickyBroadcast(makeGeneralIntent(info, bcastType)); + } + + private void sendGeneralBroadcastDelayed(NetworkInfo info, String bcastType, int delayMs) { + sendStickyBroadcastDelayed(makeGeneralIntent(info, bcastType), delayMs); + } + + private void sendDataActivityBroadcast(int deviceType, boolean active) { + Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE); + intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType); + intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, + RECEIVE_DATA_ACTIVITY_CHANGE, null, null, 0, null, null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * Called when an attempt to fail over to another network has failed. + * @param info the {@link NetworkInfo} for the failed network + */ + private void handleConnectionFailure(NetworkInfo info) { + mNetTrackers[info.getType()].setTeardownRequested(false); + + String reason = info.getReason(); + String extraInfo = info.getExtraInfo(); + + String reasonText; + if (reason == null) { + reasonText = "."; + } else { + reasonText = " (" + reason + ")."; + } + loge("Attempt to connect to " + info.getTypeName() + " failed" + reasonText); + + Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info)); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); + if (getActiveNetworkInfo() == null) { + intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true); + } + if (reason != null) { + intent.putExtra(ConnectivityManager.EXTRA_REASON, reason); + } + if (extraInfo != null) { + intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, extraInfo); + } + if (info.isFailover()) { + intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true); + info.setFailover(false); + } + + if (mNetConfigs[info.getType()].isDefault()) { + tryFailover(info.getType()); + if (mActiveDefaultNetwork != -1) { + NetworkInfo switchTo = mNetTrackers[mActiveDefaultNetwork].getNetworkInfo(); + intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO, switchTo); + } else { + mDefaultInetConditionPublished = 0; + intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true); + } + } + + intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished); + + final Intent immediateIntent = new Intent(intent); + immediateIntent.setAction(CONNECTIVITY_ACTION_IMMEDIATE); + sendStickyBroadcast(immediateIntent); + sendStickyBroadcast(intent); + /* + * If the failover network is already connected, then immediately send + * out a followup broadcast indicating successful failover + */ + if (mActiveDefaultNetwork != -1) { + sendConnectedBroadcast(mNetTrackers[mActiveDefaultNetwork].getNetworkInfo()); + } + } + + private void sendStickyBroadcast(Intent intent) { + synchronized(this) { + if (!mSystemReady) { + mInitialBroadcast = new Intent(intent); + } + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + if (VDBG) { + log("sendStickyBroadcast: action=" + intent.getAction()); + } + + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + private void sendStickyBroadcastDelayed(Intent intent, int delayMs) { + if (delayMs <= 0) { + sendStickyBroadcast(intent); + } else { + if (VDBG) { + log("sendStickyBroadcastDelayed: delayMs=" + delayMs + ", action=" + + intent.getAction()); + } + mHandler.sendMessageDelayed(mHandler.obtainMessage( + EVENT_SEND_STICKY_BROADCAST_INTENT, intent), delayMs); + } + } + + void systemReady() { + mCaptivePortalTracker = CaptivePortalTracker.makeCaptivePortalTracker(mContext, this); + loadGlobalProxy(); + + synchronized(this) { + mSystemReady = true; + if (mInitialBroadcast != null) { + mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL); + mInitialBroadcast = null; + } + } + // load the global proxy at startup + mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY)); + + // Try bringing up tracker, but if KeyStore isn't ready yet, wait + // for user to unlock device. + if (!updateLockdownVpn()) { + final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); + mContext.registerReceiver(mUserPresentReceiver, filter); + } + } + + private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Try creating lockdown tracker, since user present usually means + // unlocked keystore. + if (updateLockdownVpn()) { + mContext.unregisterReceiver(this); + } + } + }; + + private boolean isNewNetTypePreferredOverCurrentNetType(int type) { + if (((type != mNetworkPreference) + && (mNetConfigs[mActiveDefaultNetwork].priority > mNetConfigs[type].priority)) + || (mNetworkPreference == mActiveDefaultNetwork)) { + return false; + } + return true; + } + + private void handleConnect(NetworkInfo info) { + final int newNetType = info.getType(); + + setupDataActivityTracking(newNetType); + + // snapshot isFailover, because sendConnectedBroadcast() resets it + boolean isFailover = info.isFailover(); + final NetworkStateTracker thisNet = mNetTrackers[newNetType]; + final String thisIface = thisNet.getLinkProperties().getInterfaceName(); + + if (VDBG) { + log("handleConnect: E newNetType=" + newNetType + " thisIface=" + thisIface + + " isFailover" + isFailover); + } + + // if this is a default net and other default is running + // kill the one not preferred + if (mNetConfigs[newNetType].isDefault()) { + if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != newNetType) { + if (isNewNetTypePreferredOverCurrentNetType(newNetType)) { + // tear down the other + NetworkStateTracker otherNet = + mNetTrackers[mActiveDefaultNetwork]; + if (DBG) { + log("Policy requires " + otherNet.getNetworkInfo().getTypeName() + + " teardown"); + } + if (!teardown(otherNet)) { + loge("Network declined teardown request"); + teardown(thisNet); + return; + } + } else { + // don't accept this one + if (VDBG) { + log("Not broadcasting CONNECT_ACTION " + + "to torn down network " + info.getTypeName()); + } + teardown(thisNet); + return; + } + } + synchronized (ConnectivityService.this) { + // have a new default network, release the transition wakelock in a second + // if it's held. The second pause is to allow apps to reconnect over the + // new network + if (mNetTransitionWakeLock.isHeld()) { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + EVENT_CLEAR_NET_TRANSITION_WAKELOCK, + mNetTransitionWakeLockSerialNumber, 0), + 1000); + } + } + mActiveDefaultNetwork = newNetType; + // this will cause us to come up initially as unconnected and switching + // to connected after our normal pause unless somebody reports us as reall + // disconnected + mDefaultInetConditionPublished = 0; + mDefaultConnectionSequence++; + mInetConditionChangeInFlight = false; + // Don't do this - if we never sign in stay, grey + //reportNetworkCondition(mActiveDefaultNetwork, 100); + } + thisNet.setTeardownRequested(false); + updateNetworkSettings(thisNet); + updateMtuSizeSettings(thisNet); + handleConnectivityChange(newNetType, false); + sendConnectedBroadcastDelayed(info, getConnectivityChangeDelay()); + + // notify battery stats service about this network + if (thisIface != null) { + try { + BatteryStatsService.getService().noteNetworkInterfaceType(thisIface, newNetType); + } catch (RemoteException e) { + // ignored; service lives in system_server + } + } + } + + private void handleCaptivePortalTrackerCheck(NetworkInfo info) { + if (DBG) log("Captive portal check " + info); + int type = info.getType(); + final NetworkStateTracker thisNet = mNetTrackers[type]; + if (mNetConfigs[type].isDefault()) { + if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != type) { + if (isNewNetTypePreferredOverCurrentNetType(type)) { + if (DBG) log("Captive check on " + info.getTypeName()); + mCaptivePortalTracker.detectCaptivePortal(new NetworkInfo(info)); + return; + } else { + if (DBG) log("Tear down low priority net " + info.getTypeName()); + teardown(thisNet); + return; + } + } + } + + if (DBG) log("handleCaptivePortalTrackerCheck: call captivePortalCheckComplete ni=" + info); + thisNet.captivePortalCheckComplete(); + } + + /** @hide */ + @Override + public void captivePortalCheckComplete(NetworkInfo info) { + enforceConnectivityInternalPermission(); + if (DBG) log("captivePortalCheckComplete: ni=" + info); + mNetTrackers[info.getType()].captivePortalCheckComplete(); + } + + /** @hide */ + @Override + public void captivePortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { + enforceConnectivityInternalPermission(); + if (DBG) log("captivePortalCheckCompleted: ni=" + info + " captive=" + isCaptivePortal); + mNetTrackers[info.getType()].captivePortalCheckCompleted(isCaptivePortal); + } + + /** + * Setup data activity tracking for the given network interface. + * + * Every {@code setupDataActivityTracking} should be paired with a + * {@link removeDataActivityTracking} for cleanup. + */ + private void setupDataActivityTracking(int type) { + final NetworkStateTracker thisNet = mNetTrackers[type]; + final String iface = thisNet.getLinkProperties().getInterfaceName(); + + final int timeout; + + if (ConnectivityManager.isNetworkTypeMobile(type)) { + timeout = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DATA_ACTIVITY_TIMEOUT_MOBILE, + 0); + // Canonicalize mobile network type + type = ConnectivityManager.TYPE_MOBILE; + } else if (ConnectivityManager.TYPE_WIFI == type) { + timeout = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DATA_ACTIVITY_TIMEOUT_WIFI, + 0); + } else { + // do not track any other networks + timeout = 0; + } + + if (timeout > 0 && iface != null) { + try { + mNetd.addIdleTimer(iface, timeout, Integer.toString(type)); + } catch (RemoteException e) { + } + } + } + + /** + * Remove data activity tracking when network disconnects. + */ + private void removeDataActivityTracking(int type) { + final NetworkStateTracker net = mNetTrackers[type]; + final String iface = net.getLinkProperties().getInterfaceName(); + + if (iface != null && (ConnectivityManager.isNetworkTypeMobile(type) || + ConnectivityManager.TYPE_WIFI == type)) { + try { + // the call fails silently if no idletimer setup for this interface + mNetd.removeIdleTimer(iface); + } catch (RemoteException e) { + } + } + } + + /** + * After a change in the connectivity state of a network. We're mainly + * concerned with making sure that the list of DNS servers is set up + * according to which networks are connected, and ensuring that the + * right routing table entries exist. + */ + private void handleConnectivityChange(int netType, boolean doReset) { + int resetMask = doReset ? NetworkUtils.RESET_ALL_ADDRESSES : 0; + boolean exempt = ConnectivityManager.isNetworkTypeExempt(netType); + if (VDBG) { + log("handleConnectivityChange: netType=" + netType + " doReset=" + doReset + + " resetMask=" + resetMask); + } + + /* + * If a non-default network is enabled, add the host routes that + * will allow it's DNS servers to be accessed. + */ + handleDnsConfigurationChange(netType); + + LinkProperties curLp = mCurrentLinkProperties[netType]; + LinkProperties newLp = null; + + if (mNetTrackers[netType].getNetworkInfo().isConnected()) { + newLp = mNetTrackers[netType].getLinkProperties(); + if (VDBG) { + log("handleConnectivityChange: changed linkProperty[" + netType + "]:" + + " doReset=" + doReset + " resetMask=" + resetMask + + "\n curLp=" + curLp + + "\n newLp=" + newLp); + } + + if (curLp != null) { + if (curLp.isIdenticalInterfaceName(newLp)) { + CompareResult car = curLp.compareAddresses(newLp); + if ((car.removed.size() != 0) || (car.added.size() != 0)) { + for (LinkAddress linkAddr : car.removed) { + if (linkAddr.getAddress() instanceof Inet4Address) { + resetMask |= NetworkUtils.RESET_IPV4_ADDRESSES; + } + if (linkAddr.getAddress() instanceof Inet6Address) { + resetMask |= NetworkUtils.RESET_IPV6_ADDRESSES; + } + } + if (DBG) { + log("handleConnectivityChange: addresses changed" + + " linkProperty[" + netType + "]:" + " resetMask=" + resetMask + + "\n car=" + car); + } + } else { + if (DBG) { + log("handleConnectivityChange: address are the same reset per doReset" + + " linkProperty[" + netType + "]:" + + " resetMask=" + resetMask); + } + } + } else { + resetMask = NetworkUtils.RESET_ALL_ADDRESSES; + if (DBG) { + log("handleConnectivityChange: interface not not equivalent reset both" + + " linkProperty[" + netType + "]:" + + " resetMask=" + resetMask); + } + } + } + if (mNetConfigs[netType].isDefault()) { + handleApplyDefaultProxy(newLp.getHttpProxy()); + } + } else { + if (VDBG) { + log("handleConnectivityChange: changed linkProperty[" + netType + "]:" + + " doReset=" + doReset + " resetMask=" + resetMask + + "\n curLp=" + curLp + + "\n newLp= null"); + } + } + mCurrentLinkProperties[netType] = newLp; + boolean resetDns = updateRoutes(newLp, curLp, mNetConfigs[netType].isDefault(), exempt); + + if (resetMask != 0 || resetDns) { + if (VDBG) log("handleConnectivityChange: resetting"); + if (curLp != null) { + if (VDBG) log("handleConnectivityChange: resetting curLp=" + curLp); + for (String iface : curLp.getAllInterfaceNames()) { + if (TextUtils.isEmpty(iface) == false) { + if (resetMask != 0) { + if (DBG) log("resetConnections(" + iface + ", " + resetMask + ")"); + NetworkUtils.resetConnections(iface, resetMask); + + // Tell VPN the interface is down. It is a temporary + // but effective fix to make VPN aware of the change. + if ((resetMask & NetworkUtils.RESET_IPV4_ADDRESSES) != 0) { + synchronized(mVpns) { + for (int i = 0; i < mVpns.size(); i++) { + mVpns.valueAt(i).interfaceStatusChanged(iface, false); + } + } + } + } + if (resetDns) { + flushVmDnsCache(); + if (VDBG) log("resetting DNS cache for " + iface); + try { + mNetd.flushInterfaceDnsCache(iface); + } catch (Exception e) { + // never crash - catch them all + if (DBG) loge("Exception resetting dns cache: " + e); + } + } + } else { + loge("Can't reset connection for type "+netType); + } + } + } + } + + // Update 464xlat state. + NetworkStateTracker tracker = mNetTrackers[netType]; + if (mClat.requiresClat(netType, tracker)) { + + // If the connection was previously using clat, but is not using it now, stop the clat + // daemon. Normally, this happens automatically when the connection disconnects, but if + // the disconnect is not reported, or if the connection's LinkProperties changed for + // some other reason (e.g., handoff changes the IP addresses on the link), it would + // still be running. If it's not running, then stopping it is a no-op. + if (Nat464Xlat.isRunningClat(curLp) && !Nat464Xlat.isRunningClat(newLp)) { + mClat.stopClat(); + } + // If the link requires clat to be running, then start the daemon now. + if (mNetTrackers[netType].getNetworkInfo().isConnected()) { + mClat.startClat(tracker); + } else { + mClat.stopClat(); + } + } + + // TODO: Temporary notifying upstread change to Tethering. + // @see bug/4455071 + /** Notify TetheringService if interface name has been changed. */ + if (TextUtils.equals(mNetTrackers[netType].getNetworkInfo().getReason(), + PhoneConstants.REASON_LINK_PROPERTIES_CHANGED)) { + if (isTetheringSupported()) { + mTethering.handleTetherIfaceChange(); + } + } + } + + /** + * Add and remove routes using the old properties (null if not previously connected), + * new properties (null if becoming disconnected). May even be double null, which + * is a noop. + * Uses isLinkDefault to determine if default routes should be set or conversely if + * host routes should be set to the dns servers + * returns a boolean indicating the routes changed + */ + private boolean updateRoutes(LinkProperties newLp, LinkProperties curLp, + boolean isLinkDefault, boolean exempt) { + Collection routesToAdd = null; + CompareResult dnsDiff = new CompareResult(); + CompareResult routeDiff = new CompareResult(); + if (curLp != null) { + // check for the delta between the current set and the new + routeDiff = curLp.compareAllRoutes(newLp); + dnsDiff = curLp.compareDnses(newLp); + } else if (newLp != null) { + routeDiff.added = newLp.getAllRoutes(); + dnsDiff.added = newLp.getDnses(); + } + + boolean routesChanged = (routeDiff.removed.size() != 0 || routeDiff.added.size() != 0); + + for (RouteInfo r : routeDiff.removed) { + if (isLinkDefault || ! r.isDefaultRoute()) { + if (VDBG) log("updateRoutes: default remove route r=" + r); + removeRoute(curLp, r, TO_DEFAULT_TABLE); + } + if (isLinkDefault == false) { + // remove from a secondary route table + removeRoute(curLp, r, TO_SECONDARY_TABLE); + } + } + + if (!isLinkDefault) { + // handle DNS routes + if (routesChanged) { + // routes changed - remove all old dns entries and add new + if (curLp != null) { + for (InetAddress oldDns : curLp.getDnses()) { + removeRouteToAddress(curLp, oldDns); + } + } + if (newLp != null) { + for (InetAddress newDns : newLp.getDnses()) { + addRouteToAddress(newLp, newDns, exempt); + } + } + } else { + // no change in routes, check for change in dns themselves + for (InetAddress oldDns : dnsDiff.removed) { + removeRouteToAddress(curLp, oldDns); + } + for (InetAddress newDns : dnsDiff.added) { + addRouteToAddress(newLp, newDns, exempt); + } + } + } + + for (RouteInfo r : routeDiff.added) { + if (isLinkDefault || ! r.isDefaultRoute()) { + addRoute(newLp, r, TO_DEFAULT_TABLE, exempt); + } else { + // add to a secondary route table + addRoute(newLp, r, TO_SECONDARY_TABLE, UNEXEMPT); + + // many radios add a default route even when we don't want one. + // remove the default route unless somebody else has asked for it + String ifaceName = newLp.getInterfaceName(); + synchronized (mRoutesLock) { + if (!TextUtils.isEmpty(ifaceName) && !mAddedRoutes.contains(r)) { + if (VDBG) log("Removing " + r + " for interface " + ifaceName); + try { + mNetd.removeRoute(ifaceName, r); + } catch (Exception e) { + // never crash - catch them all + if (DBG) loge("Exception trying to remove a route: " + e); + } + } + } + } + } + + return routesChanged; + } + + /** + * Reads the network specific MTU size from reources. + * and set it on it's iface. + */ + private void updateMtuSizeSettings(NetworkStateTracker nt) { + final String iface = nt.getLinkProperties().getInterfaceName(); + final int mtu = nt.getLinkProperties().getMtu(); + + if (mtu < 68 || mtu > 10000) { + loge("Unexpected mtu value: " + nt); + return; + } + + try { + if (VDBG) log("Setting MTU size: " + iface + ", " + mtu); + mNetd.setMtu(iface, mtu); + } catch (Exception e) { + Slog.e(TAG, "exception in setMtu()" + e); + } + } + + /** + * Reads the network specific TCP buffer sizes from SystemProperties + * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system + * wide use + */ + private void updateNetworkSettings(NetworkStateTracker nt) { + String key = nt.getTcpBufferSizesPropName(); + String bufferSizes = key == null ? null : SystemProperties.get(key); + + if (TextUtils.isEmpty(bufferSizes)) { + if (VDBG) log(key + " not found in system properties. Using defaults"); + + // Setting to default values so we won't be stuck to previous values + key = "net.tcp.buffersize.default"; + bufferSizes = SystemProperties.get(key); + } + + // Set values in kernel + if (bufferSizes.length() != 0) { + if (VDBG) { + log("Setting TCP values: [" + bufferSizes + + "] which comes from [" + key + "]"); + } + setBufferSize(bufferSizes); + } + } + + /** + * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max] + * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem + * + * @param bufferSizes in the format of "readMin, readInitial, readMax, + * writeMin, writeInitial, writeMax" + */ + private void setBufferSize(String bufferSizes) { + try { + String[] values = bufferSizes.split(","); + + if (values.length == 6) { + final String prefix = "/sys/kernel/ipv4/tcp_"; + FileUtils.stringToFile(prefix + "rmem_min", values[0]); + FileUtils.stringToFile(prefix + "rmem_def", values[1]); + FileUtils.stringToFile(prefix + "rmem_max", values[2]); + FileUtils.stringToFile(prefix + "wmem_min", values[3]); + FileUtils.stringToFile(prefix + "wmem_def", values[4]); + FileUtils.stringToFile(prefix + "wmem_max", values[5]); + } else { + loge("Invalid buffersize string: " + bufferSizes); + } + } catch (IOException e) { + loge("Can't set tcp buffer sizes:" + e); + } + } + + /** + * Adjust the per-process dns entries (net.dns.) based + * on the highest priority active net which this process requested. + * If there aren't any, clear it out + */ + private void reassessPidDns(int pid, boolean doBump) + { + if (VDBG) log("reassessPidDns for pid " + pid); + Integer myPid = new Integer(pid); + for(int i : mPriorityList) { + if (mNetConfigs[i].isDefault()) { + continue; + } + NetworkStateTracker nt = mNetTrackers[i]; + if (nt.getNetworkInfo().isConnected() && + !nt.isTeardownRequested()) { + LinkProperties p = nt.getLinkProperties(); + if (p == null) continue; + if (mNetRequestersPids[i].contains(myPid)) { + try { + mNetd.setDnsInterfaceForPid(p.getInterfaceName(), pid); + } catch (Exception e) { + Slog.e(TAG, "exception reasseses pid dns: " + e); + } + return; + } + } + } + // nothing found - delete + try { + mNetd.clearDnsInterfaceForPid(pid); + } catch (Exception e) { + Slog.e(TAG, "exception clear interface from pid: " + e); + } + } + + private void flushVmDnsCache() { + /* + * Tell the VMs to toss their DNS caches + */ + Intent intent = new Intent(Intent.ACTION_CLEAR_DNS_CACHE); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + /* + * Connectivity events can happen before boot has completed ... + */ + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + // Caller must grab mDnsLock. + private void updateDnsLocked(String network, String iface, + Collection dnses, String domains, boolean defaultDns) { + int last = 0; + if (dnses.size() == 0 && mDefaultDns != null) { + dnses = new ArrayList(); + dnses.add(mDefaultDns); + if (DBG) { + loge("no dns provided for " + network + " - using " + mDefaultDns.getHostAddress()); + } + } + + try { + mNetd.setDnsServersForInterface(iface, NetworkUtils.makeStrings(dnses), domains); + if (defaultDns) { + mNetd.setDefaultInterfaceForDns(iface); + } + + for (InetAddress dns : dnses) { + ++last; + String key = "net.dns" + last; + String value = dns.getHostAddress(); + SystemProperties.set(key, value); + } + for (int i = last + 1; i <= mNumDnsEntries; ++i) { + String key = "net.dns" + i; + SystemProperties.set(key, ""); + } + mNumDnsEntries = last; + } catch (Exception e) { + loge("exception setting default dns interface: " + e); + } + } + + private void handleDnsConfigurationChange(int netType) { + // add default net's dns entries + NetworkStateTracker nt = mNetTrackers[netType]; + if (nt != null && nt.getNetworkInfo().isConnected() && !nt.isTeardownRequested()) { + LinkProperties p = nt.getLinkProperties(); + if (p == null) return; + Collection dnses = p.getDnses(); + if (mNetConfigs[netType].isDefault()) { + String network = nt.getNetworkInfo().getTypeName(); + synchronized (mDnsLock) { + updateDnsLocked(network, p.getInterfaceName(), dnses, p.getDomains(), true); + } + } else { + try { + mNetd.setDnsServersForInterface(p.getInterfaceName(), + NetworkUtils.makeStrings(dnses), p.getDomains()); + } catch (Exception e) { + if (DBG) loge("exception setting dns servers: " + e); + } + // set per-pid dns for attached secondary nets + List pids = mNetRequestersPids[netType]; + for (Integer pid : pids) { + try { + mNetd.setDnsInterfaceForPid(p.getInterfaceName(), pid); + } catch (Exception e) { + Slog.e(TAG, "exception setting interface for pid: " + e); + } + } + } + flushVmDnsCache(); + } + } + + private int getRestoreDefaultNetworkDelay(int networkType) { + String restoreDefaultNetworkDelayStr = SystemProperties.get( + NETWORK_RESTORE_DELAY_PROP_NAME); + if(restoreDefaultNetworkDelayStr != null && + restoreDefaultNetworkDelayStr.length() != 0) { + try { + return Integer.valueOf(restoreDefaultNetworkDelayStr); + } catch (NumberFormatException e) { + } + } + // if the system property isn't set, use the value for the apn type + int ret = RESTORE_DEFAULT_NETWORK_DELAY; + + if ((networkType <= ConnectivityManager.MAX_NETWORK_TYPE) && + (mNetConfigs[networkType] != null)) { + ret = mNetConfigs[networkType].restoreTime; + } + return ret; + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump ConnectivityService " + + "from from pid=" + Binder.getCallingPid() + ", uid=" + + Binder.getCallingUid()); + return; + } + + // TODO: add locking to get atomic snapshot + pw.println(); + for (int i = 0; i < mNetTrackers.length; i++) { + final NetworkStateTracker nst = mNetTrackers[i]; + if (nst != null) { + pw.println("NetworkStateTracker for " + getNetworkTypeName(i) + ":"); + pw.increaseIndent(); + if (nst.getNetworkInfo().isConnected()) { + pw.println("Active network: " + nst.getNetworkInfo(). + getTypeName()); + } + pw.println(nst.getNetworkInfo()); + pw.println(nst.getLinkProperties()); + pw.println(nst); + pw.println(); + pw.decreaseIndent(); + } + } + + pw.println("Network Requester Pids:"); + pw.increaseIndent(); + for (int net : mPriorityList) { + String pidString = net + ": "; + for (Integer pid : mNetRequestersPids[net]) { + pidString = pidString + pid.toString() + ", "; + } + pw.println(pidString); + } + pw.println(); + pw.decreaseIndent(); + + pw.println("FeatureUsers:"); + pw.increaseIndent(); + for (Object requester : mFeatureUsers) { + pw.println(requester.toString()); + } + pw.println(); + pw.decreaseIndent(); + + synchronized (this) { + pw.println("NetworkTranstionWakeLock is currently " + + (mNetTransitionWakeLock.isHeld() ? "" : "not ") + "held."); + pw.println("It was last requested for "+mNetTransitionWakeLockCausedBy); + } + pw.println(); + + mTethering.dump(fd, pw, args); + + if (mInetLog != null) { + pw.println(); + pw.println("Inet condition reports:"); + pw.increaseIndent(); + for(int i = 0; i < mInetLog.size(); i++) { + pw.println(mInetLog.get(i)); + } + pw.decreaseIndent(); + } + } + + // must be stateless - things change under us. + private class NetworkStateTrackerHandler extends Handler { + public NetworkStateTrackerHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + NetworkInfo info; + switch (msg.what) { + case NetworkStateTracker.EVENT_STATE_CHANGED: { + info = (NetworkInfo) msg.obj; + NetworkInfo.State state = info.getState(); + + if (VDBG || (state == NetworkInfo.State.CONNECTED) || + (state == NetworkInfo.State.DISCONNECTED) || + (state == NetworkInfo.State.SUSPENDED)) { + log("ConnectivityChange for " + + info.getTypeName() + ": " + + state + "/" + info.getDetailedState()); + } + + // Since mobile has the notion of a network/apn that can be used for + // provisioning we need to check every time we're connected as + // CaptiveProtalTracker won't detected it because DCT doesn't report it + // as connected as ACTION_ANY_DATA_CONNECTION_STATE_CHANGED instead its + // reported as ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN. Which + // is received by MDST and sent here as EVENT_STATE_CHANGED. + if (ConnectivityManager.isNetworkTypeMobile(info.getType()) + && (0 != Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0)) + && (((state == NetworkInfo.State.CONNECTED) + && (info.getType() == ConnectivityManager.TYPE_MOBILE)) + || info.isConnectedToProvisioningNetwork())) { + log("ConnectivityChange checkMobileProvisioning for" + + " TYPE_MOBILE or ProvisioningNetwork"); + checkMobileProvisioning(CheckMp.MAX_TIMEOUT_MS); + } + + EventLogTags.writeConnectivityStateChanged( + info.getType(), info.getSubtype(), info.getDetailedState().ordinal()); + + if (info.getDetailedState() == + NetworkInfo.DetailedState.FAILED) { + handleConnectionFailure(info); + } else if (info.getDetailedState() == + DetailedState.CAPTIVE_PORTAL_CHECK) { + handleCaptivePortalTrackerCheck(info); + } else if (info.isConnectedToProvisioningNetwork()) { + /** + * TODO: Create ConnectivityManager.TYPE_MOBILE_PROVISIONING + * for now its an in between network, its a network that + * is actually a default network but we don't want it to be + * announced as such to keep background applications from + * trying to use it. It turns out that some still try so we + * take the additional step of clearing any default routes + * to the link that may have incorrectly setup by the lower + * levels. + */ + LinkProperties lp = getLinkProperties(info.getType()); + if (DBG) { + log("EVENT_STATE_CHANGED: connected to provisioning network, lp=" + lp); + } + + // Clear any default routes setup by the radio so + // any activity by applications trying to use this + // connection will fail until the provisioning network + // is enabled. + for (RouteInfo r : lp.getRoutes()) { + removeRoute(lp, r, TO_DEFAULT_TABLE); + } + } else if (state == NetworkInfo.State.DISCONNECTED) { + handleDisconnect(info); + } else if (state == NetworkInfo.State.SUSPENDED) { + // TODO: need to think this over. + // the logic here is, handle SUSPENDED the same as + // DISCONNECTED. The only difference being we are + // broadcasting an intent with NetworkInfo that's + // suspended. This allows the applications an + // opportunity to handle DISCONNECTED and SUSPENDED + // differently, or not. + handleDisconnect(info); + } else if (state == NetworkInfo.State.CONNECTED) { + handleConnect(info); + } + if (mLockdownTracker != null) { + mLockdownTracker.onNetworkInfoChanged(info); + } + break; + } + case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED: { + info = (NetworkInfo) msg.obj; + // TODO: Temporary allowing network configuration + // change not resetting sockets. + // @see bug/4455071 + handleConnectivityChange(info.getType(), false); + break; + } + case NetworkStateTracker.EVENT_NETWORK_SUBTYPE_CHANGED: { + info = (NetworkInfo) msg.obj; + int type = info.getType(); + updateNetworkSettings(mNetTrackers[type]); + break; + } + } + } + } + + private class InternalHandler extends Handler { + public InternalHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + NetworkInfo info; + switch (msg.what) { + case EVENT_CLEAR_NET_TRANSITION_WAKELOCK: { + String causedBy = null; + synchronized (ConnectivityService.this) { + if (msg.arg1 == mNetTransitionWakeLockSerialNumber && + mNetTransitionWakeLock.isHeld()) { + mNetTransitionWakeLock.release(); + causedBy = mNetTransitionWakeLockCausedBy; + } + } + if (causedBy != null) { + log("NetTransition Wakelock for " + causedBy + " released by timeout"); + } + break; + } + case EVENT_RESTORE_DEFAULT_NETWORK: { + FeatureUser u = (FeatureUser)msg.obj; + u.expire(); + break; + } + case EVENT_INET_CONDITION_CHANGE: { + int netType = msg.arg1; + int condition = msg.arg2; + handleInetConditionChange(netType, condition); + break; + } + case EVENT_INET_CONDITION_HOLD_END: { + int netType = msg.arg1; + int sequence = msg.arg2; + handleInetConditionHoldEnd(netType, sequence); + break; + } + case EVENT_SET_NETWORK_PREFERENCE: { + int preference = msg.arg1; + handleSetNetworkPreference(preference); + break; + } + case EVENT_SET_MOBILE_DATA: { + boolean enabled = (msg.arg1 == ENABLED); + handleSetMobileData(enabled); + break; + } + case EVENT_APPLY_GLOBAL_HTTP_PROXY: { + handleDeprecatedGlobalHttpProxy(); + break; + } + case EVENT_SET_DEPENDENCY_MET: { + boolean met = (msg.arg1 == ENABLED); + handleSetDependencyMet(msg.arg2, met); + break; + } + case EVENT_SEND_STICKY_BROADCAST_INTENT: { + Intent intent = (Intent)msg.obj; + sendStickyBroadcast(intent); + break; + } + case EVENT_SET_POLICY_DATA_ENABLE: { + final int networkType = msg.arg1; + final boolean enabled = msg.arg2 == ENABLED; + handleSetPolicyDataEnable(networkType, enabled); + break; + } + case EVENT_VPN_STATE_CHANGED: { + if (mLockdownTracker != null) { + mLockdownTracker.onVpnStateChanged((NetworkInfo) msg.obj); + } + break; + } + case EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: { + int tag = mEnableFailFastMobileDataTag.get(); + if (msg.arg1 == tag) { + MobileDataStateTracker mobileDst = + (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE]; + if (mobileDst != null) { + mobileDst.setEnableFailFastMobileData(msg.arg2); + } + } else { + log("EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: stale arg1:" + msg.arg1 + + " != tag:" + tag); + } + break; + } + case EVENT_SAMPLE_INTERVAL_ELAPSED: { + handleNetworkSamplingTimeout(); + break; + } + case EVENT_PROXY_HAS_CHANGED: { + handleApplyDefaultProxy((ProxyProperties)msg.obj); + break; + } + } + } + } + + // javadoc from interface + public int tether(String iface) { + enforceTetherChangePermission(); + + if (isTetheringSupported()) { + return mTethering.tether(iface); + } else { + return ConnectivityManager.TETHER_ERROR_UNSUPPORTED; + } + } + + // javadoc from interface + public int untether(String iface) { + enforceTetherChangePermission(); + + if (isTetheringSupported()) { + return mTethering.untether(iface); + } else { + return ConnectivityManager.TETHER_ERROR_UNSUPPORTED; + } + } + + // javadoc from interface + public int getLastTetherError(String iface) { + enforceTetherAccessPermission(); + + if (isTetheringSupported()) { + return mTethering.getLastTetherError(iface); + } else { + return ConnectivityManager.TETHER_ERROR_UNSUPPORTED; + } + } + + // TODO - proper iface API for selection by property, inspection, etc + public String[] getTetherableUsbRegexs() { + enforceTetherAccessPermission(); + if (isTetheringSupported()) { + return mTethering.getTetherableUsbRegexs(); + } else { + return new String[0]; + } + } + + public String[] getTetherableWifiRegexs() { + enforceTetherAccessPermission(); + if (isTetheringSupported()) { + return mTethering.getTetherableWifiRegexs(); + } else { + return new String[0]; + } + } + + public String[] getTetherableBluetoothRegexs() { + enforceTetherAccessPermission(); + if (isTetheringSupported()) { + return mTethering.getTetherableBluetoothRegexs(); + } else { + return new String[0]; + } + } + + public int setUsbTethering(boolean enable) { + enforceTetherChangePermission(); + if (isTetheringSupported()) { + return mTethering.setUsbTethering(enable); + } else { + return ConnectivityManager.TETHER_ERROR_UNSUPPORTED; + } + } + + // TODO - move iface listing, queries, etc to new module + // javadoc from interface + public String[] getTetherableIfaces() { + enforceTetherAccessPermission(); + return mTethering.getTetherableIfaces(); + } + + public String[] getTetheredIfaces() { + enforceTetherAccessPermission(); + return mTethering.getTetheredIfaces(); + } + + public String[] getTetheringErroredIfaces() { + enforceTetherAccessPermission(); + return mTethering.getErroredIfaces(); + } + + // if ro.tether.denied = true we default to no tethering + // gservices could set the secure setting to 1 though to enable it on a build where it + // had previously been turned off. + public boolean isTetheringSupported() { + enforceTetherAccessPermission(); + int defaultVal = (SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1); + boolean tetherEnabledInSettings = (Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.TETHER_SUPPORTED, defaultVal) != 0); + return tetherEnabledInSettings && ((mTethering.getTetherableUsbRegexs().length != 0 || + mTethering.getTetherableWifiRegexs().length != 0 || + mTethering.getTetherableBluetoothRegexs().length != 0) && + mTethering.getUpstreamIfaceTypes().length != 0); + } + + // An API NetworkStateTrackers can call when they lose their network. + // This will automatically be cleared after X seconds or a network becomes CONNECTED, + // whichever happens first. The timer is started by the first caller and not + // restarted by subsequent callers. + public void requestNetworkTransitionWakelock(String forWhom) { + enforceConnectivityInternalPermission(); + synchronized (this) { + if (mNetTransitionWakeLock.isHeld()) return; + mNetTransitionWakeLockSerialNumber++; + mNetTransitionWakeLock.acquire(); + mNetTransitionWakeLockCausedBy = forWhom; + } + mHandler.sendMessageDelayed(mHandler.obtainMessage( + EVENT_CLEAR_NET_TRANSITION_WAKELOCK, + mNetTransitionWakeLockSerialNumber, 0), + mNetTransitionWakeLockTimeout); + return; + } + + // 100 percent is full good, 0 is full bad. + public void reportInetCondition(int networkType, int percentage) { + if (VDBG) log("reportNetworkCondition(" + networkType + ", " + percentage + ")"); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.STATUS_BAR, + "ConnectivityService"); + + if (DBG) { + int pid = getCallingPid(); + int uid = getCallingUid(); + String s = pid + "(" + uid + ") reports inet is " + + (percentage > 50 ? "connected" : "disconnected") + " (" + percentage + ") on " + + "network Type " + networkType + " at " + GregorianCalendar.getInstance().getTime(); + mInetLog.add(s); + while(mInetLog.size() > INET_CONDITION_LOG_MAX_SIZE) { + mInetLog.remove(0); + } + } + mHandler.sendMessage(mHandler.obtainMessage( + EVENT_INET_CONDITION_CHANGE, networkType, percentage)); + } + + private void handleInetConditionChange(int netType, int condition) { + if (mActiveDefaultNetwork == -1) { + if (DBG) log("handleInetConditionChange: no active default network - ignore"); + return; + } + if (mActiveDefaultNetwork != netType) { + if (DBG) log("handleInetConditionChange: net=" + netType + + " != default=" + mActiveDefaultNetwork + " - ignore"); + return; + } + if (VDBG) { + log("handleInetConditionChange: net=" + + netType + ", condition=" + condition + + ",mActiveDefaultNetwork=" + mActiveDefaultNetwork); + } + mDefaultInetCondition = condition; + int delay; + if (mInetConditionChangeInFlight == false) { + if (VDBG) log("handleInetConditionChange: starting a change hold"); + // setup a new hold to debounce this + if (mDefaultInetCondition > 50) { + delay = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.INET_CONDITION_DEBOUNCE_UP_DELAY, 500); + } else { + delay = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.INET_CONDITION_DEBOUNCE_DOWN_DELAY, 3000); + } + mInetConditionChangeInFlight = true; + mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_INET_CONDITION_HOLD_END, + mActiveDefaultNetwork, mDefaultConnectionSequence), delay); + } else { + // we've set the new condition, when this hold ends that will get picked up + if (VDBG) log("handleInetConditionChange: currently in hold - not setting new end evt"); + } + } + + private void handleInetConditionHoldEnd(int netType, int sequence) { + if (DBG) { + log("handleInetConditionHoldEnd: net=" + netType + + ", condition=" + mDefaultInetCondition + + ", published condition=" + mDefaultInetConditionPublished); + } + mInetConditionChangeInFlight = false; + + if (mActiveDefaultNetwork == -1) { + if (DBG) log("handleInetConditionHoldEnd: no active default network - ignoring"); + return; + } + if (mDefaultConnectionSequence != sequence) { + if (DBG) log("handleInetConditionHoldEnd: event hold for obsolete network - ignoring"); + return; + } + // TODO: Figure out why this optimization sometimes causes a + // change in mDefaultInetCondition to be missed and the + // UI to not be updated. + //if (mDefaultInetConditionPublished == mDefaultInetCondition) { + // if (DBG) log("no change in condition - aborting"); + // return; + //} + NetworkInfo networkInfo = mNetTrackers[mActiveDefaultNetwork].getNetworkInfo(); + if (networkInfo.isConnected() == false) { + if (DBG) log("handleInetConditionHoldEnd: default network not connected - ignoring"); + return; + } + mDefaultInetConditionPublished = mDefaultInetCondition; + sendInetConditionBroadcast(networkInfo); + return; + } + + public ProxyProperties getProxy() { + // this information is already available as a world read/writable jvm property + // so this API change wouldn't have a benifit. It also breaks the passing + // of proxy info to all the JVMs. + // enforceAccessPermission(); + synchronized (mProxyLock) { + ProxyProperties ret = mGlobalProxy; + if ((ret == null) && !mDefaultProxyDisabled) ret = mDefaultProxy; + return ret; + } + } + + public void setGlobalProxy(ProxyProperties proxyProperties) { + enforceConnectivityInternalPermission(); + + synchronized (mProxyLock) { + if (proxyProperties == mGlobalProxy) return; + if (proxyProperties != null && proxyProperties.equals(mGlobalProxy)) return; + if (mGlobalProxy != null && mGlobalProxy.equals(proxyProperties)) return; + + String host = ""; + int port = 0; + String exclList = ""; + String pacFileUrl = ""; + if (proxyProperties != null && (!TextUtils.isEmpty(proxyProperties.getHost()) || + !TextUtils.isEmpty(proxyProperties.getPacFileUrl()))) { + if (!proxyProperties.isValid()) { + if (DBG) + log("Invalid proxy properties, ignoring: " + proxyProperties.toString()); + return; + } + mGlobalProxy = new ProxyProperties(proxyProperties); + host = mGlobalProxy.getHost(); + port = mGlobalProxy.getPort(); + exclList = mGlobalProxy.getExclusionList(); + if (proxyProperties.getPacFileUrl() != null) { + pacFileUrl = proxyProperties.getPacFileUrl(); + } + } else { + mGlobalProxy = null; + } + ContentResolver res = mContext.getContentResolver(); + final long token = Binder.clearCallingIdentity(); + try { + Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST, host); + Settings.Global.putInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, port); + Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST, + exclList); + Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_PAC, pacFileUrl); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + if (mGlobalProxy == null) { + proxyProperties = mDefaultProxy; + } + sendProxyBroadcast(proxyProperties); + } + + private void loadGlobalProxy() { + ContentResolver res = mContext.getContentResolver(); + String host = Settings.Global.getString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST); + int port = Settings.Global.getInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, 0); + String exclList = Settings.Global.getString(res, + Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST); + String pacFileUrl = Settings.Global.getString(res, Settings.Global.GLOBAL_HTTP_PROXY_PAC); + if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) { + ProxyProperties proxyProperties; + if (!TextUtils.isEmpty(pacFileUrl)) { + proxyProperties = new ProxyProperties(pacFileUrl); + } else { + proxyProperties = new ProxyProperties(host, port, exclList); + } + if (!proxyProperties.isValid()) { + if (DBG) log("Invalid proxy properties, ignoring: " + proxyProperties.toString()); + return; + } + + synchronized (mProxyLock) { + mGlobalProxy = proxyProperties; + } + } + } + + public ProxyProperties getGlobalProxy() { + // this information is already available as a world read/writable jvm property + // so this API change wouldn't have a benifit. It also breaks the passing + // of proxy info to all the JVMs. + // enforceAccessPermission(); + synchronized (mProxyLock) { + return mGlobalProxy; + } + } + + private void handleApplyDefaultProxy(ProxyProperties proxy) { + if (proxy != null && TextUtils.isEmpty(proxy.getHost()) + && TextUtils.isEmpty(proxy.getPacFileUrl())) { + proxy = null; + } + synchronized (mProxyLock) { + if (mDefaultProxy != null && mDefaultProxy.equals(proxy)) return; + if (mDefaultProxy == proxy) return; // catches repeated nulls + if (proxy != null && !proxy.isValid()) { + if (DBG) log("Invalid proxy properties, ignoring: " + proxy.toString()); + return; + } + mDefaultProxy = proxy; + + if (mGlobalProxy != null) return; + if (!mDefaultProxyDisabled) { + sendProxyBroadcast(proxy); + } + } + } + + private void handleDeprecatedGlobalHttpProxy() { + String proxy = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.HTTP_PROXY); + if (!TextUtils.isEmpty(proxy)) { + String data[] = proxy.split(":"); + if (data.length == 0) { + return; + } + + String proxyHost = data[0]; + int proxyPort = 8080; + if (data.length > 1) { + try { + proxyPort = Integer.parseInt(data[1]); + } catch (NumberFormatException e) { + return; + } + } + ProxyProperties p = new ProxyProperties(data[0], proxyPort, ""); + setGlobalProxy(p); + } + } + + private void sendProxyBroadcast(ProxyProperties proxy) { + if (proxy == null) proxy = new ProxyProperties("", 0, ""); + if (mPacManager.setCurrentProxyScriptUrl(proxy)) return; + if (DBG) log("sending Proxy Broadcast for " + proxy); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING | + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxy); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private static class SettingsObserver extends ContentObserver { + private int mWhat; + private Handler mHandler; + SettingsObserver(Handler handler, int what) { + super(handler); + mHandler = handler; + mWhat = what; + } + + void observe(Context context) { + ContentResolver resolver = context.getContentResolver(); + resolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.HTTP_PROXY), false, this); + } + + @Override + public void onChange(boolean selfChange) { + mHandler.obtainMessage(mWhat).sendToTarget(); + } + } + + private static void log(String s) { + Slog.d(TAG, s); + } + + private static void loge(String s) { + Slog.e(TAG, s); + } + + int convertFeatureToNetworkType(int networkType, String feature) { + int usedNetworkType = networkType; + + if(networkType == ConnectivityManager.TYPE_MOBILE) { + if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_MMS; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_SUPL)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_SUPL; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_DUN) || + TextUtils.equals(feature, Phone.FEATURE_ENABLE_DUN_ALWAYS)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_DUN; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_HIPRI)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_HIPRI; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_FOTA)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_FOTA; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_IMS)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_IMS; + } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_CBS)) { + usedNetworkType = ConnectivityManager.TYPE_MOBILE_CBS; + } else { + Slog.e(TAG, "Can't match any mobile netTracker!"); + } + } else if (networkType == ConnectivityManager.TYPE_WIFI) { + if (TextUtils.equals(feature, "p2p")) { + usedNetworkType = ConnectivityManager.TYPE_WIFI_P2P; + } else { + Slog.e(TAG, "Can't match any wifi netTracker!"); + } + } else { + Slog.e(TAG, "Unexpected network type"); + } + return usedNetworkType; + } + + private static T checkNotNull(T value, String message) { + if (value == null) { + throw new NullPointerException(message); + } + return value; + } + + /** + * Protect a socket from VPN routing rules. This method is used by + * VpnBuilder and not available in ConnectivityManager. Permissions + * are checked in Vpn class. + * @hide + */ + @Override + public boolean protectVpn(ParcelFileDescriptor socket) { + throwIfLockdownEnabled(); + try { + int type = mActiveDefaultNetwork; + int user = UserHandle.getUserId(Binder.getCallingUid()); + if (ConnectivityManager.isNetworkTypeValid(type) && mNetTrackers[type] != null) { + synchronized(mVpns) { + mVpns.get(user).protect(socket, + mNetTrackers[type].getLinkProperties().getInterfaceName()); + } + return true; + } + } catch (Exception e) { + // ignore + } finally { + try { + socket.close(); + } catch (Exception e) { + // ignore + } + } + return false; + } + + /** + * Prepare for a VPN application. This method is used by VpnDialogs + * and not available in ConnectivityManager. Permissions are checked + * in Vpn class. + * @hide + */ + @Override + public boolean prepareVpn(String oldPackage, String newPackage) { + throwIfLockdownEnabled(); + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + return mVpns.get(user).prepare(oldPackage, newPackage); + } + } + + @Override + public void markSocketAsUser(ParcelFileDescriptor socket, int uid) { + enforceMarkNetworkSocketPermission(); + final long token = Binder.clearCallingIdentity(); + try { + int mark = mNetd.getMarkForUid(uid); + // Clear the mark on the socket if no mark is needed to prevent socket reuse issues + if (mark == -1) { + mark = 0; + } + NetworkUtils.markSocket(socket.getFd(), mark); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Configure a TUN interface and return its file descriptor. Parameters + * are encoded and opaque to this class. This method is used by VpnBuilder + * and not available in ConnectivityManager. Permissions are checked in + * Vpn class. + * @hide + */ + @Override + public ParcelFileDescriptor establishVpn(VpnConfig config) { + throwIfLockdownEnabled(); + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + return mVpns.get(user).establish(config); + } + } + + /** + * Start legacy VPN, controlling native daemons as needed. Creates a + * secondary thread to perform connection work, returning quickly. + */ + @Override + public void startLegacyVpn(VpnProfile profile) { + throwIfLockdownEnabled(); + final LinkProperties egress = getActiveLinkProperties(); + if (egress == null) { + throw new IllegalStateException("Missing active network connection"); + } + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + mVpns.get(user).startLegacyVpn(profile, mKeyStore, egress); + } + } + + /** + * Return the information of the ongoing legacy VPN. This method is used + * by VpnSettings and not available in ConnectivityManager. Permissions + * are checked in Vpn class. + * @hide + */ + @Override + public LegacyVpnInfo getLegacyVpnInfo() { + throwIfLockdownEnabled(); + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + return mVpns.get(user).getLegacyVpnInfo(); + } + } + + /** + * Returns the information of the ongoing VPN. This method is used by VpnDialogs and + * not available in ConnectivityManager. + * Permissions are checked in Vpn class. + * @hide + */ + @Override + public VpnConfig getVpnConfig() { + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + return mVpns.get(user).getVpnConfig(); + } + } + + /** + * Callback for VPN subsystem. Currently VPN is not adapted to the service + * through NetworkStateTracker since it works differently. For example, it + * needs to override DNS servers but never takes the default routes. It + * relies on another data network, and it could keep existing connections + * alive after reconnecting, switching between networks, or even resuming + * from deep sleep. Calls from applications should be done synchronously + * to avoid race conditions. As these are all hidden APIs, refactoring can + * be done whenever a better abstraction is developed. + */ + public class VpnCallback { + private VpnCallback() { + } + + public void onStateChanged(NetworkInfo info) { + mHandler.obtainMessage(EVENT_VPN_STATE_CHANGED, info).sendToTarget(); + } + + public void override(String iface, List dnsServers, List searchDomains) { + if (dnsServers == null) { + restore(); + return; + } + + // Convert DNS servers into addresses. + List addresses = new ArrayList(); + for (String address : dnsServers) { + // Double check the addresses and remove invalid ones. + try { + addresses.add(InetAddress.parseNumericAddress(address)); + } catch (Exception e) { + // ignore + } + } + if (addresses.isEmpty()) { + restore(); + return; + } + + // Concatenate search domains into a string. + StringBuilder buffer = new StringBuilder(); + if (searchDomains != null) { + for (String domain : searchDomains) { + buffer.append(domain).append(' '); + } + } + String domains = buffer.toString().trim(); + + // Apply DNS changes. + synchronized (mDnsLock) { + updateDnsLocked("VPN", iface, addresses, domains, false); + } + + // Temporarily disable the default proxy (not global). + synchronized (mProxyLock) { + mDefaultProxyDisabled = true; + if (mGlobalProxy == null && mDefaultProxy != null) { + sendProxyBroadcast(null); + } + } + + // TODO: support proxy per network. + } + + public void restore() { + synchronized (mProxyLock) { + mDefaultProxyDisabled = false; + if (mGlobalProxy == null && mDefaultProxy != null) { + sendProxyBroadcast(mDefaultProxy); + } + } + } + + public void protect(ParcelFileDescriptor socket) { + try { + final int mark = mNetd.getMarkForProtect(); + NetworkUtils.markSocket(socket.getFd(), mark); + } catch (RemoteException e) { + } + } + + public void setRoutes(String interfaze, List routes) { + for (RouteInfo route : routes) { + try { + mNetd.setMarkedForwardingRoute(interfaze, route); + } catch (RemoteException e) { + } + } + } + + public void setMarkedForwarding(String interfaze) { + try { + mNetd.setMarkedForwarding(interfaze); + } catch (RemoteException e) { + } + } + + public void clearMarkedForwarding(String interfaze) { + try { + mNetd.clearMarkedForwarding(interfaze); + } catch (RemoteException e) { + } + } + + public void addUserForwarding(String interfaze, int uid, boolean forwardDns) { + int uidStart = uid * UserHandle.PER_USER_RANGE; + int uidEnd = uidStart + UserHandle.PER_USER_RANGE - 1; + addUidForwarding(interfaze, uidStart, uidEnd, forwardDns); + } + + public void clearUserForwarding(String interfaze, int uid, boolean forwardDns) { + int uidStart = uid * UserHandle.PER_USER_RANGE; + int uidEnd = uidStart + UserHandle.PER_USER_RANGE - 1; + clearUidForwarding(interfaze, uidStart, uidEnd, forwardDns); + } + + public void addUidForwarding(String interfaze, int uidStart, int uidEnd, + boolean forwardDns) { + try { + mNetd.setUidRangeRoute(interfaze,uidStart, uidEnd); + if (forwardDns) mNetd.setDnsInterfaceForUidRange(interfaze, uidStart, uidEnd); + } catch (RemoteException e) { + } + + } + + public void clearUidForwarding(String interfaze, int uidStart, int uidEnd, + boolean forwardDns) { + try { + mNetd.clearUidRangeRoute(interfaze, uidStart, uidEnd); + if (forwardDns) mNetd.clearDnsInterfaceForUidRange(uidStart, uidEnd); + } catch (RemoteException e) { + } + + } + } + + @Override + public boolean updateLockdownVpn() { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + Slog.w(TAG, "Lockdown VPN only available to AID_SYSTEM"); + return false; + } + + // Tear down existing lockdown if profile was removed + mLockdownEnabled = LockdownVpnTracker.isEnabled(); + if (mLockdownEnabled) { + if (!mKeyStore.isUnlocked()) { + Slog.w(TAG, "KeyStore locked; unable to create LockdownTracker"); + return false; + } + + final String profileName = new String(mKeyStore.get(Credentials.LOCKDOWN_VPN)); + final VpnProfile profile = VpnProfile.decode( + profileName, mKeyStore.get(Credentials.VPN + profileName)); + int user = UserHandle.getUserId(Binder.getCallingUid()); + synchronized(mVpns) { + setLockdownTracker(new LockdownVpnTracker(mContext, mNetd, this, mVpns.get(user), + profile)); + } + } else { + setLockdownTracker(null); + } + + return true; + } + + /** + * Internally set new {@link LockdownVpnTracker}, shutting down any existing + * {@link LockdownVpnTracker}. Can be {@code null} to disable lockdown. + */ + private void setLockdownTracker(LockdownVpnTracker tracker) { + // Shutdown any existing tracker + final LockdownVpnTracker existing = mLockdownTracker; + mLockdownTracker = null; + if (existing != null) { + existing.shutdown(); + } + + try { + if (tracker != null) { + mNetd.setFirewallEnabled(true); + mNetd.setFirewallInterfaceRule("lo", true); + mLockdownTracker = tracker; + mLockdownTracker.init(); + } else { + mNetd.setFirewallEnabled(false); + } + } catch (RemoteException e) { + // ignored; NMS lives inside system_server + } + } + + private void throwIfLockdownEnabled() { + if (mLockdownEnabled) { + throw new IllegalStateException("Unavailable in lockdown mode"); + } + } + + public void supplyMessenger(int networkType, Messenger messenger) { + enforceConnectivityInternalPermission(); + + if (isNetworkTypeValid(networkType) && mNetTrackers[networkType] != null) { + mNetTrackers[networkType].supplyMessenger(messenger); + } + } + + public int findConnectionTypeForIface(String iface) { + enforceConnectivityInternalPermission(); + + if (TextUtils.isEmpty(iface)) return ConnectivityManager.TYPE_NONE; + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + LinkProperties lp = tracker.getLinkProperties(); + if (lp != null && iface.equals(lp.getInterfaceName())) { + return tracker.getNetworkInfo().getType(); + } + } + } + return ConnectivityManager.TYPE_NONE; + } + + /** + * Have mobile data fail fast if enabled. + * + * @param enabled DctConstants.ENABLED/DISABLED + */ + private void setEnableFailFastMobileData(int enabled) { + int tag; + + if (enabled == DctConstants.ENABLED) { + tag = mEnableFailFastMobileDataTag.incrementAndGet(); + } else { + tag = mEnableFailFastMobileDataTag.get(); + } + mHandler.sendMessage(mHandler.obtainMessage(EVENT_ENABLE_FAIL_FAST_MOBILE_DATA, tag, + enabled)); + } + + private boolean isMobileDataStateTrackerReady() { + MobileDataStateTracker mdst = + (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI]; + return (mdst != null) && (mdst.isReady()); + } + + /** + * The ResultReceiver resultCode for checkMobileProvisioning (CMP_RESULT_CODE) + */ + + /** + * No connection was possible to the network. + * This is NOT a warm sim. + */ + private static final int CMP_RESULT_CODE_NO_CONNECTION = 0; + + /** + * A connection was made to the internet, all is well. + * This is NOT a warm sim. + */ + private static final int CMP_RESULT_CODE_CONNECTABLE = 1; + + /** + * A connection was made but no dns server was available to resolve a name to address. + * This is NOT a warm sim since provisioning network is supported. + */ + private static final int CMP_RESULT_CODE_NO_DNS = 2; + + /** + * A connection was made but could not open a TCP connection. + * This is NOT a warm sim since provisioning network is supported. + */ + private static final int CMP_RESULT_CODE_NO_TCP_CONNECTION = 3; + + /** + * A connection was made but there was a redirection, we appear to be in walled garden. + * This is an indication of a warm sim on a mobile network such as T-Mobile. + */ + private static final int CMP_RESULT_CODE_REDIRECTED = 4; + + /** + * The mobile network is a provisioning network. + * This is an indication of a warm sim on a mobile network such as AT&T. + */ + private static final int CMP_RESULT_CODE_PROVISIONING_NETWORK = 5; + + private AtomicBoolean mIsCheckingMobileProvisioning = new AtomicBoolean(false); + + @Override + public int checkMobileProvisioning(int suggestedTimeOutMs) { + int timeOutMs = -1; + if (DBG) log("checkMobileProvisioning: E suggestedTimeOutMs=" + suggestedTimeOutMs); + enforceConnectivityInternalPermission(); + + final long token = Binder.clearCallingIdentity(); + try { + timeOutMs = suggestedTimeOutMs; + if (suggestedTimeOutMs > CheckMp.MAX_TIMEOUT_MS) { + timeOutMs = CheckMp.MAX_TIMEOUT_MS; + } + + // Check that mobile networks are supported + if (!isNetworkSupported(ConnectivityManager.TYPE_MOBILE) + || !isNetworkSupported(ConnectivityManager.TYPE_MOBILE_HIPRI)) { + if (DBG) log("checkMobileProvisioning: X no mobile network"); + return timeOutMs; + } + + // If we're already checking don't do it again + // TODO: Add a queue of results... + if (mIsCheckingMobileProvisioning.getAndSet(true)) { + if (DBG) log("checkMobileProvisioning: X already checking ignore for the moment"); + return timeOutMs; + } + + // Start off with mobile notification off + setProvNotificationVisible(false, ConnectivityManager.TYPE_MOBILE_HIPRI, null, null); + + CheckMp checkMp = new CheckMp(mContext, this); + CheckMp.CallBack cb = new CheckMp.CallBack() { + @Override + void onComplete(Integer result) { + if (DBG) log("CheckMp.onComplete: result=" + result); + NetworkInfo ni = + mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI].getNetworkInfo(); + switch(result) { + case CMP_RESULT_CODE_CONNECTABLE: + case CMP_RESULT_CODE_NO_CONNECTION: + case CMP_RESULT_CODE_NO_DNS: + case CMP_RESULT_CODE_NO_TCP_CONNECTION: { + if (DBG) log("CheckMp.onComplete: ignore, connected or no connection"); + break; + } + case CMP_RESULT_CODE_REDIRECTED: { + if (DBG) log("CheckMp.onComplete: warm sim"); + String url = getMobileProvisioningUrl(); + if (TextUtils.isEmpty(url)) { + url = getMobileRedirectedProvisioningUrl(); + } + if (TextUtils.isEmpty(url) == false) { + if (DBG) log("CheckMp.onComplete: warm (redirected), url=" + url); + setProvNotificationVisible(true, + ConnectivityManager.TYPE_MOBILE_HIPRI, ni.getExtraInfo(), + url); + } else { + if (DBG) log("CheckMp.onComplete: warm (redirected), no url"); + } + break; + } + case CMP_RESULT_CODE_PROVISIONING_NETWORK: { + String url = getMobileProvisioningUrl(); + if (TextUtils.isEmpty(url) == false) { + if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), url=" + url); + setProvNotificationVisible(true, + ConnectivityManager.TYPE_MOBILE_HIPRI, ni.getExtraInfo(), + url); + } else { + if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), no url"); + } + break; + } + default: { + loge("CheckMp.onComplete: ignore unexpected result=" + result); + break; + } + } + mIsCheckingMobileProvisioning.set(false); + } + }; + CheckMp.Params params = + new CheckMp.Params(checkMp.getDefaultUrl(), timeOutMs, cb); + if (DBG) log("checkMobileProvisioning: params=" + params); + checkMp.execute(params); + } finally { + Binder.restoreCallingIdentity(token); + if (DBG) log("checkMobileProvisioning: X"); + } + return timeOutMs; + } + + static class CheckMp extends + AsyncTask { + private static final String CHECKMP_TAG = "CheckMp"; + + // adb shell setprop persist.checkmp.testfailures 1 to enable testing failures + private static boolean mTestingFailures; + + // Choosing 4 loops as half of them will use HTTPS and the other half HTTP + private static final int MAX_LOOPS = 4; + + // Number of milli-seconds to complete all of the retires + public static final int MAX_TIMEOUT_MS = 60000; + + // The socket should retry only 5 seconds, the default is longer + private static final int SOCKET_TIMEOUT_MS = 5000; + + // Sleep time for network errors + private static final int NET_ERROR_SLEEP_SEC = 3; + + // Sleep time for network route establishment + private static final int NET_ROUTE_ESTABLISHMENT_SLEEP_SEC = 3; + + // Short sleep time for polling :( + private static final int POLLING_SLEEP_SEC = 1; + + private Context mContext; + private ConnectivityService mCs; + private TelephonyManager mTm; + private Params mParams; + + /** + * Parameters for AsyncTask.execute + */ + static class Params { + private String mUrl; + private long mTimeOutMs; + private CallBack mCb; + + Params(String url, long timeOutMs, CallBack cb) { + mUrl = url; + mTimeOutMs = timeOutMs; + mCb = cb; + } + + @Override + public String toString() { + return "{" + " url=" + mUrl + " mTimeOutMs=" + mTimeOutMs + " mCb=" + mCb + "}"; + } + } + + // As explained to me by Brian Carlstrom and Kenny Root, Certificates can be + // issued by name or ip address, for Google its by name so when we construct + // this HostnameVerifier we'll pass the original Uri and use it to verify + // the host. If the host name in the original uril fails we'll test the + // hostname parameter just incase things change. + static class CheckMpHostnameVerifier implements HostnameVerifier { + Uri mOrgUri; + + CheckMpHostnameVerifier(Uri orgUri) { + mOrgUri = orgUri; + } + + @Override + public boolean verify(String hostname, SSLSession session) { + HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); + String orgUriHost = mOrgUri.getHost(); + boolean retVal = hv.verify(orgUriHost, session) || hv.verify(hostname, session); + if (DBG) { + log("isMobileOk: hostnameVerify retVal=" + retVal + " hostname=" + hostname + + " orgUriHost=" + orgUriHost); + } + return retVal; + } + } + + /** + * The call back object passed in Params. onComplete will be called + * on the main thread. + */ + abstract static class CallBack { + // Called on the main thread. + abstract void onComplete(Integer result); + } + + public CheckMp(Context context, ConnectivityService cs) { + if (Build.IS_DEBUGGABLE) { + mTestingFailures = + SystemProperties.getInt("persist.checkmp.testfailures", 0) == 1; + } else { + mTestingFailures = false; + } + + mContext = context; + mCs = cs; + + // Setup access to TelephonyService we'll be using. + mTm = (TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE); + } + + /** + * Get the default url to use for the test. + */ + public String getDefaultUrl() { + // See http://go/clientsdns for usage approval + String server = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.CAPTIVE_PORTAL_SERVER); + if (server == null) { + server = "clients3.google.com"; + } + return "http://" + server + "/generate_204"; + } + + /** + * Detect if its possible to connect to the http url. DNS based detection techniques + * do not work at all hotspots. The best way to check is to perform a request to + * a known address that fetches the data we expect. + */ + private synchronized Integer isMobileOk(Params params) { + Integer result = CMP_RESULT_CODE_NO_CONNECTION; + Uri orgUri = Uri.parse(params.mUrl); + Random rand = new Random(); + mParams = params; + + if (mCs.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) { + result = CMP_RESULT_CODE_NO_CONNECTION; + log("isMobileOk: X not mobile capable result=" + result); + return result; + } + + // See if we've already determined we've got a provisioning connection, + // if so we don't need to do anything active. + MobileDataStateTracker mdstDefault = (MobileDataStateTracker) + mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE]; + boolean isDefaultProvisioning = mdstDefault.isProvisioningNetwork(); + log("isMobileOk: isDefaultProvisioning=" + isDefaultProvisioning); + + MobileDataStateTracker mdstHipri = (MobileDataStateTracker) + mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI]; + boolean isHipriProvisioning = mdstHipri.isProvisioningNetwork(); + log("isMobileOk: isHipriProvisioning=" + isHipriProvisioning); + + if (isDefaultProvisioning || isHipriProvisioning) { + result = CMP_RESULT_CODE_PROVISIONING_NETWORK; + log("isMobileOk: X default || hipri is provisioning result=" + result); + return result; + } + + try { + // Continue trying to connect until time has run out + long endTime = SystemClock.elapsedRealtime() + params.mTimeOutMs; + + if (!mCs.isMobileDataStateTrackerReady()) { + // Wait for MobileDataStateTracker to be ready. + if (DBG) log("isMobileOk: mdst is not ready"); + while(SystemClock.elapsedRealtime() < endTime) { + if (mCs.isMobileDataStateTrackerReady()) { + // Enable fail fast as we'll do retries here and use a + // hipri connection so the default connection stays active. + if (DBG) log("isMobileOk: mdst ready, enable fail fast of mobile data"); + mCs.setEnableFailFastMobileData(DctConstants.ENABLED); + break; + } + sleep(POLLING_SLEEP_SEC); + } + } + + log("isMobileOk: start hipri url=" + params.mUrl); + + // First wait until we can start using hipri + Binder binder = new Binder(); + while(SystemClock.elapsedRealtime() < endTime) { + int ret = mCs.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, + Phone.FEATURE_ENABLE_HIPRI, binder); + if ((ret == PhoneConstants.APN_ALREADY_ACTIVE) + || (ret == PhoneConstants.APN_REQUEST_STARTED)) { + log("isMobileOk: hipri started"); + break; + } + if (VDBG) log("isMobileOk: hipri not started yet"); + result = CMP_RESULT_CODE_NO_CONNECTION; + sleep(POLLING_SLEEP_SEC); + } + + // Continue trying to connect until time has run out + while(SystemClock.elapsedRealtime() < endTime) { + try { + // Wait for hipri to connect. + // TODO: Don't poll and handle situation where hipri fails + // because default is retrying. See b/9569540 + NetworkInfo.State state = mCs + .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState(); + if (state != NetworkInfo.State.CONNECTED) { + if (true/*VDBG*/) { + log("isMobileOk: not connected ni=" + + mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI)); + } + sleep(POLLING_SLEEP_SEC); + result = CMP_RESULT_CODE_NO_CONNECTION; + continue; + } + + // Hipri has started check if this is a provisioning url + MobileDataStateTracker mdst = (MobileDataStateTracker) + mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI]; + if (mdst.isProvisioningNetwork()) { + result = CMP_RESULT_CODE_PROVISIONING_NETWORK; + if (DBG) log("isMobileOk: X isProvisioningNetwork result=" + result); + return result; + } else { + if (DBG) log("isMobileOk: isProvisioningNetwork is false, continue"); + } + + // Get of the addresses associated with the url host. We need to use the + // address otherwise HttpURLConnection object will use the name to get + // the addresses and will try every address but that will bypass the + // route to host we setup and the connection could succeed as the default + // interface might be connected to the internet via wifi or other interface. + InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(orgUri.getHost()); + } catch (UnknownHostException e) { + result = CMP_RESULT_CODE_NO_DNS; + log("isMobileOk: X UnknownHostException result=" + result); + return result; + } + log("isMobileOk: addresses=" + inetAddressesToString(addresses)); + + // Get the type of addresses supported by this link + LinkProperties lp = mCs.getLinkProperties( + ConnectivityManager.TYPE_MOBILE_HIPRI); + boolean linkHasIpv4 = lp.hasIPv4Address(); + boolean linkHasIpv6 = lp.hasIPv6Address(); + log("isMobileOk: linkHasIpv4=" + linkHasIpv4 + + " linkHasIpv6=" + linkHasIpv6); + + final ArrayList validAddresses = + new ArrayList(addresses.length); + + for (InetAddress addr : addresses) { + if (((addr instanceof Inet4Address) && linkHasIpv4) || + ((addr instanceof Inet6Address) && linkHasIpv6)) { + validAddresses.add(addr); + } + } + + if (validAddresses.size() == 0) { + return CMP_RESULT_CODE_NO_CONNECTION; + } + + int addrTried = 0; + while (true) { + // Loop through at most MAX_LOOPS valid addresses or until + // we run out of time + if (addrTried++ >= MAX_LOOPS) { + log("isMobileOk: too many loops tried - giving up"); + break; + } + if (SystemClock.elapsedRealtime() >= endTime) { + log("isMobileOk: spend too much time - giving up"); + break; + } + + InetAddress hostAddr = validAddresses.get(rand.nextInt( + validAddresses.size())); + + // Make a route to host so we check the specific interface. + if (mCs.requestRouteToHostAddress(ConnectivityManager.TYPE_MOBILE_HIPRI, + hostAddr.getAddress())) { + // Wait a short time to be sure the route is established ?? + log("isMobileOk:" + + " wait to establish route to hostAddr=" + hostAddr); + sleep(NET_ROUTE_ESTABLISHMENT_SLEEP_SEC); + } else { + log("isMobileOk:" + + " could not establish route to hostAddr=" + hostAddr); + // Wait a short time before the next attempt + sleep(NET_ERROR_SLEEP_SEC); + continue; + } + + // Rewrite the url to have numeric address to use the specific route + // using http for half the attempts and https for the other half. + // Doing https first and http second as on a redirected walled garden + // such as t-mobile uses we get a SocketTimeoutException: "SSL + // handshake timed out" which we declare as + // CMP_RESULT_CODE_NO_TCP_CONNECTION. We could change this, but by + // having http second we will be using logic used for some time. + URL newUrl; + String scheme = (addrTried <= (MAX_LOOPS/2)) ? "https" : "http"; + newUrl = new URL(scheme, hostAddr.getHostAddress(), + orgUri.getPath()); + log("isMobileOk: newUrl=" + newUrl); + + HttpURLConnection urlConn = null; + try { + // Open the connection set the request headers and get the response + urlConn = (HttpURLConnection)newUrl.openConnection( + java.net.Proxy.NO_PROXY); + if (scheme.equals("https")) { + ((HttpsURLConnection)urlConn).setHostnameVerifier( + new CheckMpHostnameVerifier(orgUri)); + } + urlConn.setInstanceFollowRedirects(false); + urlConn.setConnectTimeout(SOCKET_TIMEOUT_MS); + urlConn.setReadTimeout(SOCKET_TIMEOUT_MS); + urlConn.setUseCaches(false); + urlConn.setAllowUserInteraction(false); + // Set the "Connection" to "Close" as by default "Keep-Alive" + // is used which is useless in this case. + urlConn.setRequestProperty("Connection", "close"); + int responseCode = urlConn.getResponseCode(); + + // For debug display the headers + Map> headers = urlConn.getHeaderFields(); + log("isMobileOk: headers=" + headers); + + // Close the connection + urlConn.disconnect(); + urlConn = null; + + if (mTestingFailures) { + // Pretend no connection, this tests using http and https + result = CMP_RESULT_CODE_NO_CONNECTION; + log("isMobileOk: TESTING_FAILURES, pretend no connction"); + continue; + } + + if (responseCode == 204) { + // Return + result = CMP_RESULT_CODE_CONNECTABLE; + log("isMobileOk: X got expected responseCode=" + responseCode + + " result=" + result); + return result; + } else { + // Retry to be sure this was redirected, we've gotten + // occasions where a server returned 200 even though + // the device didn't have a "warm" sim. + log("isMobileOk: not expected responseCode=" + responseCode); + // TODO - it would be nice in the single-address case to do + // another DNS resolve here, but flushing the cache is a bit + // heavy-handed. + result = CMP_RESULT_CODE_REDIRECTED; + } + } catch (Exception e) { + log("isMobileOk: HttpURLConnection Exception" + e); + result = CMP_RESULT_CODE_NO_TCP_CONNECTION; + if (urlConn != null) { + urlConn.disconnect(); + urlConn = null; + } + sleep(NET_ERROR_SLEEP_SEC); + continue; + } + } + log("isMobileOk: X loops|timed out result=" + result); + return result; + } catch (Exception e) { + log("isMobileOk: Exception e=" + e); + continue; + } + } + log("isMobileOk: timed out"); + } finally { + log("isMobileOk: F stop hipri"); + mCs.setEnableFailFastMobileData(DctConstants.DISABLED); + mCs.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, + Phone.FEATURE_ENABLE_HIPRI); + + // Wait for hipri to disconnect. + long endTime = SystemClock.elapsedRealtime() + 5000; + + while(SystemClock.elapsedRealtime() < endTime) { + NetworkInfo.State state = mCs + .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState(); + if (state != NetworkInfo.State.DISCONNECTED) { + if (VDBG) { + log("isMobileOk: connected ni=" + + mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI)); + } + sleep(POLLING_SLEEP_SEC); + continue; + } + } + + log("isMobileOk: X result=" + result); + } + return result; + } + + @Override + protected Integer doInBackground(Params... params) { + return isMobileOk(params[0]); + } + + @Override + protected void onPostExecute(Integer result) { + log("onPostExecute: result=" + result); + if ((mParams != null) && (mParams.mCb != null)) { + mParams.mCb.onComplete(result); + } + } + + private String inetAddressesToString(InetAddress[] addresses) { + StringBuffer sb = new StringBuffer(); + boolean firstTime = true; + for(InetAddress addr : addresses) { + if (firstTime) { + firstTime = false; + } else { + sb.append(","); + } + sb.append(addr); + } + return sb.toString(); + } + + private void printNetworkInfo() { + boolean hasIccCard = mTm.hasIccCard(); + int simState = mTm.getSimState(); + log("hasIccCard=" + hasIccCard + + " simState=" + simState); + NetworkInfo[] ni = mCs.getAllNetworkInfo(); + if (ni != null) { + log("ni.length=" + ni.length); + for (NetworkInfo netInfo: ni) { + log("netInfo=" + netInfo.toString()); + } + } else { + log("no network info ni=null"); + } + } + + /** + * Sleep for a few seconds then return. + * @param seconds + */ + private static void sleep(int seconds) { + try { + Thread.sleep(seconds * 1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private static void log(String s) { + Slog.d(ConnectivityService.TAG, "[" + CHECKMP_TAG + "] " + s); + } + } + + // TODO: Move to ConnectivityManager and make public? + private static final String CONNECTED_TO_PROVISIONING_NETWORK_ACTION = + "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION"; + + private BroadcastReceiver mProvisioningReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(CONNECTED_TO_PROVISIONING_NETWORK_ACTION)) { + handleMobileProvisioningAction(intent.getStringExtra("EXTRA_URL")); + } + } + }; + + private void handleMobileProvisioningAction(String url) { + // Notication mark notification as not visible + setProvNotificationVisible(false, ConnectivityManager.TYPE_MOBILE_HIPRI, null, null); + + // If provisioning network handle as a special case, + // otherwise launch browser with the intent directly. + NetworkInfo ni = getProvisioningNetworkInfo(); + if ((ni != null) && ni.isConnectedToProvisioningNetwork()) { + if (DBG) log("handleMobileProvisioningAction: on provisioning network"); + MobileDataStateTracker mdst = (MobileDataStateTracker) + mNetTrackers[ConnectivityManager.TYPE_MOBILE]; + mdst.enableMobileProvisioning(url); + } else { + if (DBG) log("handleMobileProvisioningAction: on default network"); + Intent newIntent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, + Intent.CATEGORY_APP_BROWSER); + newIntent.setData(Uri.parse(url)); + newIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mContext.startActivity(newIntent); + } catch (ActivityNotFoundException e) { + loge("handleMobileProvisioningAction: startActivity failed" + e); + } + } + } + + private static final String NOTIFICATION_ID = "CaptivePortal.Notification"; + private volatile boolean mIsNotificationVisible = false; + + private void setProvNotificationVisible(boolean visible, int networkType, String extraInfo, + String url) { + if (DBG) { + log("setProvNotificationVisible: E visible=" + visible + " networkType=" + networkType + + " extraInfo=" + extraInfo + " url=" + url); + } + + Resources r = Resources.getSystem(); + NotificationManager notificationManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + + if (visible) { + CharSequence title; + CharSequence details; + int icon; + Intent intent; + Notification notification = new Notification(); + switch (networkType) { + case ConnectivityManager.TYPE_WIFI: + title = r.getString(R.string.wifi_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, + extraInfo); + icon = R.drawable.stat_notify_wifi_in_range; + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); + break; + case ConnectivityManager.TYPE_MOBILE: + case ConnectivityManager.TYPE_MOBILE_HIPRI: + title = r.getString(R.string.network_available_sign_in, 0); + // TODO: Change this to pull from NetworkInfo once a printable + // name has been added to it + details = mTelephonyManager.getNetworkOperatorName(); + icon = R.drawable.stat_notify_rssi_in_range; + intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION); + intent.putExtra("EXTRA_URL", url); + intent.setFlags(0); + notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + break; + default: + title = r.getString(R.string.network_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, + extraInfo); + icon = R.drawable.stat_notify_rssi_in_range; + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | + Intent.FLAG_ACTIVITY_NEW_TASK); + notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); + break; + } + + notification.when = 0; + notification.icon = icon; + notification.flags = Notification.FLAG_AUTO_CANCEL; + notification.tickerText = title; + notification.setLatestEventInfo(mContext, title, details, notification.contentIntent); + + try { + notificationManager.notify(NOTIFICATION_ID, networkType, notification); + } catch (NullPointerException npe) { + loge("setNotificaitionVisible: visible notificationManager npe=" + npe); + npe.printStackTrace(); + } + } else { + try { + notificationManager.cancel(NOTIFICATION_ID, networkType); + } catch (NullPointerException npe) { + loge("setNotificaitionVisible: cancel notificationManager npe=" + npe); + npe.printStackTrace(); + } + } + mIsNotificationVisible = visible; + } + + /** Location to an updatable file listing carrier provisioning urls. + * An example: + * + * + * + * http://myserver.com/foo?mdn=%3$s&iccid=%1$s&imei=%2$s + * http://www.google.com + * + */ + private static final String PROVISIONING_URL_PATH = + "/data/misc/radio/provisioning_urls.xml"; + private final File mProvisioningUrlFile = new File(PROVISIONING_URL_PATH); + + /** XML tag for root element. */ + private static final String TAG_PROVISIONING_URLS = "provisioningUrls"; + /** XML tag for individual url */ + private static final String TAG_PROVISIONING_URL = "provisioningUrl"; + /** XML tag for redirected url */ + private static final String TAG_REDIRECTED_URL = "redirectedUrl"; + /** XML attribute for mcc */ + private static final String ATTR_MCC = "mcc"; + /** XML attribute for mnc */ + private static final String ATTR_MNC = "mnc"; + + private static final int REDIRECTED_PROVISIONING = 1; + private static final int PROVISIONING = 2; + + private String getProvisioningUrlBaseFromFile(int type) { + FileReader fileReader = null; + XmlPullParser parser = null; + Configuration config = mContext.getResources().getConfiguration(); + String tagType; + + switch (type) { + case PROVISIONING: + tagType = TAG_PROVISIONING_URL; + break; + case REDIRECTED_PROVISIONING: + tagType = TAG_REDIRECTED_URL; + break; + default: + throw new RuntimeException("getProvisioningUrlBaseFromFile: Unexpected parameter " + + type); + } + + try { + fileReader = new FileReader(mProvisioningUrlFile); + parser = Xml.newPullParser(); + parser.setInput(fileReader); + XmlUtils.beginDocument(parser, TAG_PROVISIONING_URLS); + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null) break; + + if (element.equals(tagType)) { + String mcc = parser.getAttributeValue(null, ATTR_MCC); + try { + if (mcc != null && Integer.parseInt(mcc) == config.mcc) { + String mnc = parser.getAttributeValue(null, ATTR_MNC); + if (mnc != null && Integer.parseInt(mnc) == config.mnc) { + parser.next(); + if (parser.getEventType() == XmlPullParser.TEXT) { + return parser.getText(); + } + } + } + } catch (NumberFormatException e) { + loge("NumberFormatException in getProvisioningUrlBaseFromFile: " + e); + } + } + } + return null; + } catch (FileNotFoundException e) { + loge("Carrier Provisioning Urls file not found"); + } catch (XmlPullParserException e) { + loge("Xml parser exception reading Carrier Provisioning Urls file: " + e); + } catch (IOException e) { + loge("I/O exception reading Carrier Provisioning Urls file: " + e); + } finally { + if (fileReader != null) { + try { + fileReader.close(); + } catch (IOException e) {} + } + } + return null; + } + + @Override + public String getMobileRedirectedProvisioningUrl() { + enforceConnectivityInternalPermission(); + String url = getProvisioningUrlBaseFromFile(REDIRECTED_PROVISIONING); + if (TextUtils.isEmpty(url)) { + url = mContext.getResources().getString(R.string.mobile_redirected_provisioning_url); + } + return url; + } + + @Override + public String getMobileProvisioningUrl() { + enforceConnectivityInternalPermission(); + String url = getProvisioningUrlBaseFromFile(PROVISIONING); + if (TextUtils.isEmpty(url)) { + url = mContext.getResources().getString(R.string.mobile_provisioning_url); + log("getMobileProvisioningUrl: mobile_provisioining_url from resource =" + url); + } else { + log("getMobileProvisioningUrl: mobile_provisioning_url from File =" + url); + } + // populate the iccid, imei and phone number in the provisioning url. + if (!TextUtils.isEmpty(url)) { + String phoneNumber = mTelephonyManager.getLine1Number(); + if (TextUtils.isEmpty(phoneNumber)) { + phoneNumber = "0000000000"; + } + url = String.format(url, + mTelephonyManager.getSimSerialNumber() /* ICCID */, + mTelephonyManager.getDeviceId() /* IMEI */, + phoneNumber /* Phone numer */); + } + + return url; + } + + @Override + public void setProvisioningNotificationVisible(boolean visible, int networkType, + String extraInfo, String url) { + enforceConnectivityInternalPermission(); + setProvNotificationVisible(visible, networkType, extraInfo, url); + } + + @Override + public void setAirplaneMode(boolean enable) { + enforceConnectivityInternalPermission(); + final long ident = Binder.clearCallingIdentity(); + try { + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.AIRPLANE_MODE_ON, enable ? 1 : 0); + Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", enable); + mContext.sendBroadcast(intent); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void onUserStart(int userId) { + synchronized(mVpns) { + Vpn userVpn = mVpns.get(userId); + if (userVpn != null) { + loge("Starting user already has a VPN"); + return; + } + userVpn = new Vpn(mContext, mVpnCallback, mNetd, this, userId); + mVpns.put(userId, userVpn); + userVpn.startMonitoring(mContext, mTrackerHandler); + } + } + + private void onUserStop(int userId) { + synchronized(mVpns) { + Vpn userVpn = mVpns.get(userId); + if (userVpn == null) { + loge("Stopping user has no VPN"); + return; + } + mVpns.delete(userId); + } + } + + private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId == UserHandle.USER_NULL) return; + + if (Intent.ACTION_USER_STARTING.equals(action)) { + onUserStart(userId); + } else if (Intent.ACTION_USER_STOPPING.equals(action)) { + onUserStop(userId); + } + } + }; + + @Override + public LinkQualityInfo getLinkQualityInfo(int networkType) { + enforceAccessPermission(); + if (isNetworkTypeValid(networkType)) { + return mNetTrackers[networkType].getLinkQualityInfo(); + } else { + return null; + } + } + + @Override + public LinkQualityInfo getActiveLinkQualityInfo() { + enforceAccessPermission(); + if (isNetworkTypeValid(mActiveDefaultNetwork)) { + return mNetTrackers[mActiveDefaultNetwork].getLinkQualityInfo(); + } else { + return null; + } + } + + @Override + public LinkQualityInfo[] getAllLinkQualityInfo() { + enforceAccessPermission(); + final ArrayList result = Lists.newArrayList(); + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + LinkQualityInfo li = tracker.getLinkQualityInfo(); + if (li != null) { + result.add(li); + } + } + } + + return result.toArray(new LinkQualityInfo[result.size()]); + } + + /* Infrastructure for network sampling */ + + private void handleNetworkSamplingTimeout() { + + log("Sampling interval elapsed, updating statistics .."); + + // initialize list of interfaces .. + Map mapIfaceToSample = + new HashMap(); + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + String ifaceName = tracker.getNetworkInterfaceName(); + if (ifaceName != null) { + mapIfaceToSample.put(ifaceName, null); + } + } + } + + // Read samples for all interfaces + SamplingDataTracker.getSamplingSnapshots(mapIfaceToSample); + + // process samples for all networks + for (NetworkStateTracker tracker : mNetTrackers) { + if (tracker != null) { + String ifaceName = tracker.getNetworkInterfaceName(); + SamplingDataTracker.SamplingSnapshot ss = mapIfaceToSample.get(ifaceName); + if (ss != null) { + // end the previous sampling cycle + tracker.stopSampling(ss); + // start a new sampling cycle .. + tracker.startSampling(ss); + } + } + } + + log("Done."); + + int samplingIntervalInSeconds = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.CONNECTIVITY_SAMPLING_INTERVAL_IN_SECONDS, + DEFAULT_SAMPLING_INTERVAL_IN_SECONDS); + + if (DBG) log("Setting timer for " + String.valueOf(samplingIntervalInSeconds) + "seconds"); + + setAlarm(samplingIntervalInSeconds * 1000, mSampleIntervalElapsedIntent); + } + + void setAlarm(int timeoutInMilliseconds, PendingIntent intent) { + long wakeupTime = SystemClock.elapsedRealtime() + timeoutInMilliseconds; + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, wakeupTime, intent); + } +} diff --git a/services/core/java/com/android/server/ConsumerIrService.java b/services/core/java/com/android/server/ConsumerIrService.java new file mode 100644 index 0000000..783dff1 --- /dev/null +++ b/services/core/java/com/android/server/ConsumerIrService.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.hardware.input.InputManager; +import android.hardware.IConsumerIrService; +import android.os.Handler; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.Binder; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.WorkSource; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.util.Slog; +import android.view.InputDevice; + +import java.lang.RuntimeException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.ListIterator; + +public class ConsumerIrService extends IConsumerIrService.Stub { + private static final String TAG = "ConsumerIrService"; + + private static final int MAX_XMIT_TIME = 2000000; /* in microseconds */ + + private static native int halOpen(); + private static native int halTransmit(int halObject, int carrierFrequency, int[] pattern); + private static native int[] halGetCarrierFrequencies(int halObject); + + private final Context mContext; + private final PowerManager.WakeLock mWakeLock; + private final int mHal; + private final Object mHalLock = new Object(); + + ConsumerIrService(Context context) { + mContext = context; + PowerManager pm = (PowerManager)context.getSystemService( + Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(true); + + mHal = halOpen(); + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONSUMER_IR)) { + if (mHal == 0) { + throw new RuntimeException("FEATURE_CONSUMER_IR present, but no IR HAL loaded!"); + } + } else if (mHal != 0) { + throw new RuntimeException("IR HAL present, but FEATURE_CONSUMER_IR is not set!"); + } + } + + @Override + public boolean hasIrEmitter() { + return mHal != 0; + } + + private void throwIfNoIrEmitter() { + if (mHal == 0) { + throw new UnsupportedOperationException("IR emitter not available"); + } + } + + + @Override + public void transmit(String packageName, int carrierFrequency, int[] pattern) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TRANSMIT_IR) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires TRANSMIT_IR permission"); + } + + long totalXmitTime = 0; + + for (int slice : pattern) { + if (slice <= 0) { + throw new IllegalArgumentException("Non-positive IR slice"); + } + totalXmitTime += slice; + } + + if (totalXmitTime > MAX_XMIT_TIME ) { + throw new IllegalArgumentException("IR pattern too long"); + } + + throwIfNoIrEmitter(); + + // Right now there is no mechanism to ensure fair queing of IR requests + synchronized (mHalLock) { + int err = halTransmit(mHal, carrierFrequency, pattern); + + if (err < 0) { + Slog.e(TAG, "Error transmitting: " + err); + } + } + } + + @Override + public int[] getCarrierFrequencies() { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TRANSMIT_IR) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires TRANSMIT_IR permission"); + } + + throwIfNoIrEmitter(); + + synchronized(mHalLock) { + return halGetCarrierFrequencies(mHal); + } + } +} diff --git a/services/core/java/com/android/server/CountryDetectorService.java b/services/core/java/com/android/server/CountryDetectorService.java new file mode 100644 index 0000000..a478b2f --- /dev/null +++ b/services/core/java/com/android/server/CountryDetectorService.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.server; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; + +import com.android.internal.os.BackgroundThread; +import com.android.server.location.ComprehensiveCountryDetector; + +import android.content.Context; +import android.location.Country; +import android.location.CountryListener; +import android.location.ICountryDetector; +import android.location.ICountryListener; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.util.Slog; + +/** + * This class detects the country that the user is in through + * {@link ComprehensiveCountryDetector}. + * + * @hide + */ +public class CountryDetectorService extends ICountryDetector.Stub implements Runnable { + + /** + * The class represents the remote listener, it will also removes itself + * from listener list when the remote process was died. + */ + private final class Receiver implements IBinder.DeathRecipient { + private final ICountryListener mListener; + private final IBinder mKey; + + public Receiver(ICountryListener listener) { + mListener = listener; + mKey = listener.asBinder(); + } + + public void binderDied() { + removeListener(mKey); + } + + @Override + public boolean equals(Object otherObj) { + if (otherObj instanceof Receiver) { + return mKey.equals(((Receiver) otherObj).mKey); + } + return false; + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + public ICountryListener getListener() { + return mListener; + } + } + + private final static String TAG = "CountryDetector"; + + /** Whether to dump the state of the country detector service to bugreports */ + private static final boolean DEBUG = false; + + private final HashMap mReceivers; + private final Context mContext; + private ComprehensiveCountryDetector mCountryDetector; + private boolean mSystemReady; + private Handler mHandler; + private CountryListener mLocationBasedDetectorListener; + + public CountryDetectorService(Context context) { + super(); + mReceivers = new HashMap(); + mContext = context; + } + + @Override + public Country detectCountry() { + if (!mSystemReady) { + return null; // server not yet active + } else { + return mCountryDetector.detectCountry(); + } + } + + /** + * Add the ICountryListener into the listener list. + */ + @Override + public void addCountryListener(ICountryListener listener) throws RemoteException { + if (!mSystemReady) { + throw new RemoteException(); + } + addListener(listener); + } + + /** + * Remove the ICountryListener from the listener list. + */ + @Override + public void removeCountryListener(ICountryListener listener) throws RemoteException { + if (!mSystemReady) { + throw new RemoteException(); + } + removeListener(listener.asBinder()); + } + + private void addListener(ICountryListener listener) { + synchronized (mReceivers) { + Receiver r = new Receiver(listener); + try { + listener.asBinder().linkToDeath(r, 0); + mReceivers.put(listener.asBinder(), r); + if (mReceivers.size() == 1) { + Slog.d(TAG, "The first listener is added"); + setCountryListener(mLocationBasedDetectorListener); + } + } catch (RemoteException e) { + Slog.e(TAG, "linkToDeath failed:", e); + } + } + } + + private void removeListener(IBinder key) { + synchronized (mReceivers) { + mReceivers.remove(key); + if (mReceivers.isEmpty()) { + setCountryListener(null); + Slog.d(TAG, "No listener is left"); + } + } + } + + + protected void notifyReceivers(Country country) { + synchronized(mReceivers) { + for (Receiver receiver : mReceivers.values()) { + try { + receiver.getListener().onCountryDetected(country); + } catch (RemoteException e) { + // TODO: Shall we remove the receiver? + Slog.e(TAG, "notifyReceivers failed:", e); + } + } + } + } + + void systemRunning() { + // Shall we wait for the initialization finish. + BackgroundThread.getHandler().post(this); + } + + private void initialize() { + mCountryDetector = new ComprehensiveCountryDetector(mContext); + mLocationBasedDetectorListener = new CountryListener() { + public void onCountryDetected(final Country country) { + mHandler.post(new Runnable() { + public void run() { + notifyReceivers(country); + } + }); + } + }; + } + + public void run() { + mHandler = new Handler(); + initialize(); + mSystemReady = true; + } + + protected void setCountryListener(final CountryListener listener) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCountryDetector.setCountryListener(listener); + } + }); + } + + // For testing + boolean isSystemReady() { + return mSystemReady; + } + + @SuppressWarnings("unused") + @Override + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + if (!DEBUG) return; + try { + final Printer p = new PrintWriterPrinter(fout); + p.println("CountryDetectorService state:"); + p.println(" Number of listeners=" + mReceivers.keySet().size()); + if (mCountryDetector == null) { + p.println(" ComprehensiveCountryDetector not initialized"); + } else { + p.println(" " + mCountryDetector.toString()); + } + } catch (Exception e) { + Slog.e(TAG, "Failed to dump CountryDetectorService: ", e); + } + } +} diff --git a/services/core/java/com/android/server/DiskStatsService.java b/services/core/java/com/android/server/DiskStatsService.java new file mode 100644 index 0000000..ac25dc5 --- /dev/null +++ b/services/core/java/com/android/server/DiskStatsService.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.os.Binder; +import android.os.Environment; +import android.os.FileUtils; +import android.os.StatFs; +import android.os.SystemClock; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * This service exists only as a "dumpsys" target which reports + * statistics about the status of the disk. + */ +public class DiskStatsService extends Binder { + private static final String TAG = "DiskStatsService"; + + private final Context mContext; + + public DiskStatsService(Context context) { + mContext = context; + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); + + // Run a quick-and-dirty performance test: write 512 bytes + byte[] junk = new byte[512]; + for (int i = 0; i < junk.length; i++) junk[i] = (byte) i; // Write nonzero bytes + + File tmp = new File(Environment.getDataDirectory(), "system/perftest.tmp"); + FileOutputStream fos = null; + IOException error = null; + + long before = SystemClock.uptimeMillis(); + try { + fos = new FileOutputStream(tmp); + fos.write(junk); + } catch (IOException e) { + error = e; + } finally { + try { if (fos != null) fos.close(); } catch (IOException e) {} + } + + long after = SystemClock.uptimeMillis(); + if (tmp.exists()) tmp.delete(); + + if (error != null) { + pw.print("Test-Error: "); + pw.println(error.toString()); + } else { + pw.print("Latency: "); + pw.print(after - before); + pw.println("ms [512B Data Write]"); + } + + reportFreeSpace(Environment.getDataDirectory(), "Data", pw); + reportFreeSpace(Environment.getDownloadCacheDirectory(), "Cache", pw); + reportFreeSpace(new File("/system"), "System", pw); + + // TODO: Read /proc/yaffs and report interesting values; + // add configurable (through args) performance test parameters. + } + + private void reportFreeSpace(File path, String name, PrintWriter pw) { + try { + StatFs statfs = new StatFs(path.getPath()); + long bsize = statfs.getBlockSize(); + long avail = statfs.getAvailableBlocks(); + long total = statfs.getBlockCount(); + if (bsize <= 0 || total <= 0) { + throw new IllegalArgumentException( + "Invalid stat: bsize=" + bsize + " avail=" + avail + " total=" + total); + } + + pw.print(name); + pw.print("-Free: "); + pw.print(avail * bsize / 1024); + pw.print("K / "); + pw.print(total * bsize / 1024); + pw.print("K total = "); + pw.print(avail * 100 / total); + pw.println("% free"); + } catch (IllegalArgumentException e) { + pw.print(name); + pw.print("-Error: "); + pw.println(e.toString()); + return; + } + } +} diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java new file mode 100644 index 0000000..4a8bf72 --- /dev/null +++ b/services/core/java/com/android/server/DockObserver.java @@ -0,0 +1,206 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.UEventObserver; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.util.Slog; + +import java.io.FileNotFoundException; +import java.io.FileReader; + +/** + *

      DockObserver monitors for a docking station. + */ +final class DockObserver extends UEventObserver { + private static final String TAG = DockObserver.class.getSimpleName(); + + private static final String DOCK_UEVENT_MATCH = "DEVPATH=/devices/virtual/switch/dock"; + private static final String DOCK_STATE_PATH = "/sys/class/switch/dock/state"; + + private static final int MSG_DOCK_STATE_CHANGED = 0; + + private final Object mLock = new Object(); + + private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; + private int mPreviousDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; + + private boolean mSystemReady; + + private final Context mContext; + private final PowerManager mPowerManager; + private final PowerManager.WakeLock mWakeLock; + + public DockObserver(Context context) { + mContext = context; + + mPowerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + init(); // set initial status + startObserving(DOCK_UEVENT_MATCH); + } + + @Override + public void onUEvent(UEventObserver.UEvent event) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Slog.v(TAG, "Dock UEVENT: " + event.toString()); + } + + synchronized (mLock) { + try { + int newState = Integer.parseInt(event.get("SWITCH_STATE")); + if (newState != mDockState) { + mPreviousDockState = mDockState; + mDockState = newState; + if (mSystemReady) { + // Wake up immediately when docked or undocked. + mPowerManager.wakeUp(SystemClock.uptimeMillis()); + + updateLocked(); + } + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Could not parse switch state from event " + event); + } + } + } + + private void init() { + synchronized (mLock) { + try { + char[] buffer = new char[1024]; + FileReader file = new FileReader(DOCK_STATE_PATH); + try { + int len = file.read(buffer, 0, 1024); + mDockState = Integer.valueOf((new String(buffer, 0, len)).trim()); + mPreviousDockState = mDockState; + } finally { + file.close(); + } + } catch (FileNotFoundException e) { + Slog.w(TAG, "This kernel does not have dock station support"); + } catch (Exception e) { + Slog.e(TAG, "" , e); + } + } + } + + void systemReady() { + synchronized (mLock) { + // don't bother broadcasting undocked here + if (mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { + updateLocked(); + } + mSystemReady = true; + } + } + + private void updateLocked() { + mWakeLock.acquire(); + mHandler.sendEmptyMessage(MSG_DOCK_STATE_CHANGED); + } + + private void handleDockStateChange() { + synchronized (mLock) { + Slog.i(TAG, "Dock state changed: " + mDockState); + + // Skip the dock intent if not yet provisioned. + final ContentResolver cr = mContext.getContentResolver(); + if (Settings.Global.getInt(cr, + Settings.Global.DEVICE_PROVISIONED, 0) == 0) { + Slog.i(TAG, "Device not provisioned, skipping dock broadcast"); + return; + } + + // Pack up the values and broadcast them to everyone + Intent intent = new Intent(Intent.ACTION_DOCK_EVENT); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra(Intent.EXTRA_DOCK_STATE, mDockState); + + // Play a sound to provide feedback to confirm dock connection. + // Particularly useful for flaky contact pins... + if (Settings.Global.getInt(cr, + Settings.Global.DOCK_SOUNDS_ENABLED, 1) == 1) { + String whichSound = null; + if (mDockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) { + if ((mPreviousDockState == Intent.EXTRA_DOCK_STATE_DESK) || + (mPreviousDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) || + (mPreviousDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) { + whichSound = Settings.Global.DESK_UNDOCK_SOUND; + } else if (mPreviousDockState == Intent.EXTRA_DOCK_STATE_CAR) { + whichSound = Settings.Global.CAR_UNDOCK_SOUND; + } + } else { + if ((mDockState == Intent.EXTRA_DOCK_STATE_DESK) || + (mDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) || + (mDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) { + whichSound = Settings.Global.DESK_DOCK_SOUND; + } else if (mDockState == Intent.EXTRA_DOCK_STATE_CAR) { + whichSound = Settings.Global.CAR_DOCK_SOUND; + } + } + + if (whichSound != null) { + final String soundPath = Settings.Global.getString(cr, whichSound); + if (soundPath != null) { + final Uri soundUri = Uri.parse("file://" + soundPath); + if (soundUri != null) { + final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); + if (sfx != null) { + sfx.setStreamType(AudioManager.STREAM_SYSTEM); + sfx.play(); + } + } + } + } + } + + // Send the dock event intent. + // There are many components in the system watching for this so as to + // adjust audio routing, screen orientation, etc. + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + + // Release the wake lock that was acquired when the message was posted. + mWakeLock.release(); + } + } + + private final Handler mHandler = new Handler(true /*async*/) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DOCK_STATE_CHANGED: + handleDockStateChange(); + break; + } + } + }; +} diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java new file mode 100644 index 0000000..29b04da --- /dev/null +++ b/services/core/java/com/android/server/DropBoxManagerService.java @@ -0,0 +1,781 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Debug; +import android.os.DropBoxManager; +import android.os.FileUtils; +import android.os.Handler; +import android.os.Message; +import android.os.StatFs; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.format.Time; +import android.util.Slog; + +import com.android.internal.os.IDropBoxManagerService; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.zip.GZIPOutputStream; + +/** + * Implementation of {@link IDropBoxManagerService} using the filesystem. + * Clients use {@link DropBoxManager} to access this service. + */ +public final class DropBoxManagerService extends IDropBoxManagerService.Stub { + private static final String TAG = "DropBoxManagerService"; + private static final int DEFAULT_AGE_SECONDS = 3 * 86400; + private static final int DEFAULT_MAX_FILES = 1000; + private static final int DEFAULT_QUOTA_KB = 5 * 1024; + private static final int DEFAULT_QUOTA_PERCENT = 10; + private static final int DEFAULT_RESERVE_PERCENT = 10; + private static final int QUOTA_RESCAN_MILLIS = 5000; + + // mHandler 'what' value. + private static final int MSG_SEND_BROADCAST = 1; + + private static final boolean PROFILE_DUMP = false; + + // TODO: This implementation currently uses one file per entry, which is + // inefficient for smallish entries -- consider using a single queue file + // per tag (or even globally) instead. + + // The cached context and derived objects + + private final Context mContext; + private final ContentResolver mContentResolver; + private final File mDropBoxDir; + + // Accounting of all currently written log files (set in init()). + + private FileList mAllFiles = null; + private HashMap mFilesByTag = null; + + // Various bits of disk information + + private StatFs mStatFs = null; + private int mBlockSize = 0; + private int mCachedQuotaBlocks = 0; // Space we can use: computed from free space, etc. + private long mCachedQuotaUptimeMillis = 0; + + private volatile boolean mBooted = false; + + // Provide a way to perform sendBroadcast asynchronously to avoid deadlocks. + private final Handler mHandler; + + /** Receives events that might indicate a need to clean up files. */ + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + mBooted = true; + return; + } + + // Else, for ACTION_DEVICE_STORAGE_LOW: + mCachedQuotaUptimeMillis = 0; // Force a re-check of quota size + + // Run the initialization in the background (not this main thread). + // The init() and trimToFit() methods are synchronized, so they still + // block other users -- but at least the onReceive() call can finish. + new Thread() { + public void run() { + try { + init(); + trimToFit(); + } catch (IOException e) { + Slog.e(TAG, "Can't init", e); + } + } + }.start(); + } + }; + + /** + * Creates an instance of managed drop box storage. Normally there is one of these + * run by the system, but others can be created for testing and other purposes. + * + * @param context to use for receiving free space & gservices intents + * @param path to store drop box entries in + */ + public DropBoxManagerService(final Context context, File path) { + mDropBoxDir = path; + + // Set up intent receivers + mContext = context; + mContentResolver = context.getContentResolver(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); + filter.addAction(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mReceiver, filter); + + mContentResolver.registerContentObserver( + Settings.Global.CONTENT_URI, true, + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + mReceiver.onReceive(context, (Intent) null); + } + }); + + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_SEND_BROADCAST) { + mContext.sendBroadcastAsUser((Intent)msg.obj, UserHandle.OWNER, + android.Manifest.permission.READ_LOGS); + } + } + }; + + // The real work gets done lazily in init() -- that way service creation always + // succeeds, and things like disk problems cause individual method failures. + } + + /** Unregisters broadcast receivers and any other hooks -- for test instances */ + public void stop() { + mContext.unregisterReceiver(mReceiver); + } + + @Override + public void add(DropBoxManager.Entry entry) { + File temp = null; + OutputStream output = null; + final String tag = entry.getTag(); + try { + int flags = entry.getFlags(); + if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException(); + + init(); + if (!isTagEnabled(tag)) return; + long max = trimToFit(); + long lastTrim = System.currentTimeMillis(); + + byte[] buffer = new byte[mBlockSize]; + InputStream input = entry.getInputStream(); + + // First, accumulate up to one block worth of data in memory before + // deciding whether to compress the data or not. + + int read = 0; + while (read < buffer.length) { + int n = input.read(buffer, read, buffer.length - read); + if (n <= 0) break; + read += n; + } + + // If we have at least one block, compress it -- otherwise, just write + // the data in uncompressed form. + + temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp"); + int bufferSize = mBlockSize; + if (bufferSize > 4096) bufferSize = 4096; + if (bufferSize < 512) bufferSize = 512; + FileOutputStream foutput = new FileOutputStream(temp); + output = new BufferedOutputStream(foutput, bufferSize); + if (read == buffer.length && ((flags & DropBoxManager.IS_GZIPPED) == 0)) { + output = new GZIPOutputStream(output); + flags = flags | DropBoxManager.IS_GZIPPED; + } + + do { + output.write(buffer, 0, read); + + long now = System.currentTimeMillis(); + if (now - lastTrim > 30 * 1000) { + max = trimToFit(); // In case data dribbles in slowly + lastTrim = now; + } + + read = input.read(buffer); + if (read <= 0) { + FileUtils.sync(foutput); + output.close(); // Get a final size measurement + output = null; + } else { + output.flush(); // So the size measurement is pseudo-reasonable + } + + long len = temp.length(); + if (len > max) { + Slog.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > " + max + " bytes)"); + temp.delete(); + temp = null; // Pass temp = null to createEntry() to leave a tombstone + break; + } + } while (read > 0); + + long time = createEntry(temp, tag, flags); + temp = null; + + final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); + dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag); + dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time); + if (!mBooted) { + dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + } + // Call sendBroadcast after returning from this call to avoid deadlock. In particular + // the caller may be holding the WindowManagerService lock but sendBroadcast requires a + // lock in ActivityManagerService. ActivityManagerService has been caught holding that + // very lock while waiting for the WindowManagerService lock. + mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent)); + } catch (IOException e) { + Slog.e(TAG, "Can't write: " + tag, e); + } finally { + try { if (output != null) output.close(); } catch (IOException e) {} + entry.close(); + if (temp != null) temp.delete(); + } + } + + public boolean isTagEnabled(String tag) { + final long token = Binder.clearCallingIdentity(); + try { + return !"disabled".equals(Settings.Global.getString( + mContentResolver, Settings.Global.DROPBOX_TAG_PREFIX + tag)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + public synchronized DropBoxManager.Entry getNextEntry(String tag, long millis) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.READ_LOGS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("READ_LOGS permission required"); + } + + try { + init(); + } catch (IOException e) { + Slog.e(TAG, "Can't init", e); + return null; + } + + FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag); + if (list == null) return null; + + for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) { + if (entry.tag == null) continue; + if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) { + return new DropBoxManager.Entry(entry.tag, entry.timestampMillis); + } + try { + return new DropBoxManager.Entry( + entry.tag, entry.timestampMillis, entry.file, entry.flags); + } catch (IOException e) { + Slog.e(TAG, "Can't read: " + entry.file, e); + // Continue to next file + } + } + + return null; + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: Can't dump DropBoxManagerService"); + return; + } + + try { + init(); + } catch (IOException e) { + pw.println("Can't initialize: " + e); + Slog.e(TAG, "Can't init", e); + return; + } + + if (PROFILE_DUMP) Debug.startMethodTracing("/data/trace/dropbox.dump"); + + StringBuilder out = new StringBuilder(); + boolean doPrint = false, doFile = false; + ArrayList searchArgs = new ArrayList(); + for (int i = 0; args != null && i < args.length; i++) { + if (args[i].equals("-p") || args[i].equals("--print")) { + doPrint = true; + } else if (args[i].equals("-f") || args[i].equals("--file")) { + doFile = true; + } else if (args[i].startsWith("-")) { + out.append("Unknown argument: ").append(args[i]).append("\n"); + } else { + searchArgs.add(args[i]); + } + } + + out.append("Drop box contents: ").append(mAllFiles.contents.size()).append(" entries\n"); + + if (!searchArgs.isEmpty()) { + out.append("Searching for:"); + for (String a : searchArgs) out.append(" ").append(a); + out.append("\n"); + } + + int numFound = 0, numArgs = searchArgs.size(); + Time time = new Time(); + out.append("\n"); + for (EntryFile entry : mAllFiles.contents) { + time.set(entry.timestampMillis); + String date = time.format("%Y-%m-%d %H:%M:%S"); + boolean match = true; + for (int i = 0; i < numArgs && match; i++) { + String arg = searchArgs.get(i); + match = (date.contains(arg) || arg.equals(entry.tag)); + } + if (!match) continue; + + numFound++; + if (doPrint) out.append("========================================\n"); + out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag); + if (entry.file == null) { + out.append(" (no file)\n"); + continue; + } else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) { + out.append(" (contents lost)\n"); + continue; + } else { + out.append(" ("); + if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed "); + out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data"); + out.append(", ").append(entry.file.length()).append(" bytes)\n"); + } + + if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) { + if (!doPrint) out.append(" "); + out.append(entry.file.getPath()).append("\n"); + } + + if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && (doPrint || !doFile)) { + DropBoxManager.Entry dbe = null; + InputStreamReader isr = null; + try { + dbe = new DropBoxManager.Entry( + entry.tag, entry.timestampMillis, entry.file, entry.flags); + + if (doPrint) { + isr = new InputStreamReader(dbe.getInputStream()); + char[] buf = new char[4096]; + boolean newline = false; + for (;;) { + int n = isr.read(buf); + if (n <= 0) break; + out.append(buf, 0, n); + newline = (buf[n - 1] == '\n'); + + // Flush periodically when printing to avoid out-of-memory. + if (out.length() > 65536) { + pw.write(out.toString()); + out.setLength(0); + } + } + if (!newline) out.append("\n"); + } else { + String text = dbe.getText(70); + boolean truncated = (text.length() == 70); + out.append(" ").append(text.trim().replace('\n', '/')); + if (truncated) out.append(" ..."); + out.append("\n"); + } + } catch (IOException e) { + out.append("*** ").append(e.toString()).append("\n"); + Slog.e(TAG, "Can't read: " + entry.file, e); + } finally { + if (dbe != null) dbe.close(); + if (isr != null) { + try { + isr.close(); + } catch (IOException unused) { + } + } + } + } + + if (doPrint) out.append("\n"); + } + + if (numFound == 0) out.append("(No entries found.)\n"); + + if (args == null || args.length == 0) { + if (!doPrint) out.append("\n"); + out.append("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS] [tag]\n"); + } + + pw.write(out.toString()); + if (PROFILE_DUMP) Debug.stopMethodTracing(); + } + + /////////////////////////////////////////////////////////////////////////// + + /** Chronologically sorted list of {@link #EntryFile} */ + private static final class FileList implements Comparable { + public int blocks = 0; + public final TreeSet contents = new TreeSet(); + + /** Sorts bigger FileList instances before smaller ones. */ + public final int compareTo(FileList o) { + if (blocks != o.blocks) return o.blocks - blocks; + if (this == o) return 0; + if (hashCode() < o.hashCode()) return -1; + if (hashCode() > o.hashCode()) return 1; + return 0; + } + } + + /** Metadata describing an on-disk log file. */ + private static final class EntryFile implements Comparable { + public final String tag; + public final long timestampMillis; + public final int flags; + public final File file; + public final int blocks; + + /** Sorts earlier EntryFile instances before later ones. */ + public final int compareTo(EntryFile o) { + if (timestampMillis < o.timestampMillis) return -1; + if (timestampMillis > o.timestampMillis) return 1; + if (file != null && o.file != null) return file.compareTo(o.file); + if (o.file != null) return -1; + if (file != null) return 1; + if (this == o) return 0; + if (hashCode() < o.hashCode()) return -1; + if (hashCode() > o.hashCode()) return 1; + return 0; + } + + /** + * Moves an existing temporary file to a new log filename. + * @param temp file to rename + * @param dir to store file in + * @param tag to use for new log file name + * @param timestampMillis of log entry + * @param flags for the entry data + * @param blockSize to use for space accounting + * @throws IOException if the file can't be moved + */ + public EntryFile(File temp, File dir, String tag,long timestampMillis, + int flags, int blockSize) throws IOException { + if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException(); + + this.tag = tag; + this.timestampMillis = timestampMillis; + this.flags = flags; + this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + + ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") + + ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : "")); + + if (!temp.renameTo(this.file)) { + throw new IOException("Can't rename " + temp + " to " + this.file); + } + this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize); + } + + /** + * Creates a zero-length tombstone for a file whose contents were lost. + * @param dir to store file in + * @param tag to use for new log file name + * @param timestampMillis of log entry + * @throws IOException if the file can't be created. + */ + public EntryFile(File dir, String tag, long timestampMillis) throws IOException { + this.tag = tag; + this.timestampMillis = timestampMillis; + this.flags = DropBoxManager.IS_EMPTY; + this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost"); + this.blocks = 0; + new FileOutputStream(this.file).close(); + } + + /** + * Extracts metadata from an existing on-disk log filename. + * @param file name of existing log file + * @param blockSize to use for space accounting + */ + public EntryFile(File file, int blockSize) { + this.file = file; + this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize); + + String name = file.getName(); + int at = name.lastIndexOf('@'); + if (at < 0) { + this.tag = null; + this.timestampMillis = 0; + this.flags = DropBoxManager.IS_EMPTY; + return; + } + + int flags = 0; + this.tag = Uri.decode(name.substring(0, at)); + if (name.endsWith(".gz")) { + flags |= DropBoxManager.IS_GZIPPED; + name = name.substring(0, name.length() - 3); + } + if (name.endsWith(".lost")) { + flags |= DropBoxManager.IS_EMPTY; + name = name.substring(at + 1, name.length() - 5); + } else if (name.endsWith(".txt")) { + flags |= DropBoxManager.IS_TEXT; + name = name.substring(at + 1, name.length() - 4); + } else if (name.endsWith(".dat")) { + name = name.substring(at + 1, name.length() - 4); + } else { + this.flags = DropBoxManager.IS_EMPTY; + this.timestampMillis = 0; + return; + } + this.flags = flags; + + long millis; + try { millis = Long.valueOf(name); } catch (NumberFormatException e) { millis = 0; } + this.timestampMillis = millis; + } + + /** + * Creates a EntryFile object with only a timestamp for comparison purposes. + * @param timestampMillis to compare with. + */ + public EntryFile(long millis) { + this.tag = null; + this.timestampMillis = millis; + this.flags = DropBoxManager.IS_EMPTY; + this.file = null; + this.blocks = 0; + } + } + + /////////////////////////////////////////////////////////////////////////// + + /** If never run before, scans disk contents to build in-memory tracking data. */ + private synchronized void init() throws IOException { + if (mStatFs == null) { + if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) { + throw new IOException("Can't mkdir: " + mDropBoxDir); + } + try { + mStatFs = new StatFs(mDropBoxDir.getPath()); + mBlockSize = mStatFs.getBlockSize(); + } catch (IllegalArgumentException e) { // StatFs throws this on error + throw new IOException("Can't statfs: " + mDropBoxDir); + } + } + + if (mAllFiles == null) { + File[] files = mDropBoxDir.listFiles(); + if (files == null) throw new IOException("Can't list files: " + mDropBoxDir); + + mAllFiles = new FileList(); + mFilesByTag = new HashMap(); + + // Scan pre-existing files. + for (File file : files) { + if (file.getName().endsWith(".tmp")) { + Slog.i(TAG, "Cleaning temp file: " + file); + file.delete(); + continue; + } + + EntryFile entry = new EntryFile(file, mBlockSize); + if (entry.tag == null) { + Slog.w(TAG, "Unrecognized file: " + file); + continue; + } else if (entry.timestampMillis == 0) { + Slog.w(TAG, "Invalid filename: " + file); + file.delete(); + continue; + } + + enrollEntry(entry); + } + } + } + + /** Adds a disk log file to in-memory tracking for accounting and enumeration. */ + private synchronized void enrollEntry(EntryFile entry) { + mAllFiles.contents.add(entry); + mAllFiles.blocks += entry.blocks; + + // mFilesByTag is used for trimming, so don't list empty files. + // (Zero-length/lost files are trimmed by date from mAllFiles.) + + if (entry.tag != null && entry.file != null && entry.blocks > 0) { + FileList tagFiles = mFilesByTag.get(entry.tag); + if (tagFiles == null) { + tagFiles = new FileList(); + mFilesByTag.put(entry.tag, tagFiles); + } + tagFiles.contents.add(entry); + tagFiles.blocks += entry.blocks; + } + } + + /** Moves a temporary file to a final log filename and enrolls it. */ + private synchronized long createEntry(File temp, String tag, int flags) throws IOException { + long t = System.currentTimeMillis(); + + // Require each entry to have a unique timestamp; if there are entries + // >10sec in the future (due to clock skew), drag them back to avoid + // keeping them around forever. + + SortedSet tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000)); + EntryFile[] future = null; + if (!tail.isEmpty()) { + future = tail.toArray(new EntryFile[tail.size()]); + tail.clear(); // Remove from mAllFiles + } + + if (!mAllFiles.contents.isEmpty()) { + t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1); + } + + if (future != null) { + for (EntryFile late : future) { + mAllFiles.blocks -= late.blocks; + FileList tagFiles = mFilesByTag.get(late.tag); + if (tagFiles != null && tagFiles.contents.remove(late)) { + tagFiles.blocks -= late.blocks; + } + if ((late.flags & DropBoxManager.IS_EMPTY) == 0) { + enrollEntry(new EntryFile( + late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize)); + } else { + enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++)); + } + } + } + + if (temp == null) { + enrollEntry(new EntryFile(mDropBoxDir, tag, t)); + } else { + enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize)); + } + return t; + } + + /** + * Trims the files on disk to make sure they aren't using too much space. + * @return the overall quota for storage (in bytes) + */ + private synchronized long trimToFit() { + // Expunge aged items (including tombstones marking deleted data). + + int ageSeconds = Settings.Global.getInt(mContentResolver, + Settings.Global.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS); + int maxFiles = Settings.Global.getInt(mContentResolver, + Settings.Global.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES); + long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000; + while (!mAllFiles.contents.isEmpty()) { + EntryFile entry = mAllFiles.contents.first(); + if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < maxFiles) break; + + FileList tag = mFilesByTag.get(entry.tag); + if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks; + if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks; + if (entry.file != null) entry.file.delete(); + } + + // Compute overall quota (a fraction of available free space) in blocks. + // The quota changes dynamically based on the amount of free space; + // that way when lots of data is available we can use it, but we'll get + // out of the way if storage starts getting tight. + + long uptimeMillis = SystemClock.uptimeMillis(); + if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) { + int quotaPercent = Settings.Global.getInt(mContentResolver, + Settings.Global.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT); + int reservePercent = Settings.Global.getInt(mContentResolver, + Settings.Global.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT); + int quotaKb = Settings.Global.getInt(mContentResolver, + Settings.Global.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB); + + mStatFs.restat(mDropBoxDir.getPath()); + int available = mStatFs.getAvailableBlocks(); + int nonreserved = available - mStatFs.getBlockCount() * reservePercent / 100; + int maximum = quotaKb * 1024 / mBlockSize; + mCachedQuotaBlocks = Math.min(maximum, Math.max(0, nonreserved * quotaPercent / 100)); + mCachedQuotaUptimeMillis = uptimeMillis; + } + + // If we're using too much space, delete old items to make room. + // + // We trim each tag independently (this is why we keep per-tag lists). + // Space is "fairly" shared between tags -- they are all squeezed + // equally until enough space is reclaimed. + // + // A single circular buffer (a la logcat) would be simpler, but this + // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+ + // kernel crash dumps, and 100KB+ ANR reports) without swamping small, + // well-behaved data streams (event statistics, profile data, etc). + // + // Deleted files are replaced with zero-length tombstones to mark what + // was lost. Tombstones are expunged by age (see above). + + if (mAllFiles.blocks > mCachedQuotaBlocks) { + // Find a fair share amount of space to limit each tag + int unsqueezed = mAllFiles.blocks, squeezed = 0; + TreeSet tags = new TreeSet(mFilesByTag.values()); + for (FileList tag : tags) { + if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) { + break; + } + unsqueezed -= tag.blocks; + squeezed++; + } + int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed; + + // Remove old items from each tag until it meets the per-tag quota. + for (FileList tag : tags) { + if (mAllFiles.blocks < mCachedQuotaBlocks) break; + while (tag.blocks > tagQuota && !tag.contents.isEmpty()) { + EntryFile entry = tag.contents.first(); + if (tag.contents.remove(entry)) tag.blocks -= entry.blocks; + if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks; + + try { + if (entry.file != null) entry.file.delete(); + enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis)); + } catch (IOException e) { + Slog.e(TAG, "Can't write tombstone file", e); + } + } + } + } + + return mCachedQuotaBlocks * mBlockSize; + } +} diff --git a/services/core/java/com/android/server/EntropyMixer.java b/services/core/java/com/android/server/EntropyMixer.java new file mode 100644 index 0000000..cfdbf7d --- /dev/null +++ b/services/core/java/com/android/server/EntropyMixer.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Binder; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.util.Slog; + +/** + * A service designed to load and periodically save "randomness" + * for the Linux kernel RNG and to mix in data from Hardware RNG (if present) + * into the Linux RNG. + * + *

      When a Linux system starts up, the entropy pool associated with + * {@code /dev/random} may be in a fairly predictable state. Applications which + * depend strongly on randomness may find {@code /dev/random} or + * {@code /dev/urandom} returning predictable data. In order to counteract + * this effect, it's helpful to carry the entropy pool information across + * shutdowns and startups. + * + *

      On systems with Hardware RNG (/dev/hw_random), a block of output from HW + * RNG is mixed into the Linux RNG on EntropyMixer's startup and whenever + * EntropyMixer periodically runs to save a block of output from Linux RNG on + * disk. This mixing is done in a way that does not increase the Linux RNG's + * entropy estimate is not increased. This is to avoid having to trust/verify + * the quality and authenticity of the "randomness" of the HW RNG. + * + *

      This class was modeled after the script in + * man + * 4 random. + */ +public class EntropyMixer extends Binder { + private static final String TAG = "EntropyMixer"; + private static final int ENTROPY_WHAT = 1; + private static final int ENTROPY_WRITE_PERIOD = 3 * 60 * 60 * 1000; // 3 hrs + private static final long START_TIME = System.currentTimeMillis(); + private static final long START_NANOTIME = System.nanoTime(); + + private final String randomDevice; + private final String hwRandomDevice; + private final String entropyFile; + + /** + * Handler that periodically updates the entropy on disk. + */ + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what != ENTROPY_WHAT) { + Slog.e(TAG, "Will not process invalid message"); + return; + } + addHwRandomEntropy(); + writeEntropy(); + scheduleEntropyWriter(); + } + }; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + writeEntropy(); + } + }; + + public EntropyMixer(Context context) { + this(context, getSystemDir() + "/entropy.dat", "/dev/urandom", "/dev/hw_random"); + } + + /** Test only interface, not for public use */ + public EntropyMixer( + Context context, + String entropyFile, + String randomDevice, + String hwRandomDevice) { + if (randomDevice == null) { throw new NullPointerException("randomDevice"); } + if (hwRandomDevice == null) { throw new NullPointerException("hwRandomDevice"); } + if (entropyFile == null) { throw new NullPointerException("entropyFile"); } + + this.randomDevice = randomDevice; + this.hwRandomDevice = hwRandomDevice; + this.entropyFile = entropyFile; + loadInitialEntropy(); + addDeviceSpecificEntropy(); + addHwRandomEntropy(); + writeEntropy(); + scheduleEntropyWriter(); + IntentFilter broadcastFilter = new IntentFilter(Intent.ACTION_SHUTDOWN); + broadcastFilter.addAction(Intent.ACTION_POWER_CONNECTED); + broadcastFilter.addAction(Intent.ACTION_REBOOT); + context.registerReceiver(mBroadcastReceiver, broadcastFilter); + } + + private void scheduleEntropyWriter() { + mHandler.removeMessages(ENTROPY_WHAT); + mHandler.sendEmptyMessageDelayed(ENTROPY_WHAT, ENTROPY_WRITE_PERIOD); + } + + private void loadInitialEntropy() { + try { + RandomBlock.fromFile(entropyFile).toFile(randomDevice, false); + } catch (FileNotFoundException e) { + Slog.w(TAG, "No existing entropy file -- first boot?"); + } catch (IOException e) { + Slog.w(TAG, "Failure loading existing entropy file", e); + } + } + + private void writeEntropy() { + try { + Slog.i(TAG, "Writing entropy..."); + RandomBlock.fromFile(randomDevice).toFile(entropyFile, true); + } catch (IOException e) { + Slog.w(TAG, "Unable to write entropy", e); + } + } + + /** + * Add additional information to the kernel entropy pool. The + * information isn't necessarily "random", but that's ok. Even + * sending non-random information to {@code /dev/urandom} is useful + * because, while it doesn't increase the "quality" of the entropy pool, + * it mixes more bits into the pool, which gives us a higher degree + * of uncertainty in the generated randomness. Like nature, writes to + * the random device can only cause the quality of the entropy in the + * kernel to stay the same or increase. + * + *

      For maximum effect, we try to target information which varies + * on a per-device basis, and is not easily observable to an + * attacker. + */ + private void addDeviceSpecificEntropy() { + PrintWriter out = null; + try { + out = new PrintWriter(new FileOutputStream(randomDevice)); + out.println("Copyright (C) 2009 The Android Open Source Project"); + out.println("All Your Randomness Are Belong To Us"); + out.println(START_TIME); + out.println(START_NANOTIME); + out.println(SystemProperties.get("ro.serialno")); + out.println(SystemProperties.get("ro.bootmode")); + out.println(SystemProperties.get("ro.baseband")); + out.println(SystemProperties.get("ro.carrier")); + out.println(SystemProperties.get("ro.bootloader")); + out.println(SystemProperties.get("ro.hardware")); + out.println(SystemProperties.get("ro.revision")); + out.println(SystemProperties.get("ro.build.fingerprint")); + out.println(new Object().hashCode()); + out.println(System.currentTimeMillis()); + out.println(System.nanoTime()); + } catch (IOException e) { + Slog.w(TAG, "Unable to add device specific data to the entropy pool", e); + } finally { + if (out != null) { + out.close(); + } + } + } + + /** + * Mixes in the output from HW RNG (if present) into the Linux RNG. + */ + private void addHwRandomEntropy() { + try { + RandomBlock.fromFile(hwRandomDevice).toFile(randomDevice, false); + Slog.i(TAG, "Added HW RNG output to entropy pool"); + } catch (FileNotFoundException ignored) { + // HW RNG not present/exposed -- ignore + } catch (IOException e) { + Slog.w(TAG, "Failed to add HW RNG output to entropy pool", e); + } + } + + private static String getSystemDir() { + File dataDir = Environment.getDataDirectory(); + File systemDir = new File(dataDir, "system"); + systemDir.mkdirs(); + return systemDir.toString(); + } +} diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags new file mode 100644 index 0000000..399e7d1 --- /dev/null +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -0,0 +1,188 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package com.android.server + +# --------------------------- +# BatteryService.java +# --------------------------- +2722 battery_level (level|1|6),(voltage|1|1),(temperature|1|1) +2723 battery_status (status|1|5),(health|1|5),(present|1|5),(plugged|1|5),(technology|3) +# This is logged when battery goes from discharging to charging. +# It lets us count the total amount of time between charges and the discharge level +2730 battery_discharge (duration|2|3),(minLevel|1|6),(maxLevel|1|6) + + +# --------------------------- +# PowerManagerService.java +# --------------------------- +# This is logged when the device is being forced to sleep (typically by +# the user pressing the power button). +2724 power_sleep_requested (wakeLocksCleared|1|1) +# This is logged when the screen on broadcast has completed +2725 power_screen_broadcast_send (wakelockCount|1|1) +# This is logged when the screen broadcast has completed +2726 power_screen_broadcast_done (on|1|5),(broadcastDuration|2|3),(wakelockCount|1|1) +# This is logged when the screen on broadcast has completed +2727 power_screen_broadcast_stop (which|1|5),(wakelockCount|1|1) +# This is logged when the screen is turned on or off. +2728 power_screen_state (offOrOn|1|5),(becauseOfUser|1|5),(totalTouchDownTime|2|3),(touchCycles|1|1) +# This is logged when the partial wake lock (keeping the device awake +# regardless of whether the screen is off) is acquired or released. +2729 power_partial_wake_state (releasedorAcquired|1|5),(tag|3) + +# +# Leave IDs through 2739 for more power logs (2730 used by battery_discharge above) +# + + +# --------------------------- +# DeviceStorageMonitorService.java +# --------------------------- +# The disk space free on the /data partition, in bytes +2744 free_storage_changed (data|2|2) +# Device low memory notification and disk space free on the /data partition, in bytes at that time +2745 low_storage (data|2|2) +# disk space free on the /data, /system, and /cache partitions in bytes +2746 free_storage_left (data|2|2),(system|2|2),(cache|2|2) +# file on cache partition was deleted +2748 cache_file_deleted (path|3) + + +# --------------------------- +# NotificationManagerService.java +# --------------------------- +# when a NotificationManager.notify is called +2750 notification_enqueue (pkg|3),(id|1|5),(tag|3),(userid|1|5),(notification|3) +# when someone tries to cancel a notification, the notification manager sometimes +# calls this with flags too +2751 notification_cancel (pkg|3),(id|1|5),(tag|3),(userid|1|5),(required_flags|1),(forbidden_flags|1) +# when someone tries to cancel all of the notifications for a particular package +2752 notification_cancel_all (pkg|3),(userid|1|5),(required_flags|1),(forbidden_flags|1) + + +# --------------------------- +# Watchdog.java +# --------------------------- +2802 watchdog (Service|3) +2803 watchdog_proc_pss (Process|3),(Pid|1|5),(Pss|1|2) +2804 watchdog_soft_reset (Process|3),(Pid|1|5),(MaxPss|1|2),(Pss|1|2),(Skip|3) +2805 watchdog_hard_reset (Process|3),(Pid|1|5),(MaxPss|1|2),(Pss|1|2) +2806 watchdog_pss_stats (EmptyPss|1|2),(EmptyCount|1|1),(BackgroundPss|1|2),(BackgroundCount|1|1),(ServicePss|1|2),(ServiceCount|1|1),(VisiblePss|1|2),(VisibleCount|1|1),(ForegroundPss|1|2),(ForegroundCount|1|1),(NoPssCount|1|1) +2807 watchdog_proc_stats (DeathsInOne|1|1),(DeathsInTwo|1|1),(DeathsInThree|1|1),(DeathsInFour|1|1),(DeathsInFive|1|1) +2808 watchdog_scheduled_reboot (Now|2|1),(Interval|1|3),(StartTime|1|3),(Window|1|3),(Skip|3) +2809 watchdog_meminfo (MemFree|1|2),(Buffers|1|2),(Cached|1|2),(Active|1|2),(Inactive|1|2),(AnonPages|1|2),(Mapped|1|2),(Slab|1|2),(SReclaimable|1|2),(SUnreclaim|1|2),(PageTables|1|2) +2810 watchdog_vmstat (runtime|2|3),(pgfree|1|1),(pgactivate|1|1),(pgdeactivate|1|1),(pgfault|1|1),(pgmajfault|1|1) +2811 watchdog_requested_reboot (NoWait|1|1),(ScheduleInterval|1|3),(RecheckInterval|1|3),(StartTime|1|3),(Window|1|3),(MinScreenOff|1|3),(MinNextAlarm|1|3) + + +# --------------------------- +# BackupManagerService.java +# --------------------------- +2820 backup_data_changed (Package|3) +2821 backup_start (Transport|3) +2822 backup_transport_failure (Package|3) +2823 backup_agent_failure (Package|3),(Message|3) +2824 backup_package (Package|3),(Size|1|2) +2825 backup_success (Packages|1|1),(Time|1|3) +2826 backup_reset (Transport|3) +2827 backup_initialize +2830 restore_start (Transport|3),(Source|2|5) +2831 restore_transport_failure +2832 restore_agent_failure (Package|3),(Message|3) +2833 restore_package (Package|3),(Size|1|2) +2834 restore_success (Packages|1|1),(Time|1|3) + + +# --------------------------- +# SystemServer.java +# --------------------------- +# SystemServer.run() starts: +3010 boot_progress_system_run (time|2|3) + + +# --------------------------- +# PackageManagerService.java +# --------------------------- +# Package Manager starts: +3060 boot_progress_pms_start (time|2|3) +# Package Manager .apk scan starts: +3070 boot_progress_pms_system_scan_start (time|2|3) +# Package Manager .apk scan starts: +3080 boot_progress_pms_data_scan_start (time|2|3) +# Package Manager .apk scan ends: +3090 boot_progress_pms_scan_end (time|2|3) +# Package Manager ready: +3100 boot_progress_pms_ready (time|2|3) +# + check activity_launch_time for Home app +# Value of "unknown sources" setting at app install time +3110 unknown_sources_enabled (value|1) + + +# --------------------------- +# WindowManagerService.java +# --------------------------- +# Out of memory for surfaces. +31000 wm_no_surface_memory (Window|3),(PID|1|5),(Operation|3) +# Task created. +31001 wm_task_created (TaskId|1|5),(StackId|1|5) +# Task moved to top (1) or bottom (0). +31002 wm_task_moved (TaskId|1|5),(ToTop|1),(Index|1) +# Task removed with source explanation. +31003 wm_task_removed (TaskId|1|5),(Reason|3) +# Stack created. +31004 wm_stack_created (StackId|1|5),(RelativeBoxId|1|5),(Position|1),(Weight|1|6) +# Home stack moved to top (1) or bottom (0). +31005 wm_home_stack_moved (ToTop|1) +# Stack removed. +31006 wm_stack_removed (StackId|1|5) + + +# --------------------------- +# InputMethodManagerService.java +# --------------------------- +# Re-connecting to input method service because we haven't received its interface +32000 imf_force_reconnect_ime (IME|4),(Time Since Connect|2|3),(Showing|1|1) + + +# --------------------------- +# ConnectivityService.java +# --------------------------- +# Connectivity state changed +50020 connectivity_state_changed (type|1),(subtype|1),(state|1) + + +# --------------------------- +# NetworkStatsService.java +# --------------------------- +51100 netstats_mobile_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3) +51101 netstats_wifi_sample (dev_rx_bytes|2|2),(dev_tx_bytes|2|2),(dev_rx_pkts|2|1),(dev_tx_pkts|2|1),(xt_rx_bytes|2|2),(xt_tx_bytes|2|2),(xt_rx_pkts|2|1),(xt_tx_pkts|2|1),(uid_rx_bytes|2|2),(uid_tx_bytes|2|2),(uid_rx_pkts|2|1),(uid_tx_pkts|2|1),(trusted_time|2|3) + + +# --------------------------- +# LockdownVpnTracker.java +# --------------------------- +51200 lockdown_vpn_connecting (egress_net|1) +51201 lockdown_vpn_connected (egress_net|1) +51202 lockdown_vpn_error (egress_net|1) + +# --------------------------- +# ConfigUpdateInstallReceiver.java +# --------------------------- +51300 config_install_failed (dir|3) + +# --------------------------- +# IntentFirewall.java +# --------------------------- +51400 ifw_intent_matched (Intent Type|1|5),(Component Name|3),(Caller Uid|1|5),(Caller Pkg Count|1|1),(Caller Pkgs|3),(Action|3),(MIME Type|3),(URI|3),(Flags|1|5) + +# --------------------------- +# IdleMaintenanceService.java +# --------------------------- +2753 idle_maintenance_window_start (time|2|3), (lastUserActivity|2|3), (batteryLevel|1|6), (batteryCharging|1|5) +2754 idle_maintenance_window_finish (time|2|3), (lastUserActivity|2|3), (batteryLevel|1|6), (batteryCharging|1|5) + +# --------------------------- +# MountService.java +# --------------------------- +2755 fstrim_start (time|2|3) +2756 fstrim_finish (time|2|3) diff --git a/services/core/java/com/android/server/FgThread.java b/services/core/java/com/android/server/FgThread.java new file mode 100644 index 0000000..3b655f2 --- /dev/null +++ b/services/core/java/com/android/server/FgThread.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.os.Handler; +import android.os.HandlerThread; + +/** + * Shared singleton foreground thread for the system. This is a thread for regular + * foreground service operations, which shouldn't be blocked by anything running in + * the background. In particular, the shared background thread could be doing + * relatively long-running operations like saving state to disk (in addition to + * simply being a background priority), which can cause operations scheduled on it + * to be delayed for a user-noticeable amount of time. + */ +public final class FgThread extends HandlerThread { + private static FgThread sInstance; + private static Handler sHandler; + + private FgThread() { + super("android.fg", android.os.Process.THREAD_PRIORITY_DEFAULT); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new FgThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + sHandler.post(new Runnable() { + @Override + public void run() { + android.os.Process.setCanSelfBackground(false); + } + }); + } + } + + public static FgThread get() { + synchronized (UiThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + + public static Handler getHandler() { + synchronized (UiThread.class) { + ensureThreadLocked(); + return sHandler; + } + } +} diff --git a/services/core/java/com/android/server/INativeDaemonConnectorCallbacks.java b/services/core/java/com/android/server/INativeDaemonConnectorCallbacks.java new file mode 100644 index 0000000..6fbf713 --- /dev/null +++ b/services/core/java/com/android/server/INativeDaemonConnectorCallbacks.java @@ -0,0 +1,24 @@ + +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +interface INativeDaemonConnectorCallbacks { + + void onDaemonConnected(); + boolean onEvent(int code, String raw, String[] cooked); +} diff --git a/services/core/java/com/android/server/IdleMaintenanceService.java b/services/core/java/com/android/server/IdleMaintenanceService.java new file mode 100644 index 0000000..b0a1aca --- /dev/null +++ b/services/core/java/com/android/server/IdleMaintenanceService.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.Activity; +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; + +/** + * This service observes the device state and when applicable sends + * broadcasts at the beginning and at the end of a period during which + * observers can perform idle maintenance tasks. Typical use of the + * idle maintenance is to perform somehow expensive tasks that can be + * postponed to a moment when they will not degrade user experience. + * + * The current implementation is very simple. The start of a maintenance + * window is announced if: the screen is off or showing a dream AND the + * battery level is more than twenty percent AND at least one hour passed + * activity). + * + * The end of a maintenance window is announced only if: a start was + * announced AND the screen turned on or a dream was stopped. + */ +public class IdleMaintenanceService extends BroadcastReceiver { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = IdleMaintenanceService.class.getSimpleName(); + + private static final int LAST_USER_ACTIVITY_TIME_INVALID = -1; + + private static final long MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS = 24 * 60 * 60 * 1000; // 1 day + + private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_CHARGING = 30; // percent + + private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_NOT_CHARGING = 80; // percent + + private static final int MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_RUNNING = 20; // percent + + private static final long MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START = 71 * 60 * 1000; // 71 min + + private static final long MAX_IDLE_MAINTENANCE_DURATION = 71 * 60 * 1000; // 71 min + + private static final String ACTION_UPDATE_IDLE_MAINTENANCE_STATE = + "com.android.server.IdleMaintenanceService.action.UPDATE_IDLE_MAINTENANCE_STATE"; + + private static final String ACTION_FORCE_IDLE_MAINTENANCE = + "com.android.server.IdleMaintenanceService.action.FORCE_IDLE_MAINTENANCE"; + + private static final Intent sIdleMaintenanceStartIntent; + static { + sIdleMaintenanceStartIntent = new Intent(Intent.ACTION_IDLE_MAINTENANCE_START); + sIdleMaintenanceStartIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + }; + + private static final Intent sIdleMaintenanceEndIntent; + static { + sIdleMaintenanceEndIntent = new Intent(Intent.ACTION_IDLE_MAINTENANCE_END); + sIdleMaintenanceEndIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + } + + private final AlarmManager mAlarmService; + + private final BatteryService mBatteryService; + + private final PendingIntent mUpdateIdleMaintenanceStatePendingIntent; + + private final Context mContext; + + private final WakeLock mWakeLock; + + private final Handler mHandler; + + private long mLastIdleMaintenanceStartTimeMillis; + + private long mLastUserActivityElapsedTimeMillis = LAST_USER_ACTIVITY_TIME_INVALID; + + private boolean mIdleMaintenanceStarted; + + public IdleMaintenanceService(Context context, BatteryService batteryService) { + mContext = context; + mBatteryService = batteryService; + + mAlarmService = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); + + mHandler = new Handler(mContext.getMainLooper()); + + Intent intent = new Intent(ACTION_UPDATE_IDLE_MAINTENANCE_STATE); + intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mUpdateIdleMaintenanceStatePendingIntent = PendingIntent.getBroadcast(mContext, 0, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + register(mHandler); + } + + public void register(Handler handler) { + IntentFilter intentFilter = new IntentFilter(); + + // Alarm actions. + intentFilter.addAction(ACTION_UPDATE_IDLE_MAINTENANCE_STATE); + + // Battery actions. + intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); + + // Screen actions. + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + + // Dream actions. + intentFilter.addAction(Intent.ACTION_DREAMING_STARTED); + intentFilter.addAction(Intent.ACTION_DREAMING_STOPPED); + + mContext.registerReceiverAsUser(this, UserHandle.ALL, + intentFilter, null, mHandler); + + intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_FORCE_IDLE_MAINTENANCE); + mContext.registerReceiverAsUser(this, UserHandle.ALL, + intentFilter, android.Manifest.permission.SET_ACTIVITY_WATCHER, mHandler); + } + + private void scheduleUpdateIdleMaintenanceState(long delayMillis) { + final long triggetRealTimeMillis = SystemClock.elapsedRealtime() + delayMillis; + mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggetRealTimeMillis, + mUpdateIdleMaintenanceStatePendingIntent); + } + + private void unscheduleUpdateIdleMaintenanceState() { + mAlarmService.cancel(mUpdateIdleMaintenanceStatePendingIntent); + } + + private void updateIdleMaintenanceState(boolean noisy) { + if (mIdleMaintenanceStarted) { + // Idle maintenance can be interrupted by user activity, or duration + // time out, or low battery. + if (!lastUserActivityPermitsIdleMaintenanceRunning() + || !batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning()) { + unscheduleUpdateIdleMaintenanceState(); + mIdleMaintenanceStarted = false; + EventLogTags.writeIdleMaintenanceWindowFinish(SystemClock.elapsedRealtime(), + mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(), + isBatteryCharging() ? 1 : 0); + sendIdleMaintenanceEndIntent(); + // We stopped since we don't have enough battery or timed out but the + // user is not using the device, so we should be able to run maintenance + // in the next maintenance window since the battery may be charged + // without interaction and the min interval between maintenances passed. + if (!batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning()) { + scheduleUpdateIdleMaintenanceState( + getNextIdleMaintenanceIntervalStartFromNow()); + } + } + } else if (deviceStatePermitsIdleMaintenanceStart(noisy) + && lastUserActivityPermitsIdleMaintenanceStart(noisy) + && lastRunPermitsIdleMaintenanceStart(noisy)) { + // Now that we started idle maintenance, we should schedule another + // update for the moment when the idle maintenance times out. + scheduleUpdateIdleMaintenanceState(MAX_IDLE_MAINTENANCE_DURATION); + mIdleMaintenanceStarted = true; + EventLogTags.writeIdleMaintenanceWindowStart(SystemClock.elapsedRealtime(), + mLastUserActivityElapsedTimeMillis, mBatteryService.getBatteryLevel(), + isBatteryCharging() ? 1 : 0); + mLastIdleMaintenanceStartTimeMillis = SystemClock.elapsedRealtime(); + sendIdleMaintenanceStartIntent(); + } else if (lastUserActivityPermitsIdleMaintenanceStart(noisy)) { + if (lastRunPermitsIdleMaintenanceStart(noisy)) { + // The user does not use the device and we did not run maintenance in more + // than the min interval between runs, so schedule an update - maybe the + // battery will be charged latter. + scheduleUpdateIdleMaintenanceState(MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START); + } else { + // The user does not use the device but we have run maintenance in the min + // interval between runs, so schedule an update after the min interval ends. + scheduleUpdateIdleMaintenanceState( + getNextIdleMaintenanceIntervalStartFromNow()); + } + } + } + + private long getNextIdleMaintenanceIntervalStartFromNow() { + return mLastIdleMaintenanceStartTimeMillis + MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS + - SystemClock.elapsedRealtime(); + } + + private void sendIdleMaintenanceStartIntent() { + mWakeLock.acquire(); + try { + ActivityManagerNative.getDefault().performIdleMaintenance(); + } catch (RemoteException e) { + } + mContext.sendOrderedBroadcastAsUser(sIdleMaintenanceStartIntent, UserHandle.ALL, + null, this, mHandler, Activity.RESULT_OK, null, null); + } + + private void sendIdleMaintenanceEndIntent() { + mWakeLock.acquire(); + mContext.sendOrderedBroadcastAsUser(sIdleMaintenanceEndIntent, UserHandle.ALL, + null, this, mHandler, Activity.RESULT_OK, null, null); + } + + private boolean deviceStatePermitsIdleMaintenanceStart(boolean noisy) { + final int minBatteryLevel = isBatteryCharging() + ? MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_CHARGING + : MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_START_NOT_CHARGING; + boolean allowed = (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID + && mBatteryService.getBatteryLevel() > minBatteryLevel); + if (!allowed && noisy) { + Slog.i("IdleMaintenance", "Idle maintenance not allowed due to power"); + } + return allowed; + } + + private boolean lastUserActivityPermitsIdleMaintenanceStart(boolean noisy) { + // The last time the user poked the device is above the threshold. + boolean allowed = (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID + && SystemClock.elapsedRealtime() - mLastUserActivityElapsedTimeMillis + > MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START); + if (!allowed && noisy) { + Slog.i("IdleMaintenance", "Idle maintenance not allowed due to last user activity"); + } + return allowed; + } + + private boolean lastRunPermitsIdleMaintenanceStart(boolean noisy) { + // Enough time passed since the last maintenance run. + boolean allowed = SystemClock.elapsedRealtime() - mLastIdleMaintenanceStartTimeMillis + > MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS; + if (!allowed && noisy) { + Slog.i("IdleMaintenance", "Idle maintenance not allowed due time since last"); + } + return allowed; + } + + private boolean lastUserActivityPermitsIdleMaintenanceRunning() { + // The user is not using the device. + return (mLastUserActivityElapsedTimeMillis != LAST_USER_ACTIVITY_TIME_INVALID); + } + + private boolean batteryLevelAndMaintenanceTimeoutPermitsIdleMaintenanceRunning() { + // Battery not too low and the maintenance duration did not timeout. + return (mBatteryService.getBatteryLevel() > MIN_BATTERY_LEVEL_IDLE_MAINTENANCE_RUNNING + && mLastIdleMaintenanceStartTimeMillis + MAX_IDLE_MAINTENANCE_DURATION + > SystemClock.elapsedRealtime()); + } + + private boolean isBatteryCharging() { + return mBatteryService.getPlugType() > 0 + && mBatteryService.getInvalidCharger() == 0; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.i(LOG_TAG, intent.getAction()); + } + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + // We care about battery only if maintenance is in progress so we can + // stop it if battery is too low. Note that here we assume that the + // maintenance clients are properly holding a wake lock. We will + // refactor the maintenance to use services instead of intents for the + // next release. The only client for this for now is internal an holds + // a wake lock correctly. + if (mIdleMaintenanceStarted) { + updateIdleMaintenanceState(false); + } + } else if (Intent.ACTION_SCREEN_ON.equals(action) + || Intent.ACTION_DREAMING_STOPPED.equals(action)) { + mLastUserActivityElapsedTimeMillis = LAST_USER_ACTIVITY_TIME_INVALID; + // Unschedule any future updates since we already know that maintenance + // cannot be performed since the user is back. + unscheduleUpdateIdleMaintenanceState(); + // If the screen went on/stopped dreaming, we know the user is using the + // device which means that idle maintenance should be stopped if running. + updateIdleMaintenanceState(false); + } else if (Intent.ACTION_SCREEN_OFF.equals(action) + || Intent.ACTION_DREAMING_STARTED.equals(action)) { + mLastUserActivityElapsedTimeMillis = SystemClock.elapsedRealtime(); + // If screen went off/started dreaming, we may be able to start idle maintenance + // after the minimal user inactivity elapses. We schedule an alarm for when + // this timeout elapses since the device may go to sleep by then. + scheduleUpdateIdleMaintenanceState(MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START); + } else if (ACTION_UPDATE_IDLE_MAINTENANCE_STATE.equals(action)) { + updateIdleMaintenanceState(false); + } else if (ACTION_FORCE_IDLE_MAINTENANCE.equals(action)) { + long now = SystemClock.elapsedRealtime() - 1; + mLastUserActivityElapsedTimeMillis = now - MIN_USER_INACTIVITY_IDLE_MAINTENANCE_START; + mLastIdleMaintenanceStartTimeMillis = now - MIN_IDLE_MAINTENANCE_INTERVAL_MILLIS; + updateIdleMaintenanceState(true); + } else if (Intent.ACTION_IDLE_MAINTENANCE_START.equals(action) + || Intent.ACTION_IDLE_MAINTENANCE_END.equals(action)) { + // We were holding a wake lock while broadcasting the idle maintenance + // intents but now that we finished the broadcast release the wake lock. + mWakeLock.release(); + } + } +} diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java new file mode 100644 index 0000000..26424a5 --- /dev/null +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -0,0 +1,3574 @@ +/* + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.server; + +import com.android.internal.content.PackageMonitor; +import com.android.internal.inputmethod.InputMethodUtils; +import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputSessionCallback; +import com.android.internal.view.IInputMethodClient; +import com.android.internal.view.IInputMethodManager; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.InputBindResult; +import com.android.server.statusbar.StatusBarManagerService; +import com.android.server.wm.WindowManagerService; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.app.ActivityManagerNative; +import android.app.AppGlobals; +import android.app.AlertDialog; +import android.app.IUserSwitchObserver; +import android.app.KeyguardManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.inputmethodservice.InputMethodService; +import android.os.Binder; +import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.IInterface; +import android.os.IRemoteCallback; +import android.os.Message; +import android.os.Process; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; +import android.util.AtomicFile; +import android.util.EventLog; +import android.util.LruCache; +import android.util.Pair; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.util.Slog; +import android.util.Xml; +import android.view.IWindowManager; +import android.view.InputChannel; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputBinding; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.ArrayAdapter; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RadioButton; +import android.widget.Switch; +import android.widget.TextView; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.TreeMap; + +/** + * This class provides a system service that manages input methods. + */ +public class InputMethodManagerService extends IInputMethodManager.Stub + implements ServiceConnection, Handler.Callback { + static final boolean DEBUG = false; + static final String TAG = "InputMethodManagerService"; + + static final int MSG_SHOW_IM_PICKER = 1; + static final int MSG_SHOW_IM_SUBTYPE_PICKER = 2; + static final int MSG_SHOW_IM_SUBTYPE_ENABLER = 3; + static final int MSG_SHOW_IM_CONFIG = 4; + + static final int MSG_UNBIND_INPUT = 1000; + static final int MSG_BIND_INPUT = 1010; + static final int MSG_SHOW_SOFT_INPUT = 1020; + static final int MSG_HIDE_SOFT_INPUT = 1030; + static final int MSG_ATTACH_TOKEN = 1040; + static final int MSG_CREATE_SESSION = 1050; + + static final int MSG_START_INPUT = 2000; + static final int MSG_RESTART_INPUT = 2010; + + static final int MSG_UNBIND_METHOD = 3000; + static final int MSG_BIND_METHOD = 3010; + static final int MSG_SET_ACTIVE = 3020; + + static final int MSG_HARD_KEYBOARD_SWITCH_CHANGED = 4000; + + static final long TIME_TO_RECONNECT = 10*1000; + + static final int SECURE_SUGGESTION_SPANS_MAX_SIZE = 20; + + private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher"; + + + final Context mContext; + final Resources mRes; + final Handler mHandler; + final InputMethodSettings mSettings; + final SettingsObserver mSettingsObserver; + final IWindowManager mIWindowManager; + final HandlerCaller mCaller; + final boolean mHasFeature; + private InputMethodFileManager mFileManager; + private InputMethodAndSubtypeListManager mImListManager; + private final HardKeyboardListener mHardKeyboardListener; + private final WindowManagerService mWindowManagerService; + + final InputBindResult mNoBinding = new InputBindResult(null, null, null, -1); + + // All known input methods. mMethodMap also serves as the global + // lock for this class. + final ArrayList mMethodList = new ArrayList(); + final HashMap mMethodMap = new HashMap(); + private final LruCache mSecureSuggestionSpans = + new LruCache(SECURE_SUGGESTION_SPANS_MAX_SIZE); + + // Used to bring IME service up to visible adjustment while it is being shown. + final ServiceConnection mVisibleConnection = new ServiceConnection() { + @Override public void onServiceConnected(ComponentName name, IBinder service) { + } + + @Override public void onServiceDisconnected(ComponentName name) { + } + }; + boolean mVisibleBound = false; + + // Ongoing notification + private NotificationManager mNotificationManager; + private KeyguardManager mKeyguardManager; + private StatusBarManagerService mStatusBar; + private Notification mImeSwitcherNotification; + private PendingIntent mImeSwitchPendingIntent; + private boolean mShowOngoingImeSwitcherForPhones; + private boolean mNotificationShown; + private final boolean mImeSelectedOnBoot; + + class SessionState { + final ClientState client; + final IInputMethod method; + + IInputMethodSession session; + InputChannel channel; + + @Override + public String toString() { + return "SessionState{uid " + client.uid + " pid " + client.pid + + " method " + Integer.toHexString( + System.identityHashCode(method)) + + " session " + Integer.toHexString( + System.identityHashCode(session)) + + " channel " + channel + + "}"; + } + + SessionState(ClientState _client, IInputMethod _method, + IInputMethodSession _session, InputChannel _channel) { + client = _client; + method = _method; + session = _session; + channel = _channel; + } + } + + static final class ClientState { + final IInputMethodClient client; + final IInputContext inputContext; + final int uid; + final int pid; + final InputBinding binding; + + boolean sessionRequested; + SessionState curSession; + + @Override + public String toString() { + return "ClientState{" + Integer.toHexString( + System.identityHashCode(this)) + " uid " + uid + + " pid " + pid + "}"; + } + + ClientState(IInputMethodClient _client, IInputContext _inputContext, + int _uid, int _pid) { + client = _client; + inputContext = _inputContext; + uid = _uid; + pid = _pid; + binding = new InputBinding(null, inputContext.asBinder(), uid, pid); + } + } + + final HashMap mClients + = new HashMap(); + + /** + * Set once the system is ready to run third party code. + */ + boolean mSystemReady; + + /** + * Id of the currently selected input method. + */ + String mCurMethodId; + + /** + * The current binding sequence number, incremented every time there is + * a new bind performed. + */ + int mCurSeq; + + /** + * The client that is currently bound to an input method. + */ + ClientState mCurClient; + + /** + * The last window token that gained focus. + */ + IBinder mCurFocusedWindow; + + /** + * The input context last provided by the current client. + */ + IInputContext mCurInputContext; + + /** + * The attributes last provided by the current client. + */ + EditorInfo mCurAttribute; + + /** + * The input method ID of the input method service that we are currently + * connected to or in the process of connecting to. + */ + String mCurId; + + /** + * The current subtype of the current input method. + */ + private InputMethodSubtype mCurrentSubtype; + + // This list contains the pairs of InputMethodInfo and InputMethodSubtype. + private final HashMap> + mShortcutInputMethodsAndSubtypes = + new HashMap>(); + + // Was the keyguard locked when this client became current? + private boolean mCurClientInKeyguard; + + /** + * Set to true if our ServiceConnection is currently actively bound to + * a service (whether or not we have gotten its IBinder back yet). + */ + boolean mHaveConnection; + + /** + * Set if the client has asked for the input method to be shown. + */ + boolean mShowRequested; + + /** + * Set if we were explicitly told to show the input method. + */ + boolean mShowExplicitlyRequested; + + /** + * Set if we were forced to be shown. + */ + boolean mShowForced; + + /** + * Set if we last told the input method to show itself. + */ + boolean mInputShown; + + /** + * The Intent used to connect to the current input method. + */ + Intent mCurIntent; + + /** + * The token we have made for the currently active input method, to + * identify it in the future. + */ + IBinder mCurToken; + + /** + * If non-null, this is the input method service we are currently connected + * to. + */ + IInputMethod mCurMethod; + + /** + * Time that we last initiated a bind to the input method, to determine + * if we should try to disconnect and reconnect to it. + */ + long mLastBindTime; + + /** + * Have we called mCurMethod.bindInput()? + */ + boolean mBoundToMethod; + + /** + * Currently enabled session. Only touched by service thread, not + * protected by a lock. + */ + SessionState mEnabledSession; + + /** + * True if the screen is on. The value is true initially. + */ + boolean mScreenOn = true; + + int mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT; + int mImeWindowVis; + + private AlertDialog.Builder mDialogBuilder; + private AlertDialog mSwitchingDialog; + private View mSwitchingDialogTitleView; + private InputMethodInfo[] mIms; + private int[] mSubtypeIds; + private Locale mLastSystemLocale; + private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor(); + private final IPackageManager mIPackageManager; + + class SettingsObserver extends ContentObserver { + String mLastEnabled = ""; + + SettingsObserver(Handler handler) { + super(handler); + ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.DEFAULT_INPUT_METHOD), false, this); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_INPUT_METHODS), false, this); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE), false, this); + } + + @Override public void onChange(boolean selfChange) { + synchronized (mMethodMap) { + boolean enabledChanged = false; + String newEnabled = mSettings.getEnabledInputMethodsStr(); + if (!mLastEnabled.equals(newEnabled)) { + mLastEnabled = newEnabled; + enabledChanged = true; + } + updateFromSettingsLocked(enabledChanged); + } + } + } + + class ImmsBroadcastReceiver extends android.content.BroadcastReceiver { + private void updateActive() { + // Inform the current client of the change in active status + if (mCurClient != null && mCurClient.client != null) { + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO( + MSG_SET_ACTIVE, mScreenOn ? 1 : 0, mCurClient)); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_ON.equals(action)) { + mScreenOn = true; + refreshImeWindowVisibilityLocked(); + updateActive(); + return; + } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mScreenOn = false; + setImeWindowVisibilityStatusHiddenLocked(); + updateActive(); + return; + } else if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { + hideInputMethodMenu(); + // No need to updateActive + return; + } else { + Slog.w(TAG, "Unexpected intent " + intent); + } + } + } + + class MyPackageMonitor extends PackageMonitor { + private boolean isChangingPackagesOfCurrentUser() { + final int userId = getChangingUserId(); + final boolean retval = userId == mSettings.getCurrentUserId(); + if (DEBUG) { + if (!retval) { + Slog.d(TAG, "--- ignore this call back from a background user: " + userId); + } + } + return retval; + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { + if (!isChangingPackagesOfCurrentUser()) { + return false; + } + synchronized (mMethodMap) { + String curInputMethodId = mSettings.getSelectedInputMethod(); + final int N = mMethodList.size(); + if (curInputMethodId != null) { + for (int i=0; i 0) { + defIm = InputMethodUtils.getMostApplicableDefaultIME( + mSettings.getEnabledInputMethodListLocked()); + Slog.i(TAG, "No default found, using " + defIm.getId()); + } + if (defIm != null) { + setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false); + } + } + + private void resetAllInternalStateLocked(final boolean updateOnlyWhenLocaleChanged, + final boolean resetDefaultEnabledIme) { + if (!mSystemReady) { + // not system ready + return; + } + final Locale newLocale = mRes.getConfiguration().locale; + if (!updateOnlyWhenLocaleChanged + || (newLocale != null && !newLocale.equals(mLastSystemLocale))) { + if (!updateOnlyWhenLocaleChanged) { + hideCurrentInputLocked(0, null); + mCurMethodId = null; + unbindCurrentMethodLocked(true, false); + } + if (DEBUG) { + Slog.i(TAG, "Locale has been changed to " + newLocale); + } + // InputMethodAndSubtypeListManager should be reset when the locale is changed. + mImListManager = new InputMethodAndSubtypeListManager(mContext, this); + buildInputMethodListLocked(mMethodList, mMethodMap, resetDefaultEnabledIme); + if (!updateOnlyWhenLocaleChanged) { + final String selectedImiId = mSettings.getSelectedInputMethod(); + if (TextUtils.isEmpty(selectedImiId)) { + // This is the first time of the user switch and + // set the current ime to the proper one. + resetDefaultImeLocked(mContext); + } + } else { + // If the locale is changed, needs to reset the default ime + resetDefaultImeLocked(mContext); + } + updateFromSettingsLocked(true); + mLastSystemLocale = newLocale; + if (!updateOnlyWhenLocaleChanged) { + try { + startInputInnerLocked(); + } catch (RuntimeException e) { + Slog.w(TAG, "Unexpected exception", e); + } + } + } + } + + private void resetStateIfCurrentLocaleChangedLocked() { + resetAllInternalStateLocked(true /* updateOnlyWhenLocaleChanged */, + true /* resetDefaultImeLocked */); + } + + private void switchUserLocked(int newUserId) { + mSettings.setCurrentUserId(newUserId); + // InputMethodFileManager should be reset when the user is changed + mFileManager = new InputMethodFileManager(mMethodMap, newUserId); + final String defaultImiId = mSettings.getSelectedInputMethod(); + // For secondary users, the list of enabled IMEs may not have been updated since the + // callbacks to PackageMonitor are ignored for the secondary user. Here, defaultImiId may + // not be empty even if the IME has been uninstalled by the primary user. + // Even in such cases, IMMS works fine because it will find the most applicable + // IME for that user. + final boolean initialUserSwitch = TextUtils.isEmpty(defaultImiId); + if (DEBUG) { + Slog.d(TAG, "Switch user: " + newUserId + " current ime = " + defaultImiId); + } + resetAllInternalStateLocked(false /* updateOnlyWhenLocaleChanged */, + initialUserSwitch /* needsToResetDefaultIme */); + if (initialUserSwitch) { + InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mContext.getPackageManager(), + mSettings.getEnabledInputMethodListLocked()); + } + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + // The input method manager only throws security exceptions, so let's + // log all others. + if (!(e instanceof SecurityException)) { + Slog.wtf(TAG, "Input Method Manager Crash", e); + } + throw e; + } + } + + public void systemRunning(StatusBarManagerService statusBar) { + synchronized (mMethodMap) { + if (DEBUG) { + Slog.d(TAG, "--- systemReady"); + } + if (!mSystemReady) { + mSystemReady = true; + mKeyguardManager = + (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + mNotificationManager = (NotificationManager) + mContext.getSystemService(Context.NOTIFICATION_SERVICE); + mStatusBar = statusBar; + statusBar.setIconVisibility("ime", false); + updateImeWindowStatusLocked(); + mShowOngoingImeSwitcherForPhones = mRes.getBoolean( + com.android.internal.R.bool.show_ongoing_ime_switcher); + if (mShowOngoingImeSwitcherForPhones) { + mWindowManagerService.setOnHardKeyboardStatusChangeListener( + mHardKeyboardListener); + } + buildInputMethodListLocked(mMethodList, mMethodMap, + !mImeSelectedOnBoot /* resetDefaultEnabledIme */); + if (!mImeSelectedOnBoot) { + Slog.w(TAG, "Reset the default IME as \"Resource\" is ready here."); + resetStateIfCurrentLocaleChangedLocked(); + InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( + mContext.getPackageManager(), + mSettings.getEnabledInputMethodListLocked()); + } + mLastSystemLocale = mRes.getConfiguration().locale; + try { + startInputInnerLocked(); + } catch (RuntimeException e) { + Slog.w(TAG, "Unexpected exception", e); + } + } + } + } + + private void setImeWindowVisibilityStatusHiddenLocked() { + mImeWindowVis = 0; + updateImeWindowStatusLocked(); + } + + private void refreshImeWindowVisibilityLocked() { + final Configuration conf = mRes.getConfiguration(); + final boolean haveHardKeyboard = conf.keyboard + != Configuration.KEYBOARD_NOKEYS; + final boolean hardKeyShown = haveHardKeyboard + && conf.hardKeyboardHidden + != Configuration.HARDKEYBOARDHIDDEN_YES; + + final boolean isScreenLocked = isKeyguardLocked(); + final boolean inputActive = !isScreenLocked && (mInputShown || hardKeyShown); + // We assume the softkeyboard is shown when the input is active as long as the + // hard keyboard is not shown. + final boolean inputVisible = inputActive && !hardKeyShown; + mImeWindowVis = (inputActive ? InputMethodService.IME_ACTIVE : 0) + | (inputVisible ? InputMethodService.IME_VISIBLE : 0); + updateImeWindowStatusLocked(); + } + + private void updateImeWindowStatusLocked() { + setImeWindowStatus(mCurToken, mImeWindowVis, mBackDisposition); + } + + // --------------------------------------------------------------------------------------- + // Check whether or not this is a valid IPC. Assumes an IPC is valid when either + // 1) it comes from the system process + // 2) the calling process' user id is identical to the current user id IMMS thinks. + private boolean calledFromValidUser() { + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + if (DEBUG) { + Slog.d(TAG, "--- calledFromForegroundUserOrSystemProcess ? " + + "calling uid = " + uid + " system uid = " + Process.SYSTEM_UID + + " calling userId = " + userId + ", foreground user id = " + + mSettings.getCurrentUserId() + ", calling pid = " + Binder.getCallingPid() + + InputMethodUtils.getApiCallStack()); + } + if (uid == Process.SYSTEM_UID || userId == mSettings.getCurrentUserId()) { + return true; + } + + // Caveat: A process which has INTERACT_ACROSS_USERS_FULL gets results for the + // foreground user, not for the user of that process. Accordingly InputMethodManagerService + // must not manage background users' states in any functions. + // Note that privacy-sensitive IPCs, such as setInputMethod, are still securely guarded + // by a token. + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED) { + if (DEBUG) { + Slog.d(TAG, "--- Access granted because the calling process has " + + "the INTERACT_ACROSS_USERS_FULL permission"); + } + return true; + } + Slog.w(TAG, "--- IPC called from background users. Ignore. \n" + + InputMethodUtils.getStackTrace()); + return false; + } + + private boolean bindCurrentInputMethodService( + Intent service, ServiceConnection conn, int flags) { + if (service == null || conn == null) { + Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn); + return false; + } + return mContext.bindServiceAsUser(service, conn, flags, + new UserHandle(mSettings.getCurrentUserId())); + } + + @Override + public List getInputMethodList() { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return Collections.emptyList(); + } + synchronized (mMethodMap) { + return new ArrayList(mMethodList); + } + } + + @Override + public List getEnabledInputMethodList() { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return Collections.emptyList(); + } + synchronized (mMethodMap) { + return mSettings.getEnabledInputMethodListLocked(); + } + } + + private HashMap> + getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked() { + HashMap> enabledInputMethodAndSubtypes = + new HashMap>(); + for (InputMethodInfo imi: mSettings.getEnabledInputMethodListLocked()) { + enabledInputMethodAndSubtypes.put( + imi, mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true)); + } + return enabledInputMethodAndSubtypes; + } + + /** + * @param imiId if null, returns enabled subtypes for the current imi + * @return enabled subtypes of the specified imi + */ + @Override + public List getEnabledInputMethodSubtypeList(String imiId, + boolean allowsImplicitlySelectedSubtypes) { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return Collections.emptyList(); + } + synchronized (mMethodMap) { + final InputMethodInfo imi; + if (imiId == null && mCurMethodId != null) { + imi = mMethodMap.get(mCurMethodId); + } else { + imi = mMethodMap.get(imiId); + } + if (imi == null) { + return Collections.emptyList(); + } + return mSettings.getEnabledInputMethodSubtypeListLocked( + mContext, imi, allowsImplicitlySelectedSubtypes); + } + } + + @Override + public void addClient(IInputMethodClient client, + IInputContext inputContext, int uid, int pid) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + mClients.put(client.asBinder(), new ClientState(client, + inputContext, uid, pid)); + } + } + + @Override + public void removeClient(IInputMethodClient client) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + ClientState cs = mClients.remove(client.asBinder()); + if (cs != null) { + clearClientSessionLocked(cs); + } + } + } + + void executeOrSendMessage(IInterface target, Message msg) { + if (target.asBinder() instanceof Binder) { + mCaller.sendMessage(msg); + } else { + handleMessage(msg); + msg.recycle(); + } + } + + void unbindCurrentClientLocked() { + if (mCurClient != null) { + if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client = " + + mCurClient.client.asBinder()); + if (mBoundToMethod) { + mBoundToMethod = false; + if (mCurMethod != null) { + executeOrSendMessage(mCurMethod, mCaller.obtainMessageO( + MSG_UNBIND_INPUT, mCurMethod)); + } + } + + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO( + MSG_SET_ACTIVE, 0, mCurClient)); + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO( + MSG_UNBIND_METHOD, mCurSeq, mCurClient.client)); + mCurClient.sessionRequested = false; + mCurClient = null; + + hideInputMethodMenuLocked(); + } + } + + private int getImeShowFlags() { + int flags = 0; + if (mShowForced) { + flags |= InputMethod.SHOW_FORCED + | InputMethod.SHOW_EXPLICIT; + } else if (mShowExplicitlyRequested) { + flags |= InputMethod.SHOW_EXPLICIT; + } + return flags; + } + + private int getAppShowFlags() { + int flags = 0; + if (mShowForced) { + flags |= InputMethodManager.SHOW_FORCED; + } else if (!mShowExplicitlyRequested) { + flags |= InputMethodManager.SHOW_IMPLICIT; + } + return flags; + } + + InputBindResult attachNewInputLocked(boolean initial) { + if (!mBoundToMethod) { + executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( + MSG_BIND_INPUT, mCurMethod, mCurClient.binding)); + mBoundToMethod = true; + } + final SessionState session = mCurClient.curSession; + if (initial) { + executeOrSendMessage(session.method, mCaller.obtainMessageOOO( + MSG_START_INPUT, session, mCurInputContext, mCurAttribute)); + } else { + executeOrSendMessage(session.method, mCaller.obtainMessageOOO( + MSG_RESTART_INPUT, session, mCurInputContext, mCurAttribute)); + } + if (mShowRequested) { + if (DEBUG) Slog.v(TAG, "Attach new input asks to show input"); + showCurrentInputLocked(getAppShowFlags(), null); + } + return new InputBindResult(session.session, + session.channel != null ? session.channel.dup() : null, mCurId, mCurSeq); + } + + InputBindResult startInputLocked(IInputMethodClient client, + IInputContext inputContext, EditorInfo attribute, int controlFlags) { + // If no method is currently selected, do nothing. + if (mCurMethodId == null) { + return mNoBinding; + } + + ClientState cs = mClients.get(client.asBinder()); + if (cs == null) { + throw new IllegalArgumentException("unknown client " + + client.asBinder()); + } + + try { + if (!mIWindowManager.inputMethodClientHasFocus(cs.client)) { + // Check with the window manager to make sure this client actually + // has a window with focus. If not, reject. This is thread safe + // because if the focus changes some time before or after, the + // next client receiving focus that has any interest in input will + // be calling through here after that change happens. + Slog.w(TAG, "Starting input on non-focused client " + cs.client + + " (uid=" + cs.uid + " pid=" + cs.pid + ")"); + return null; + } + } catch (RemoteException e) { + } + + return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags); + } + + InputBindResult startInputUncheckedLocked(ClientState cs, + IInputContext inputContext, EditorInfo attribute, int controlFlags) { + // If no method is currently selected, do nothing. + if (mCurMethodId == null) { + return mNoBinding; + } + + if (mCurClient != cs) { + // Was the keyguard locked when switching over to the new client? + mCurClientInKeyguard = isKeyguardLocked(); + // If the client is changing, we need to switch over to the new + // one. + unbindCurrentClientLocked(); + if (DEBUG) Slog.v(TAG, "switching to client: client = " + + cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard); + + // If the screen is on, inform the new client it is active + if (mScreenOn) { + executeOrSendMessage(cs.client, mCaller.obtainMessageIO( + MSG_SET_ACTIVE, mScreenOn ? 1 : 0, cs)); + } + } + + // Bump up the sequence for this client and attach it. + mCurSeq++; + if (mCurSeq <= 0) mCurSeq = 1; + mCurClient = cs; + mCurInputContext = inputContext; + mCurAttribute = attribute; + + // Check if the input method is changing. + if (mCurId != null && mCurId.equals(mCurMethodId)) { + if (cs.curSession != null) { + // Fast case: if we are already connected to the input method, + // then just return it. + return attachNewInputLocked( + (controlFlags&InputMethodManager.CONTROL_START_INITIAL) != 0); + } + if (mHaveConnection) { + if (mCurMethod != null) { + // Return to client, and we will get back with it when + // we have had a session made for it. + requestClientSessionLocked(cs); + return new InputBindResult(null, null, mCurId, mCurSeq); + } else if (SystemClock.uptimeMillis() + < (mLastBindTime+TIME_TO_RECONNECT)) { + // In this case we have connected to the service, but + // don't yet have its interface. If it hasn't been too + // long since we did the connection, we'll return to + // the client and wait to get the service interface so + // we can report back. If it has been too long, we want + // to fall through so we can try a disconnect/reconnect + // to see if we can get back in touch with the service. + return new InputBindResult(null, null, mCurId, mCurSeq); + } else { + EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, + mCurMethodId, SystemClock.uptimeMillis()-mLastBindTime, 0); + } + } + } + + return startInputInnerLocked(); + } + + InputBindResult startInputInnerLocked() { + if (mCurMethodId == null) { + return mNoBinding; + } + + if (!mSystemReady) { + // If the system is not yet ready, we shouldn't be running third + // party code. + return new InputBindResult(null, null, mCurMethodId, mCurSeq); + } + + InputMethodInfo info = mMethodMap.get(mCurMethodId); + if (info == null) { + throw new IllegalArgumentException("Unknown id: " + mCurMethodId); + } + + unbindCurrentMethodLocked(false, true); + + mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE); + mCurIntent.setComponent(info.getComponent()); + mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL, + com.android.internal.R.string.input_method_binding_label); + mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( + mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0)); + if (bindCurrentInputMethodService(mCurIntent, this, Context.BIND_AUTO_CREATE + | Context.BIND_NOT_VISIBLE | Context.BIND_SHOWING_UI)) { + mLastBindTime = SystemClock.uptimeMillis(); + mHaveConnection = true; + mCurId = info.getId(); + mCurToken = new Binder(); + try { + if (true || DEBUG) Slog.v(TAG, "Adding window token: " + mCurToken); + mIWindowManager.addWindowToken(mCurToken, + WindowManager.LayoutParams.TYPE_INPUT_METHOD); + } catch (RemoteException e) { + } + return new InputBindResult(null, null, mCurId, mCurSeq); + } else { + mCurIntent = null; + Slog.w(TAG, "Failure connecting to input method service: " + + mCurIntent); + } + return null; + } + + @Override + public InputBindResult startInput(IInputMethodClient client, + IInputContext inputContext, EditorInfo attribute, int controlFlags) { + if (!calledFromValidUser()) { + return null; + } + synchronized (mMethodMap) { + final long ident = Binder.clearCallingIdentity(); + try { + return startInputLocked(client, inputContext, attribute, controlFlags); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @Override + public void finishInput(IInputMethodClient client) { + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mMethodMap) { + if (mCurIntent != null && name.equals(mCurIntent.getComponent())) { + mCurMethod = IInputMethod.Stub.asInterface(service); + if (mCurToken == null) { + Slog.w(TAG, "Service connected without a token!"); + unbindCurrentMethodLocked(false, false); + return; + } + if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken); + executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( + MSG_ATTACH_TOKEN, mCurMethod, mCurToken)); + if (mCurClient != null) { + clearClientSessionLocked(mCurClient); + requestClientSessionLocked(mCurClient); + } + } + } + } + + void onSessionCreated(IInputMethod method, IInputMethodSession session, + InputChannel channel) { + synchronized (mMethodMap) { + if (mCurMethod != null && method != null + && mCurMethod.asBinder() == method.asBinder()) { + if (mCurClient != null) { + clearClientSessionLocked(mCurClient); + mCurClient.curSession = new SessionState(mCurClient, + method, session, channel); + InputBindResult res = attachNewInputLocked(true); + if (res.method != null) { + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO( + MSG_BIND_METHOD, mCurClient.client, res)); + } + return; + } + } + } + + // Session abandoned. Close its associated input channel. + channel.dispose(); + } + + void unbindCurrentMethodLocked(boolean reportToClient, boolean savePosition) { + if (mVisibleBound) { + mContext.unbindService(mVisibleConnection); + mVisibleBound = false; + } + + if (mHaveConnection) { + mContext.unbindService(this); + mHaveConnection = false; + } + + if (mCurToken != null) { + try { + if (DEBUG) Slog.v(TAG, "Removing window token: " + mCurToken); + if ((mImeWindowVis & InputMethodService.IME_ACTIVE) != 0 && savePosition) { + // The current IME is shown. Hence an IME switch (transition) is happening. + mWindowManagerService.saveLastInputMethodWindowForTransition(); + } + mIWindowManager.removeWindowToken(mCurToken); + } catch (RemoteException e) { + } + mCurToken = null; + } + + mCurId = null; + clearCurMethodLocked(); + + if (reportToClient && mCurClient != null) { + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO( + MSG_UNBIND_METHOD, mCurSeq, mCurClient.client)); + } + } + + void requestClientSessionLocked(ClientState cs) { + if (!cs.sessionRequested) { + if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs); + InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString()); + cs.sessionRequested = true; + executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO( + MSG_CREATE_SESSION, mCurMethod, channels[1], + new MethodCallback(this, mCurMethod, channels[0]))); + } + } + + void clearClientSessionLocked(ClientState cs) { + finishSessionLocked(cs.curSession); + cs.curSession = null; + cs.sessionRequested = false; + } + + private void finishSessionLocked(SessionState sessionState) { + if (sessionState != null) { + if (sessionState.session != null) { + try { + sessionState.session.finishSession(); + } catch (RemoteException e) { + Slog.w(TAG, "Session failed to close due to remote exception", e); + setImeWindowVisibilityStatusHiddenLocked(); + } + sessionState.session = null; + } + if (sessionState.channel != null) { + sessionState.channel.dispose(); + sessionState.channel = null; + } + } + } + + void clearCurMethodLocked() { + if (mCurMethod != null) { + for (ClientState cs : mClients.values()) { + clearClientSessionLocked(cs); + } + + finishSessionLocked(mEnabledSession); + mEnabledSession = null; + mCurMethod = null; + } + if (mStatusBar != null) { + mStatusBar.setIconVisibility("ime", false); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (mMethodMap) { + if (DEBUG) Slog.v(TAG, "Service disconnected: " + name + + " mCurIntent=" + mCurIntent); + if (mCurMethod != null && mCurIntent != null + && name.equals(mCurIntent.getComponent())) { + clearCurMethodLocked(); + // We consider this to be a new bind attempt, since the system + // should now try to restart the service for us. + mLastBindTime = SystemClock.uptimeMillis(); + mShowRequested = mInputShown; + mInputShown = false; + if (mCurClient != null) { + executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO( + MSG_UNBIND_METHOD, mCurSeq, mCurClient.client)); + } + } + } + } + + @Override + public void updateStatusIcon(IBinder token, String packageName, int iconId) { + int uid = Binder.getCallingUid(); + long ident = Binder.clearCallingIdentity(); + try { + if (token == null || mCurToken != token) { + Slog.w(TAG, "Ignoring setInputMethod of uid " + uid + " token: " + token); + return; + } + + synchronized (mMethodMap) { + if (iconId == 0) { + if (DEBUG) Slog.d(TAG, "hide the small icon for the input method"); + if (mStatusBar != null) { + mStatusBar.setIconVisibility("ime", false); + } + } else if (packageName != null) { + if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); + CharSequence contentDescription = null; + try { + // Use PackageManager to load label + final PackageManager packageManager = mContext.getPackageManager(); + contentDescription = packageManager.getApplicationLabel( + mIPackageManager.getApplicationInfo(packageName, 0, + mSettings.getCurrentUserId())); + } catch (RemoteException e) { + /* ignore */ + } + if (mStatusBar != null) { + mStatusBar.setIcon("ime", packageName, iconId, 0, + contentDescription != null + ? contentDescription.toString() : null); + mStatusBar.setIconVisibility("ime", true); + } + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private boolean needsToShowImeSwitchOngoingNotification() { + if (!mShowOngoingImeSwitcherForPhones) return false; + if (isScreenLocked()) return false; + synchronized (mMethodMap) { + List imis = mSettings.getEnabledInputMethodListLocked(); + final int N = imis.size(); + if (N > 2) return true; + if (N < 1) return false; + int nonAuxCount = 0; + int auxCount = 0; + InputMethodSubtype nonAuxSubtype = null; + InputMethodSubtype auxSubtype = null; + for(int i = 0; i < N; ++i) { + final InputMethodInfo imi = imis.get(i); + final List subtypes = + mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true); + final int subtypeCount = subtypes.size(); + if (subtypeCount == 0) { + ++nonAuxCount; + } else { + for (int j = 0; j < subtypeCount; ++j) { + final InputMethodSubtype subtype = subtypes.get(j); + if (!subtype.isAuxiliary()) { + ++nonAuxCount; + nonAuxSubtype = subtype; + } else { + ++auxCount; + auxSubtype = subtype; + } + } + } + } + if (nonAuxCount > 1 || auxCount > 1) { + return true; + } else if (nonAuxCount == 1 && auxCount == 1) { + if (nonAuxSubtype != null && auxSubtype != null + && (nonAuxSubtype.getLocale().equals(auxSubtype.getLocale()) + || auxSubtype.overridesImplicitlyEnabledSubtype() + || nonAuxSubtype.overridesImplicitlyEnabledSubtype()) + && nonAuxSubtype.containsExtraValueKey(TAG_TRY_SUPPRESSING_IME_SWITCHER)) { + return false; + } + return true; + } + return false; + } + } + + private boolean isKeyguardLocked() { + return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked(); + } + + // Caution! This method is called in this class. Handle multi-user carefully + @SuppressWarnings("deprecation") + @Override + public void setImeWindowStatus(IBinder token, int vis, int backDisposition) { + final long ident = Binder.clearCallingIdentity(); + try { + if (token == null || mCurToken != token) { + int uid = Binder.getCallingUid(); + Slog.w(TAG, "Ignoring setImeWindowStatus of uid " + uid + " token: " + token); + return; + } + synchronized (mMethodMap) { + // apply policy for binder calls + if (vis != 0 && isKeyguardLocked() && !mCurClientInKeyguard) { + vis = 0; + } + mImeWindowVis = vis; + mBackDisposition = backDisposition; + if (mStatusBar != null) { + mStatusBar.setImeWindowStatus(token, vis, backDisposition); + } + final boolean iconVisibility = ((vis & (InputMethodService.IME_ACTIVE)) != 0) + && (mWindowManagerService.isHardKeyboardAvailable() + || (vis & (InputMethodService.IME_VISIBLE)) != 0); + final InputMethodInfo imi = mMethodMap.get(mCurMethodId); + if (imi != null && iconVisibility && needsToShowImeSwitchOngoingNotification()) { + // Used to load label + final CharSequence title = mRes.getText( + com.android.internal.R.string.select_input_method); + final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName( + mContext, imi, mCurrentSubtype); + + mImeSwitcherNotification.setLatestEventInfo( + mContext, title, summary, mImeSwitchPendingIntent); + if (mNotificationManager != null) { + if (DEBUG) { + Slog.d(TAG, "--- show notification: label = " + summary); + } + mNotificationManager.notifyAsUser(null, + com.android.internal.R.string.select_input_method, + mImeSwitcherNotification, UserHandle.ALL); + mNotificationShown = true; + } + } else { + if (mNotificationShown && mNotificationManager != null) { + if (DEBUG) { + Slog.d(TAG, "--- hide notification"); + } + mNotificationManager.cancelAsUser(null, + com.android.internal.R.string.select_input_method, UserHandle.ALL); + mNotificationShown = false; + } + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId); + for (int i = 0; i < spans.length; ++i) { + SuggestionSpan ss = spans[i]; + if (!TextUtils.isEmpty(ss.getNotificationTargetClassName())) { + mSecureSuggestionSpans.put(ss, currentImi); + } + } + } + } + + @Override + public boolean notifySuggestionPicked(SuggestionSpan span, String originalString, int index) { + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + final InputMethodInfo targetImi = mSecureSuggestionSpans.get(span); + // TODO: Do not send the intent if the process of the targetImi is already dead. + if (targetImi != null) { + final String[] suggestions = span.getSuggestions(); + if (index < 0 || index >= suggestions.length) return false; + final String className = span.getNotificationTargetClassName(); + final Intent intent = new Intent(); + // Ensures that only a class in the original IME package will receive the + // notification. + intent.setClassName(targetImi.getPackageName(), className); + intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, originalString); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, suggestions[index]); + intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, span.hashCode()); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } finally { + Binder.restoreCallingIdentity(ident); + } + return true; + } + } + return false; + } + + void updateFromSettingsLocked(boolean enabledMayChange) { + if (enabledMayChange) { + List enabled = mSettings.getEnabledInputMethodListLocked(); + for (int i=0; i DEFAULT"); + } + mIPackageManager.setApplicationEnabledSetting(imm.getPackageName(), + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, mSettings.getCurrentUserId(), + mContext.getBasePackageName()); + } + } catch (RemoteException e) { + } + } + } + // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and + // ENABLED_INPUT_METHODS is taking care of keeping them correctly in + // sync, so we will never have a DEFAULT_INPUT_METHOD that is not + // enabled. + String id = mSettings.getSelectedInputMethod(); + // There is no input method selected, try to choose new applicable input method. + if (TextUtils.isEmpty(id) && chooseNewDefaultIMELocked()) { + id = mSettings.getSelectedInputMethod(); + } + if (!TextUtils.isEmpty(id)) { + try { + setInputMethodLocked(id, mSettings.getSelectedInputMethodSubtypeId(id)); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Unknown input method from prefs: " + id, e); + mCurMethodId = null; + unbindCurrentMethodLocked(true, false); + } + mShortcutInputMethodsAndSubtypes.clear(); + } else { + // There is no longer an input method set, so stop any current one. + mCurMethodId = null; + unbindCurrentMethodLocked(true, false); + } + } + + /* package */ void setInputMethodLocked(String id, int subtypeId) { + InputMethodInfo info = mMethodMap.get(id); + if (info == null) { + throw new IllegalArgumentException("Unknown id: " + id); + } + + // See if we need to notify a subtype change within the same IME. + if (id.equals(mCurMethodId)) { + final int subtypeCount = info.getSubtypeCount(); + if (subtypeCount <= 0) { + return; + } + final InputMethodSubtype oldSubtype = mCurrentSubtype; + final InputMethodSubtype newSubtype; + if (subtypeId >= 0 && subtypeId < subtypeCount) { + newSubtype = info.getSubtypeAt(subtypeId); + } else { + // If subtype is null, try to find the most applicable one from + // getCurrentInputMethodSubtype. + newSubtype = getCurrentInputMethodSubtypeLocked(); + } + if (newSubtype == null || oldSubtype == null) { + Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype + + ", new subtype = " + newSubtype); + return; + } + if (newSubtype != oldSubtype) { + setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true); + if (mCurMethod != null) { + try { + refreshImeWindowVisibilityLocked(); + mCurMethod.changeInputMethodSubtype(newSubtype); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call changeInputMethodSubtype"); + } + } + } + return; + } + + // Changing to a different IME. + final long ident = Binder.clearCallingIdentity(); + try { + // Set a subtype to this input method. + // subtypeId the name of a subtype which will be set. + setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false); + // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked() + // because mCurMethodId is stored as a history in + // setSelectedInputMethodAndSubtypeLocked(). + mCurMethodId = id; + + if (ActivityManagerNative.isSystemReady()) { + Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("input_method_id", id); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + unbindCurrentClientLocked(); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public boolean showSoftInput(IInputMethodClient client, int flags, + ResultReceiver resultReceiver) { + if (!calledFromValidUser()) { + return false; + } + int uid = Binder.getCallingUid(); + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mMethodMap) { + if (mCurClient == null || client == null + || mCurClient.client.asBinder() != client.asBinder()) { + try { + // We need to check if this is the current client with + // focus in the window manager, to allow this call to + // be made before input is started in it. + if (!mIWindowManager.inputMethodClientHasFocus(client)) { + Slog.w(TAG, "Ignoring showSoftInput of uid " + uid + ": " + client); + return false; + } + } catch (RemoteException e) { + return false; + } + } + + if (DEBUG) Slog.v(TAG, "Client requesting input be shown"); + return showCurrentInputLocked(flags, resultReceiver); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) { + mShowRequested = true; + if ((flags&InputMethodManager.SHOW_IMPLICIT) == 0) { + mShowExplicitlyRequested = true; + } + if ((flags&InputMethodManager.SHOW_FORCED) != 0) { + mShowExplicitlyRequested = true; + mShowForced = true; + } + + if (!mSystemReady) { + return false; + } + + boolean res = false; + if (mCurMethod != null) { + if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + mCurToken); + executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO( + MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod, + resultReceiver)); + mInputShown = true; + if (mHaveConnection && !mVisibleBound) { + bindCurrentInputMethodService( + mCurIntent, mVisibleConnection, Context.BIND_AUTO_CREATE); + mVisibleBound = true; + } + res = true; + } else if (mHaveConnection && SystemClock.uptimeMillis() + >= (mLastBindTime+TIME_TO_RECONNECT)) { + // The client has asked to have the input method shown, but + // we have been sitting here too long with a connection to the + // service and no interface received, so let's disconnect/connect + // to try to prod things along. + EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, mCurMethodId, + SystemClock.uptimeMillis()-mLastBindTime,1); + Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()"); + mContext.unbindService(this); + bindCurrentInputMethodService(mCurIntent, this, Context.BIND_AUTO_CREATE + | Context.BIND_NOT_VISIBLE); + } else { + if (DEBUG) { + Slog.d(TAG, "Can't show input: connection = " + mHaveConnection + ", time = " + + ((mLastBindTime+TIME_TO_RECONNECT) - SystemClock.uptimeMillis())); + } + } + + return res; + } + + @Override + public boolean hideSoftInput(IInputMethodClient client, int flags, + ResultReceiver resultReceiver) { + if (!calledFromValidUser()) { + return false; + } + int uid = Binder.getCallingUid(); + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mMethodMap) { + if (mCurClient == null || client == null + || mCurClient.client.asBinder() != client.asBinder()) { + try { + // We need to check if this is the current client with + // focus in the window manager, to allow this call to + // be made before input is started in it. + if (!mIWindowManager.inputMethodClientHasFocus(client)) { + if (DEBUG) Slog.w(TAG, "Ignoring hideSoftInput of uid " + + uid + ": " + client); + setImeWindowVisibilityStatusHiddenLocked(); + return false; + } + } catch (RemoteException e) { + setImeWindowVisibilityStatusHiddenLocked(); + return false; + } + } + + if (DEBUG) Slog.v(TAG, "Client requesting input be hidden"); + return hideCurrentInputLocked(flags, resultReceiver); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + boolean hideCurrentInputLocked(int flags, ResultReceiver resultReceiver) { + if ((flags&InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 + && (mShowExplicitlyRequested || mShowForced)) { + if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); + return false; + } + if (mShowForced && (flags&InputMethodManager.HIDE_NOT_ALWAYS) != 0) { + if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); + return false; + } + boolean res; + if (mInputShown && mCurMethod != null) { + executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( + MSG_HIDE_SOFT_INPUT, mCurMethod, resultReceiver)); + res = true; + } else { + res = false; + } + if (mHaveConnection && mVisibleBound) { + mContext.unbindService(mVisibleConnection); + mVisibleBound = false; + } + mInputShown = false; + mShowRequested = false; + mShowExplicitlyRequested = false; + mShowForced = false; + return res; + } + + @Override + public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken, + int controlFlags, int softInputMode, int windowFlags, + EditorInfo attribute, IInputContext inputContext) { + // Needs to check the validity before clearing calling identity + final boolean calledFromValidUser = calledFromValidUser(); + + InputBindResult res = null; + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mMethodMap) { + if (DEBUG) Slog.v(TAG, "windowGainedFocus: " + client.asBinder() + + " controlFlags=#" + Integer.toHexString(controlFlags) + + " softInputMode=#" + Integer.toHexString(softInputMode) + + " windowFlags=#" + Integer.toHexString(windowFlags)); + + ClientState cs = mClients.get(client.asBinder()); + if (cs == null) { + throw new IllegalArgumentException("unknown client " + + client.asBinder()); + } + + try { + if (!mIWindowManager.inputMethodClientHasFocus(cs.client)) { + // Check with the window manager to make sure this client actually + // has a window with focus. If not, reject. This is thread safe + // because if the focus changes some time before or after, the + // next client receiving focus that has any interest in input will + // be calling through here after that change happens. + Slog.w(TAG, "Focus gain on non-focused client " + cs.client + + " (uid=" + cs.uid + " pid=" + cs.pid + ")"); + return null; + } + } catch (RemoteException e) { + } + + if (!calledFromValidUser) { + Slog.w(TAG, "A background user is requesting window. Hiding IME."); + Slog.w(TAG, "If you want to interect with IME, you need " + + "android.permission.INTERACT_ACROSS_USERS_FULL"); + hideCurrentInputLocked(0, null); + return null; + } + + if (mCurFocusedWindow == windowToken) { + Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client + + " attribute=" + attribute + ", token = " + windowToken); + if (attribute != null) { + return startInputUncheckedLocked(cs, inputContext, attribute, + controlFlags); + } + return null; + } + mCurFocusedWindow = windowToken; + + // Should we auto-show the IME even if the caller has not + // specified what should be done with it? + // We only do this automatically if the window can resize + // to accommodate the IME (so what the user sees will give + // them good context without input information being obscured + // by the IME) or if running on a large screen where there + // is more room for the target window + IME. + final boolean doAutoShow = + (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) + == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + || mRes.getConfiguration().isLayoutSizeAtLeast( + Configuration.SCREENLAYOUT_SIZE_LARGE); + final boolean isTextEditor = + (controlFlags&InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0; + + // We want to start input before showing the IME, but after closing + // it. We want to do this after closing it to help the IME disappear + // more quickly (not get stuck behind it initializing itself for the + // new focused input, even if its window wants to hide the IME). + boolean didStart = false; + + switch (softInputMode&WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) { + case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED: + if (!isTextEditor || !doAutoShow) { + if (WindowManager.LayoutParams.mayUseInputMethod(windowFlags)) { + // There is no focus view, and this window will + // be behind any soft input window, so hide the + // soft input window if it is shown. + if (DEBUG) Slog.v(TAG, "Unspecified window will hide input"); + hideCurrentInputLocked(InputMethodManager.HIDE_NOT_ALWAYS, null); + } + } else if (isTextEditor && doAutoShow && (softInputMode & + WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + // There is a focus view, and we are navigating forward + // into the window, so show the input window for the user. + // We only do this automatically if the window can resize + // to accommodate the IME (so what the user sees will give + // them good context without input information being obscured + // by the IME) or if running on a large screen where there + // is more room for the target window + IME. + if (DEBUG) Slog.v(TAG, "Unspecified window will show input"); + if (attribute != null) { + res = startInputUncheckedLocked(cs, inputContext, attribute, + controlFlags); + didStart = true; + } + showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null); + } + break; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED: + // Do nothing. + break; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN: + if ((softInputMode & + WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward"); + hideCurrentInputLocked(0, null); + } + break; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN: + if (DEBUG) Slog.v(TAG, "Window asks to hide input"); + hideCurrentInputLocked(0, null); + break; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE: + if ((softInputMode & + WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + if (DEBUG) Slog.v(TAG, "Window asks to show input going forward"); + if (attribute != null) { + res = startInputUncheckedLocked(cs, inputContext, attribute, + controlFlags); + didStart = true; + } + showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null); + } + break; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE: + if (DEBUG) Slog.v(TAG, "Window asks to always show input"); + if (attribute != null) { + res = startInputUncheckedLocked(cs, inputContext, attribute, + controlFlags); + didStart = true; + } + showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null); + break; + } + + if (!didStart && attribute != null) { + res = startInputUncheckedLocked(cs, inputContext, attribute, + controlFlags); + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + return res; + } + + @Override + public void showInputMethodPickerFromClient(IInputMethodClient client) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + if (mCurClient == null || client == null + || mCurClient.client.asBinder() != client.asBinder()) { + Slog.w(TAG, "Ignoring showInputMethodPickerFromClient of uid " + + Binder.getCallingUid() + ": " + client); + } + + // Always call subtype picker, because subtype picker is a superset of input method + // picker. + mHandler.sendEmptyMessage(MSG_SHOW_IM_SUBTYPE_PICKER); + } + } + + @Override + public void setInputMethod(IBinder token, String id) { + if (!calledFromValidUser()) { + return; + } + setInputMethodWithSubtypeId(token, id, NOT_A_SUBTYPE_ID); + } + + @Override + public void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + if (subtype != null) { + setInputMethodWithSubtypeId(token, id, InputMethodUtils.getSubtypeIdFromHashCode( + mMethodMap.get(id), subtype.hashCode())); + } else { + setInputMethod(token, id); + } + } + } + + @Override + public void showInputMethodAndSubtypeEnablerFromClient( + IInputMethodClient client, String inputMethodId) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + if (mCurClient == null || client == null + || mCurClient.client.asBinder() != client.asBinder()) { + Slog.w(TAG, "Ignoring showInputMethodAndSubtypeEnablerFromClient of: " + client); + } + executeOrSendMessage(mCurMethod, mCaller.obtainMessageO( + MSG_SHOW_IM_SUBTYPE_ENABLER, inputMethodId)); + } + } + + @Override + public boolean switchToLastInputMethod(IBinder token) { + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + final Pair lastIme = mSettings.getLastInputMethodAndSubtypeLocked(); + final InputMethodInfo lastImi; + if (lastIme != null) { + lastImi = mMethodMap.get(lastIme.first); + } else { + lastImi = null; + } + String targetLastImiId = null; + int subtypeId = NOT_A_SUBTYPE_ID; + if (lastIme != null && lastImi != null) { + final boolean imiIdIsSame = lastImi.getId().equals(mCurMethodId); + final int lastSubtypeHash = Integer.valueOf(lastIme.second); + final int currentSubtypeHash = mCurrentSubtype == null ? NOT_A_SUBTYPE_ID + : mCurrentSubtype.hashCode(); + // If the last IME is the same as the current IME and the last subtype is not + // defined, there is no need to switch to the last IME. + if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) { + targetLastImiId = lastIme.first; + subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + } + } + + if (TextUtils.isEmpty(targetLastImiId) + && !InputMethodUtils.canAddToLastInputMethod(mCurrentSubtype)) { + // This is a safety net. If the currentSubtype can't be added to the history + // and the framework couldn't find the last ime, we will make the last ime be + // the most applicable enabled keyboard subtype of the system imes. + final List enabled = mSettings.getEnabledInputMethodListLocked(); + if (enabled != null) { + final int N = enabled.size(); + final String locale = mCurrentSubtype == null + ? mRes.getConfiguration().locale.toString() + : mCurrentSubtype.getLocale(); + for (int i = 0; i < N; ++i) { + final InputMethodInfo imi = enabled.get(i); + if (imi.getSubtypeCount() > 0 && InputMethodUtils.isSystemIme(imi)) { + InputMethodSubtype keyboardSubtype = + InputMethodUtils.findLastResortApplicableSubtypeLocked(mRes, + InputMethodUtils.getSubtypes(imi), + InputMethodUtils.SUBTYPE_MODE_KEYBOARD, locale, true); + if (keyboardSubtype != null) { + targetLastImiId = imi.getId(); + subtypeId = InputMethodUtils.getSubtypeIdFromHashCode( + imi, keyboardSubtype.hashCode()); + if(keyboardSubtype.getLocale().equals(locale)) { + break; + } + } + } + } + } + } + + if (!TextUtils.isEmpty(targetLastImiId)) { + if (DEBUG) { + Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second + + ", from: " + mCurMethodId + ", " + subtypeId); + } + setInputMethodWithSubtypeId(token, targetLastImiId, subtypeId); + return true; + } else { + return false; + } + } + } + + @Override + public boolean switchToNextInputMethod(IBinder token, boolean onlyCurrentIme) { + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + final ImeSubtypeListItem nextSubtype = mImListManager.getNextInputMethod( + onlyCurrentIme, mMethodMap.get(mCurMethodId), mCurrentSubtype); + if (nextSubtype == null) { + return false; + } + setInputMethodWithSubtypeId(token, nextSubtype.mImi.getId(), nextSubtype.mSubtypeId); + return true; + } + } + + @Override + public boolean shouldOfferSwitchingToNextInputMethod(IBinder token) { + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + final ImeSubtypeListItem nextSubtype = mImListManager.getNextInputMethod( + false /* onlyCurrentIme */, mMethodMap.get(mCurMethodId), mCurrentSubtype); + if (nextSubtype == null) { + return false; + } + return true; + } + } + + @Override + public InputMethodSubtype getLastInputMethodSubtype() { + if (!calledFromValidUser()) { + return null; + } + synchronized (mMethodMap) { + final Pair lastIme = mSettings.getLastInputMethodAndSubtypeLocked(); + // TODO: Handle the case of the last IME with no subtypes + if (lastIme == null || TextUtils.isEmpty(lastIme.first) + || TextUtils.isEmpty(lastIme.second)) return null; + final InputMethodInfo lastImi = mMethodMap.get(lastIme.first); + if (lastImi == null) return null; + try { + final int lastSubtypeHash = Integer.valueOf(lastIme.second); + final int lastSubtypeId = + InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { + return null; + } + return lastImi.getSubtypeAt(lastSubtypeId); + } catch (NumberFormatException e) { + return null; + } + } + } + + @Override + public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes) { + if (!calledFromValidUser()) { + return; + } + // By this IPC call, only a process which shares the same uid with the IME can add + // additional input method subtypes to the IME. + if (TextUtils.isEmpty(imiId) || subtypes == null || subtypes.length == 0) return; + synchronized (mMethodMap) { + final InputMethodInfo imi = mMethodMap.get(imiId); + if (imi == null) return; + final String[] packageInfos; + try { + packageInfos = mIPackageManager.getPackagesForUid(Binder.getCallingUid()); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get package infos"); + return; + } + if (packageInfos != null) { + final int packageNum = packageInfos.length; + for (int i = 0; i < packageNum; ++i) { + if (packageInfos[i].equals(imi.getPackageName())) { + mFileManager.addInputMethodSubtypes(imi, subtypes); + final long ident = Binder.clearCallingIdentity(); + try { + buildInputMethodListLocked(mMethodList, mMethodMap, + false /* resetDefaultEnabledIme */); + } finally { + Binder.restoreCallingIdentity(ident); + } + return; + } + } + } + } + return; + } + + private void setInputMethodWithSubtypeId(IBinder token, String id, int subtypeId) { + synchronized (mMethodMap) { + if (token == null) { + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_SECURE_SETTINGS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Using null token requires permission " + + android.Manifest.permission.WRITE_SECURE_SETTINGS); + } + } else if (mCurToken != token) { + Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid() + + " token: " + token); + return; + } + + final long ident = Binder.clearCallingIdentity(); + try { + setInputMethodLocked(id, subtypeId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @Override + public void hideMySoftInput(IBinder token, int flags) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + if (token == null || mCurToken != token) { + if (DEBUG) Slog.w(TAG, "Ignoring hideInputMethod of uid " + + Binder.getCallingUid() + " token: " + token); + return; + } + long ident = Binder.clearCallingIdentity(); + try { + hideCurrentInputLocked(flags, null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @Override + public void showMySoftInput(IBinder token, int flags) { + if (!calledFromValidUser()) { + return; + } + synchronized (mMethodMap) { + if (token == null || mCurToken != token) { + Slog.w(TAG, "Ignoring showMySoftInput of uid " + + Binder.getCallingUid() + " token: " + token); + return; + } + long ident = Binder.clearCallingIdentity(); + try { + showCurrentInputLocked(flags, null); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + void setEnabledSessionInMainThread(SessionState session) { + if (mEnabledSession != session) { + if (mEnabledSession != null) { + try { + if (DEBUG) Slog.v(TAG, "Disabling: " + mEnabledSession); + mEnabledSession.method.setSessionEnabled( + mEnabledSession.session, false); + } catch (RemoteException e) { + } + } + mEnabledSession = session; + try { + if (DEBUG) Slog.v(TAG, "Enabling: " + mEnabledSession); + session.method.setSessionEnabled( + session.session, true); + } catch (RemoteException e) { + } + } + } + + @Override + public boolean handleMessage(Message msg) { + SomeArgs args; + switch (msg.what) { + case MSG_SHOW_IM_PICKER: + showInputMethodMenu(); + return true; + + case MSG_SHOW_IM_SUBTYPE_PICKER: + showInputMethodSubtypeMenu(); + return true; + + case MSG_SHOW_IM_SUBTYPE_ENABLER: + args = (SomeArgs)msg.obj; + showInputMethodAndSubtypeEnabler((String)args.arg1); + args.recycle(); + return true; + + case MSG_SHOW_IM_CONFIG: + showConfigureInputMethods(); + return true; + + // --------------------------------------------------------- + + case MSG_UNBIND_INPUT: + try { + ((IInputMethod)msg.obj).unbindInput(); + } catch (RemoteException e) { + // There is nothing interesting about the method dying. + } + return true; + case MSG_BIND_INPUT: + args = (SomeArgs)msg.obj; + try { + ((IInputMethod)args.arg1).bindInput((InputBinding)args.arg2); + } catch (RemoteException e) { + } + args.recycle(); + return true; + case MSG_SHOW_SOFT_INPUT: + args = (SomeArgs)msg.obj; + try { + if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput(" + + msg.arg1 + ", " + args.arg2 + ")"); + ((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2); + } catch (RemoteException e) { + } + args.recycle(); + return true; + case MSG_HIDE_SOFT_INPUT: + args = (SomeArgs)msg.obj; + try { + if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".hideSoftInput(0, " + + args.arg2 + ")"); + ((IInputMethod)args.arg1).hideSoftInput(0, (ResultReceiver)args.arg2); + } catch (RemoteException e) { + } + args.recycle(); + return true; + case MSG_ATTACH_TOKEN: + args = (SomeArgs)msg.obj; + try { + if (DEBUG) Slog.v(TAG, "Sending attach of token: " + args.arg2); + ((IInputMethod)args.arg1).attachToken((IBinder)args.arg2); + } catch (RemoteException e) { + } + args.recycle(); + return true; + case MSG_CREATE_SESSION: { + args = (SomeArgs)msg.obj; + IInputMethod method = (IInputMethod)args.arg1; + InputChannel channel = (InputChannel)args.arg2; + try { + method.createSession(channel, (IInputSessionCallback)args.arg3); + } catch (RemoteException e) { + } finally { + // Dispose the channel if the input method is not local to this process + // because the remote proxy will get its own copy when unparceled. + if (channel != null && Binder.isProxy(method)) { + channel.dispose(); + } + } + args.recycle(); + return true; + } + // --------------------------------------------------------- + + case MSG_START_INPUT: + args = (SomeArgs)msg.obj; + try { + SessionState session = (SessionState)args.arg1; + setEnabledSessionInMainThread(session); + session.method.startInput((IInputContext)args.arg2, + (EditorInfo)args.arg3); + } catch (RemoteException e) { + } + args.recycle(); + return true; + case MSG_RESTART_INPUT: + args = (SomeArgs)msg.obj; + try { + SessionState session = (SessionState)args.arg1; + setEnabledSessionInMainThread(session); + session.method.restartInput((IInputContext)args.arg2, + (EditorInfo)args.arg3); + } catch (RemoteException e) { + } + args.recycle(); + return true; + + // --------------------------------------------------------- + + case MSG_UNBIND_METHOD: + try { + ((IInputMethodClient)msg.obj).onUnbindMethod(msg.arg1); + } catch (RemoteException e) { + // There is nothing interesting about the last client dying. + } + return true; + case MSG_BIND_METHOD: { + args = (SomeArgs)msg.obj; + IInputMethodClient client = (IInputMethodClient)args.arg1; + InputBindResult res = (InputBindResult)args.arg2; + try { + client.onBindMethod(res); + } catch (RemoteException e) { + Slog.w(TAG, "Client died receiving input method " + args.arg2); + } finally { + // Dispose the channel if the input method is not local to this process + // because the remote proxy will get its own copy when unparceled. + if (res.channel != null && Binder.isProxy(client)) { + res.channel.dispose(); + } + } + args.recycle(); + return true; + } + case MSG_SET_ACTIVE: + try { + ((ClientState)msg.obj).client.setActive(msg.arg1 != 0); + } catch (RemoteException e) { + Slog.w(TAG, "Got RemoteException sending setActive(false) notification to pid " + + ((ClientState)msg.obj).pid + " uid " + + ((ClientState)msg.obj).uid); + } + return true; + + // -------------------------------------------------------------- + case MSG_HARD_KEYBOARD_SWITCH_CHANGED: + mHardKeyboardListener.handleHardKeyboardStatusChange( + msg.arg1 == 1, msg.arg2 == 1); + return true; + } + return false; + } + + private boolean chooseNewDefaultIMELocked() { + final InputMethodInfo imi = InputMethodUtils.getMostApplicableDefaultIME( + mSettings.getEnabledInputMethodListLocked()); + if (imi != null) { + if (DEBUG) { + Slog.d(TAG, "New default IME was selected: " + imi.getId()); + } + resetSelectedInputMethodAndSubtypeLocked(imi.getId()); + return true; + } + + return false; + } + + void buildInputMethodListLocked(ArrayList list, + HashMap map, boolean resetDefaultEnabledIme) { + if (DEBUG) { + Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme + + " \n ------ \n" + InputMethodUtils.getStackTrace()); + } + list.clear(); + map.clear(); + + // Use for queryIntentServicesAsUser + final PackageManager pm = mContext.getPackageManager(); + String disabledSysImes = mSettings.getDisabledSystemInputMethods(); + if (disabledSysImes == null) disabledSysImes = ""; + + final List services = pm.queryIntentServicesAsUser( + new Intent(InputMethod.SERVICE_INTERFACE), + PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, + mSettings.getCurrentUserId()); + + final HashMap> additionalSubtypes = + mFileManager.getAllAdditionalInputMethodSubtypes(); + for (int i = 0; i < services.size(); ++i) { + ResolveInfo ri = services.get(i); + ServiceInfo si = ri.serviceInfo; + ComponentName compName = new ComponentName(si.packageName, si.name); + if (!android.Manifest.permission.BIND_INPUT_METHOD.equals( + si.permission)) { + Slog.w(TAG, "Skipping input method " + compName + + ": it does not require the permission " + + android.Manifest.permission.BIND_INPUT_METHOD); + continue; + } + + if (DEBUG) Slog.d(TAG, "Checking " + compName); + + try { + InputMethodInfo p = new InputMethodInfo(mContext, ri, additionalSubtypes); + list.add(p); + final String id = p.getId(); + map.put(id, p); + + if (DEBUG) { + Slog.d(TAG, "Found an input method " + p); + } + + } catch (XmlPullParserException e) { + Slog.w(TAG, "Unable to load input method " + compName, e); + } catch (IOException e) { + Slog.w(TAG, "Unable to load input method " + compName, e); + } + } + + if (resetDefaultEnabledIme) { + final ArrayList defaultEnabledIme = + InputMethodUtils.getDefaultEnabledImes(mContext, mSystemReady, list); + for (int i = 0; i < defaultEnabledIme.size(); ++i) { + final InputMethodInfo imi = defaultEnabledIme.get(i); + if (DEBUG) { + Slog.d(TAG, "--- enable ime = " + imi); + } + setInputMethodEnabledLocked(imi.getId(), true); + } + } + + final String defaultImiId = mSettings.getSelectedInputMethod(); + if (!TextUtils.isEmpty(defaultImiId)) { + if (!map.containsKey(defaultImiId)) { + Slog.w(TAG, "Default IME is uninstalled. Choose new default IME."); + if (chooseNewDefaultIMELocked()) { + updateFromSettingsLocked(true); + } + } else { + // Double check that the default IME is certainly enabled. + setInputMethodEnabledLocked(defaultImiId, true); + } + } + } + + // ---------------------------------------------------------------------- + + private void showInputMethodMenu() { + showInputMethodMenuInternal(false); + } + + private void showInputMethodSubtypeMenu() { + showInputMethodMenuInternal(true); + } + + private void showInputMethodAndSubtypeEnabler(String inputMethodId) { + Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (!TextUtils.isEmpty(inputMethodId)) { + intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, inputMethodId); + } + mContext.startActivityAsUser(intent, null, UserHandle.CURRENT); + } + + private void showConfigureInputMethods() { + Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + mContext.startActivityAsUser(intent, null, UserHandle.CURRENT); + } + + private boolean isScreenLocked() { + return mKeyguardManager != null + && mKeyguardManager.isKeyguardLocked() && mKeyguardManager.isKeyguardSecure(); + } + private void showInputMethodMenuInternal(boolean showSubtypes) { + if (DEBUG) Slog.v(TAG, "Show switching menu"); + + final Context context = mContext; + final boolean isScreenLocked = isScreenLocked(); + + final String lastInputMethodId = mSettings.getSelectedInputMethod(); + int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); + if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId); + + synchronized (mMethodMap) { + final HashMap> immis = + getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(); + if (immis == null || immis.size() == 0) { + return; + } + + hideInputMethodMenuLocked(); + + final List imList = + mImListManager.getSortedInputMethodAndSubtypeList( + showSubtypes, mInputShown, isScreenLocked); + + if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { + final InputMethodSubtype currentSubtype = getCurrentInputMethodSubtypeLocked(); + if (currentSubtype != null) { + final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId); + lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( + currentImi, currentSubtype.hashCode()); + } + } + + final int N = imList.size(); + mIms = new InputMethodInfo[N]; + mSubtypeIds = new int[N]; + int checkedItem = 0; + for (int i = 0; i < N; ++i) { + final ImeSubtypeListItem item = imList.get(i); + mIms[i] = item.mImi; + mSubtypeIds[i] = item.mSubtypeId; + if (mIms[i].getId().equals(lastInputMethodId)) { + int subtypeId = mSubtypeIds[i]; + if ((subtypeId == NOT_A_SUBTYPE_ID) + || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) + || (subtypeId == lastInputMethodSubtypeId)) { + checkedItem = i; + } + } + } + final TypedArray a = context.obtainStyledAttributes(null, + com.android.internal.R.styleable.DialogPreference, + com.android.internal.R.attr.alertDialogStyle, 0); + mDialogBuilder = new AlertDialog.Builder(context) + .setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + hideInputMethodMenu(); + } + }) + .setIcon(a.getDrawable( + com.android.internal.R.styleable.DialogPreference_dialogTitle)); + a.recycle(); + final LayoutInflater inflater = + (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + final View tv = inflater.inflate( + com.android.internal.R.layout.input_method_switch_dialog_title, null); + mDialogBuilder.setCustomTitle(tv); + + // Setup layout for a toggle switch of the hardware keyboard + mSwitchingDialogTitleView = tv; + mSwitchingDialogTitleView.findViewById( + com.android.internal.R.id.hard_keyboard_section).setVisibility( + mWindowManagerService.isHardKeyboardAvailable() ? + View.VISIBLE : View.GONE); + final Switch hardKeySwitch = ((Switch)mSwitchingDialogTitleView.findViewById( + com.android.internal.R.id.hard_keyboard_switch)); + hardKeySwitch.setChecked(mWindowManagerService.isHardKeyboardEnabled()); + hardKeySwitch.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + @Override + public void onCheckedChanged( + CompoundButton buttonView, boolean isChecked) { + mWindowManagerService.setHardKeyboardEnabled(isChecked); + // Ensure that the input method dialog is dismissed when changing + // the hardware keyboard state. + hideInputMethodMenu(); + } + }); + + final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(context, + com.android.internal.R.layout.simple_list_item_2_single_choice, imList, + checkedItem); + + mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, + new AlertDialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + synchronized (mMethodMap) { + if (mIms == null || mIms.length <= which + || mSubtypeIds == null || mSubtypeIds.length <= which) { + return; + } + InputMethodInfo im = mIms[which]; + int subtypeId = mSubtypeIds[which]; + adapter.mCheckedItem = which; + adapter.notifyDataSetChanged(); + hideInputMethodMenu(); + if (im != null) { + if ((subtypeId < 0) + || (subtypeId >= im.getSubtypeCount())) { + subtypeId = NOT_A_SUBTYPE_ID; + } + setInputMethodLocked(im.getId(), subtypeId); + } + } + } + }); + + if (showSubtypes && !isScreenLocked) { + mDialogBuilder.setPositiveButton( + com.android.internal.R.string.configure_input_methods, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + showConfigureInputMethods(); + } + }); + } + mSwitchingDialog = mDialogBuilder.create(); + mSwitchingDialog.setCanceledOnTouchOutside(true); + mSwitchingDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG); + mSwitchingDialog.getWindow().getAttributes().privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + mSwitchingDialog.getWindow().getAttributes().setTitle("Select input method"); + mSwitchingDialog.show(); + } + } + + private static class ImeSubtypeListItem implements Comparable { + public final CharSequence mImeName; + public final CharSequence mSubtypeName; + public final InputMethodInfo mImi; + public final int mSubtypeId; + private final boolean mIsSystemLocale; + private final boolean mIsSystemLanguage; + + public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, + InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { + mImeName = imeName; + mSubtypeName = subtypeName; + mImi = imi; + mSubtypeId = subtypeId; + if (TextUtils.isEmpty(subtypeLocale)) { + mIsSystemLocale = false; + mIsSystemLanguage = false; + } else { + mIsSystemLocale = subtypeLocale.equals(systemLocale); + mIsSystemLanguage = mIsSystemLocale + || subtypeLocale.startsWith(systemLocale.substring(0, 2)); + } + } + + @Override + public int compareTo(ImeSubtypeListItem other) { + if (TextUtils.isEmpty(mImeName)) { + return 1; + } + if (TextUtils.isEmpty(other.mImeName)) { + return -1; + } + if (!TextUtils.equals(mImeName, other.mImeName)) { + return mImeName.toString().compareTo(other.mImeName.toString()); + } + if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { + return 0; + } + if (mIsSystemLocale) { + return -1; + } + if (other.mIsSystemLocale) { + return 1; + } + if (mIsSystemLanguage) { + return -1; + } + if (other.mIsSystemLanguage) { + return 1; + } + if (TextUtils.isEmpty(mSubtypeName)) { + return 1; + } + if (TextUtils.isEmpty(other.mSubtypeName)) { + return -1; + } + return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); + } + } + + private static class ImeSubtypeListAdapter extends ArrayAdapter { + private final LayoutInflater mInflater; + private final int mTextViewResourceId; + private final List mItemsList; + public int mCheckedItem; + public ImeSubtypeListAdapter(Context context, int textViewResourceId, + List itemsList, int checkedItem) { + super(context, textViewResourceId, itemsList); + mTextViewResourceId = textViewResourceId; + mItemsList = itemsList; + mCheckedItem = checkedItem; + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View view = convertView != null ? convertView + : mInflater.inflate(mTextViewResourceId, null); + if (position < 0 || position >= mItemsList.size()) return view; + final ImeSubtypeListItem item = mItemsList.get(position); + final CharSequence imeName = item.mImeName; + final CharSequence subtypeName = item.mSubtypeName; + final TextView firstTextView = (TextView)view.findViewById(android.R.id.text1); + final TextView secondTextView = (TextView)view.findViewById(android.R.id.text2); + if (TextUtils.isEmpty(subtypeName)) { + firstTextView.setText(imeName); + secondTextView.setVisibility(View.GONE); + } else { + firstTextView.setText(subtypeName); + secondTextView.setText(imeName); + secondTextView.setVisibility(View.VISIBLE); + } + final RadioButton radioButton = + (RadioButton)view.findViewById(com.android.internal.R.id.radio); + radioButton.setChecked(position == mCheckedItem); + return view; + } + } + + void hideInputMethodMenu() { + synchronized (mMethodMap) { + hideInputMethodMenuLocked(); + } + } + + void hideInputMethodMenuLocked() { + if (DEBUG) Slog.v(TAG, "Hide switching menu"); + + if (mSwitchingDialog != null) { + mSwitchingDialog.dismiss(); + mSwitchingDialog = null; + } + + mDialogBuilder = null; + mIms = null; + } + + // ---------------------------------------------------------------------- + + @Override + public boolean setInputMethodEnabled(String id, boolean enabled) { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_SECURE_SETTINGS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Requires permission " + + android.Manifest.permission.WRITE_SECURE_SETTINGS); + } + + long ident = Binder.clearCallingIdentity(); + try { + return setInputMethodEnabledLocked(id, enabled); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + boolean setInputMethodEnabledLocked(String id, boolean enabled) { + // Make sure this is a valid input method. + InputMethodInfo imm = mMethodMap.get(id); + if (imm == null) { + throw new IllegalArgumentException("Unknown id: " + mCurMethodId); + } + + List>> enabledInputMethodsList = mSettings + .getEnabledInputMethodsAndSubtypeListLocked(); + + if (enabled) { + for (Pair> pair: enabledInputMethodsList) { + if (pair.first.equals(id)) { + // We are enabling this input method, but it is already enabled. + // Nothing to do. The previous state was enabled. + return true; + } + } + mSettings.appendAndPutEnabledInputMethodLocked(id, false); + // Previous state was disabled. + return false; + } else { + StringBuilder builder = new StringBuilder(); + if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + builder, enabledInputMethodsList, id)) { + // Disabled input method is currently selected, switch to another one. + final String selId = mSettings.getSelectedInputMethod(); + if (id.equals(selId) && !chooseNewDefaultIMELocked()) { + Slog.i(TAG, "Can't find new IME, unsetting the current input method."); + resetSelectedInputMethodAndSubtypeLocked(""); + } + // Previous state was enabled. + return true; + } else { + // We are disabling the input method but it is already disabled. + // Nothing to do. The previous state was disabled. + return false; + } + } + } + + private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId, + boolean setSubtypeOnly) { + // Update the history of InputMethod and Subtype + mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype); + + // Set Subtype here + if (imi == null || subtypeId < 0) { + mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID); + mCurrentSubtype = null; + } else { + if (subtypeId < imi.getSubtypeCount()) { + InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId); + mSettings.putSelectedSubtype(subtype.hashCode()); + mCurrentSubtype = subtype; + } else { + mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID); + // If the subtype is not specified, choose the most applicable one + mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); + } + } + + // Workaround. + // ASEC is not ready in the IMMS constructor. Accordingly, forward-locked + // IMEs are not recognized and considered uninstalled. + // Actually, we can't move everything after SystemReady because + // IMMS needs to run in the encryption lock screen. So, we just skip changing + // the default IME here and try cheking the default IME again in systemReady(). + // TODO: Do nothing before system ready and implement a separated logic for + // the encryption lock screen. + // TODO: ASEC should be ready before IMMS is instantiated. + if (mSystemReady && !setSubtypeOnly) { + // Set InputMethod here + mSettings.putSelectedInputMethod(imi != null ? imi.getId() : ""); + } + } + + private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) { + InputMethodInfo imi = mMethodMap.get(newDefaultIme); + int lastSubtypeId = NOT_A_SUBTYPE_ID; + // newDefaultIme is empty when there is no candidate for the selected IME. + if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { + String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); + if (subtypeHashCode != null) { + try { + lastSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( + imi, Integer.valueOf(subtypeHashCode)); + } catch (NumberFormatException e) { + Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e); + } + } + } + setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false); + } + + // If there are no selected shortcuts, tries finding the most applicable ones. + private Pair + findLastResortApplicableShortcutInputMethodAndSubtypeLocked(String mode) { + List imis = mSettings.getEnabledInputMethodListLocked(); + InputMethodInfo mostApplicableIMI = null; + InputMethodSubtype mostApplicableSubtype = null; + boolean foundInSystemIME = false; + + // Search applicable subtype for each InputMethodInfo + for (InputMethodInfo imi: imis) { + final String imiId = imi.getId(); + if (foundInSystemIME && !imiId.equals(mCurMethodId)) { + continue; + } + InputMethodSubtype subtype = null; + final List enabledSubtypes = + mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true); + // 1. Search by the current subtype's locale from enabledSubtypes. + if (mCurrentSubtype != null) { + subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, enabledSubtypes, mode, mCurrentSubtype.getLocale(), false); + } + // 2. Search by the system locale from enabledSubtypes. + // 3. Search the first enabled subtype matched with mode from enabledSubtypes. + if (subtype == null) { + subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, enabledSubtypes, mode, null, true); + } + final ArrayList overridingImplicitlyEnabledSubtypes = + InputMethodUtils.getOverridingImplicitlyEnabledSubtypes(imi, mode); + final ArrayList subtypesForSearch = + overridingImplicitlyEnabledSubtypes.isEmpty() + ? InputMethodUtils.getSubtypes(imi) + : overridingImplicitlyEnabledSubtypes; + // 4. Search by the current subtype's locale from all subtypes. + if (subtype == null && mCurrentSubtype != null) { + subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, subtypesForSearch, mode, mCurrentSubtype.getLocale(), false); + } + // 5. Search by the system locale from all subtypes. + // 6. Search the first enabled subtype matched with mode from all subtypes. + if (subtype == null) { + subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, subtypesForSearch, mode, null, true); + } + if (subtype != null) { + if (imiId.equals(mCurMethodId)) { + // The current input method is the most applicable IME. + mostApplicableIMI = imi; + mostApplicableSubtype = subtype; + break; + } else if (!foundInSystemIME) { + // The system input method is 2nd applicable IME. + mostApplicableIMI = imi; + mostApplicableSubtype = subtype; + if ((imi.getServiceInfo().applicationInfo.flags + & ApplicationInfo.FLAG_SYSTEM) != 0) { + foundInSystemIME = true; + } + } + } + } + if (DEBUG) { + if (mostApplicableIMI != null) { + Slog.w(TAG, "Most applicable shortcut input method was:" + + mostApplicableIMI.getId()); + if (mostApplicableSubtype != null) { + Slog.w(TAG, "Most applicable shortcut input method subtype was:" + + "," + mostApplicableSubtype.getMode() + "," + + mostApplicableSubtype.getLocale()); + } + } + } + if (mostApplicableIMI != null) { + return new Pair (mostApplicableIMI, + mostApplicableSubtype); + } else { + return null; + } + } + + /** + * @return Return the current subtype of this input method. + */ + @Override + public InputMethodSubtype getCurrentInputMethodSubtype() { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return null; + } + synchronized (mMethodMap) { + return getCurrentInputMethodSubtypeLocked(); + } + } + + private InputMethodSubtype getCurrentInputMethodSubtypeLocked() { + if (mCurMethodId == null) { + return null; + } + final boolean subtypeIsSelected = mSettings.isSubtypeSelected(); + final InputMethodInfo imi = mMethodMap.get(mCurMethodId); + if (imi == null || imi.getSubtypeCount() == 0) { + return null; + } + if (!subtypeIsSelected || mCurrentSubtype == null + || !InputMethodUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { + int subtypeId = mSettings.getSelectedInputMethodSubtypeId(mCurMethodId); + if (subtypeId == NOT_A_SUBTYPE_ID) { + // If there are no selected subtypes, the framework will try to find + // the most applicable subtype from explicitly or implicitly enabled + // subtypes. + List explicitlyOrImplicitlyEnabledSubtypes = + mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true); + // If there is only one explicitly or implicitly enabled subtype, + // just returns it. + if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { + mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); + } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { + mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, explicitlyOrImplicitlyEnabledSubtypes, + InputMethodUtils.SUBTYPE_MODE_KEYBOARD, null, true); + if (mCurrentSubtype == null) { + mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, + true); + } + } + } else { + mCurrentSubtype = InputMethodUtils.getSubtypes(imi).get(subtypeId); + } + } + return mCurrentSubtype; + } + + private void addShortcutInputMethodAndSubtypes(InputMethodInfo imi, + InputMethodSubtype subtype) { + if (mShortcutInputMethodsAndSubtypes.containsKey(imi)) { + mShortcutInputMethodsAndSubtypes.get(imi).add(subtype); + } else { + ArrayList subtypes = new ArrayList(); + subtypes.add(subtype); + mShortcutInputMethodsAndSubtypes.put(imi, subtypes); + } + } + + // TODO: We should change the return type from List to List + @SuppressWarnings("rawtypes") + @Override + public List getShortcutInputMethodsAndSubtypes() { + synchronized (mMethodMap) { + ArrayList ret = new ArrayList(); + if (mShortcutInputMethodsAndSubtypes.size() == 0) { + // If there are no selected shortcut subtypes, the framework will try to find + // the most applicable subtype from all subtypes whose mode is + // SUBTYPE_MODE_VOICE. This is an exceptional case, so we will hardcode the mode. + Pair info = + findLastResortApplicableShortcutInputMethodAndSubtypeLocked( + InputMethodUtils.SUBTYPE_MODE_VOICE); + if (info != null) { + ret.add(info.first); + ret.add(info.second); + } + return ret; + } + for (InputMethodInfo imi: mShortcutInputMethodsAndSubtypes.keySet()) { + ret.add(imi); + for (InputMethodSubtype subtype: mShortcutInputMethodsAndSubtypes.get(imi)) { + ret.add(subtype); + } + } + return ret; + } + } + + @Override + public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) { + // TODO: Make this work even for non-current users? + if (!calledFromValidUser()) { + return false; + } + synchronized (mMethodMap) { + if (subtype != null && mCurMethodId != null) { + InputMethodInfo imi = mMethodMap.get(mCurMethodId); + int subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()); + if (subtypeId != NOT_A_SUBTYPE_ID) { + setInputMethodLocked(mCurMethodId, subtypeId); + return true; + } + } + return false; + } + } + + private static class InputMethodAndSubtypeListManager { + private final Context mContext; + // Used to load label + private final PackageManager mPm; + private final InputMethodManagerService mImms; + private final String mSystemLocaleStr; + public InputMethodAndSubtypeListManager(Context context, InputMethodManagerService imms) { + mContext = context; + mPm = context.getPackageManager(); + mImms = imms; + final Locale locale = context.getResources().getConfiguration().locale; + mSystemLocaleStr = locale != null ? locale.toString() : ""; + } + + private final TreeMap> mSortedImmis = + new TreeMap>( + new Comparator() { + @Override + public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { + if (imi2 == null) return 0; + if (imi1 == null) return 1; + if (mPm == null) { + return imi1.getId().compareTo(imi2.getId()); + } + CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId(); + CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId(); + return imiId1.toString().compareTo(imiId2.toString()); + } + }); + + public ImeSubtypeListItem getNextInputMethod( + boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { + if (imi == null) { + return null; + } + final List imList = getSortedInputMethodAndSubtypeList(); + if (imList.size() <= 1) { + return null; + } + final int N = imList.size(); + final int currentSubtypeId = subtype != null + ? InputMethodUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_ID; + for (int i = 0; i < N; ++i) { + final ImeSubtypeListItem isli = imList.get(i); + if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) { + if (!onlyCurrentIme) { + return imList.get((i + 1) % N); + } + for (int j = 0; j < N - 1; ++j) { + final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N); + if (candidate.mImi.equals(imi)) { + return candidate; + } + } + return null; + } + } + return null; + } + + public List getSortedInputMethodAndSubtypeList() { + return getSortedInputMethodAndSubtypeList(true, false, false); + } + + public List getSortedInputMethodAndSubtypeList(boolean showSubtypes, + boolean inputShown, boolean isScreenLocked) { + final ArrayList imList = new ArrayList(); + final HashMap> immis = + mImms.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(); + if (immis == null || immis.size() == 0) { + return Collections.emptyList(); + } + mSortedImmis.clear(); + mSortedImmis.putAll(immis); + for (InputMethodInfo imi : mSortedImmis.keySet()) { + if (imi == null) continue; + List explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); + HashSet enabledSubtypeSet = new HashSet(); + for (InputMethodSubtype subtype: explicitlyOrImplicitlyEnabledSubtypeList) { + enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); + } + final CharSequence imeLabel = imi.loadLabel(mPm); + if (showSubtypes && enabledSubtypeSet.size() > 0) { + final int subtypeCount = imi.getSubtypeCount(); + if (DEBUG) { + Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); + } + for (int j = 0; j < subtypeCount; ++j) { + final InputMethodSubtype subtype = imi.getSubtypeAt(j); + final String subtypeHashCode = String.valueOf(subtype.hashCode()); + // We show all enabled IMEs and subtypes when an IME is shown. + if (enabledSubtypeSet.contains(subtypeHashCode) + && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { + final CharSequence subtypeLabel = + subtype.overridesImplicitlyEnabledSubtype() ? null + : subtype.getDisplayName(mContext, imi.getPackageName(), + imi.getServiceInfo().applicationInfo); + imList.add(new ImeSubtypeListItem(imeLabel, subtypeLabel, imi, j, + subtype.getLocale(), mSystemLocaleStr)); + + // Removing this subtype from enabledSubtypeSet because we no longer + // need to add an entry of this subtype to imList to avoid duplicated + // entries. + enabledSubtypeSet.remove(subtypeHashCode); + } + } + } else { + imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, + null, mSystemLocaleStr)); + } + } + Collections.sort(imList); + return imList; + } + } + + // TODO: Cache the state for each user and reset when the cached user is removed. + private static class InputMethodFileManager { + private static final String SYSTEM_PATH = "system"; + private static final String INPUT_METHOD_PATH = "inputmethod"; + private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml"; + private static final String NODE_SUBTYPES = "subtypes"; + private static final String NODE_SUBTYPE = "subtype"; + private static final String NODE_IMI = "imi"; + private static final String ATTR_ID = "id"; + private static final String ATTR_LABEL = "label"; + private static final String ATTR_ICON = "icon"; + private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale"; + private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode"; + private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue"; + private static final String ATTR_IS_AUXILIARY = "isAuxiliary"; + private final AtomicFile mAdditionalInputMethodSubtypeFile; + private final HashMap mMethodMap; + private final HashMap> mAdditionalSubtypesMap = + new HashMap>(); + public InputMethodFileManager(HashMap methodMap, int userId) { + if (methodMap == null) { + throw new NullPointerException("methodMap is null"); + } + mMethodMap = methodMap; + final File systemDir = userId == UserHandle.USER_OWNER + ? new File(Environment.getDataDirectory(), SYSTEM_PATH) + : Environment.getUserSystemDirectory(userId); + final File inputMethodDir = new File(systemDir, INPUT_METHOD_PATH); + if (!inputMethodDir.mkdirs()) { + Slog.w(TAG, "Couldn't create dir.: " + inputMethodDir.getAbsolutePath()); + } + final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME); + mAdditionalInputMethodSubtypeFile = new AtomicFile(subtypeFile); + if (!subtypeFile.exists()) { + // If "subtypes.xml" doesn't exist, create a blank file. + writeAdditionalInputMethodSubtypes( + mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, methodMap); + } else { + readAdditionalInputMethodSubtypes( + mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile); + } + } + + private void deleteAllInputMethodSubtypes(String imiId) { + synchronized (mMethodMap) { + mAdditionalSubtypesMap.remove(imiId); + writeAdditionalInputMethodSubtypes( + mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap); + } + } + + public void addInputMethodSubtypes( + InputMethodInfo imi, InputMethodSubtype[] additionalSubtypes) { + synchronized (mMethodMap) { + final ArrayList subtypes = new ArrayList(); + final int N = additionalSubtypes.length; + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = additionalSubtypes[i]; + if (!subtypes.contains(subtype)) { + subtypes.add(subtype); + } else { + Slog.w(TAG, "Duplicated subtype definition found: " + + subtype.getLocale() + ", " + subtype.getMode()); + } + } + mAdditionalSubtypesMap.put(imi.getId(), subtypes); + writeAdditionalInputMethodSubtypes( + mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap); + } + } + + public HashMap> getAllAdditionalInputMethodSubtypes() { + synchronized (mMethodMap) { + return mAdditionalSubtypesMap; + } + } + + private static void writeAdditionalInputMethodSubtypes( + HashMap> allSubtypes, AtomicFile subtypesFile, + HashMap methodMap) { + // Safety net for the case that this function is called before methodMap is set. + final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; + FileOutputStream fos = null; + try { + fos = subtypesFile.startWrite(); + final XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, "utf-8"); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + out.startTag(null, NODE_SUBTYPES); + for (String imiId : allSubtypes.keySet()) { + if (isSetMethodMap && !methodMap.containsKey(imiId)) { + Slog.w(TAG, "IME uninstalled or not valid.: " + imiId); + continue; + } + out.startTag(null, NODE_IMI); + out.attribute(null, ATTR_ID, imiId); + final List subtypesList = allSubtypes.get(imiId); + final int N = subtypesList.size(); + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = subtypesList.get(i); + out.startTag(null, NODE_SUBTYPE); + out.attribute(null, ATTR_ICON, String.valueOf(subtype.getIconResId())); + out.attribute(null, ATTR_LABEL, String.valueOf(subtype.getNameResId())); + out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale()); + out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode()); + out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue()); + out.attribute(null, ATTR_IS_AUXILIARY, + String.valueOf(subtype.isAuxiliary() ? 1 : 0)); + out.endTag(null, NODE_SUBTYPE); + } + out.endTag(null, NODE_IMI); + } + out.endTag(null, NODE_SUBTYPES); + out.endDocument(); + subtypesFile.finishWrite(fos); + } catch (java.io.IOException e) { + Slog.w(TAG, "Error writing subtypes", e); + if (fos != null) { + subtypesFile.failWrite(fos); + } + } + } + + private static void readAdditionalInputMethodSubtypes( + HashMap> allSubtypes, AtomicFile subtypesFile) { + if (allSubtypes == null || subtypesFile == null) return; + allSubtypes.clear(); + FileInputStream fis = null; + try { + fis = subtypesFile.openRead(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + int type = parser.getEventType(); + // Skip parsing until START_TAG + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) {} + String firstNodeName = parser.getName(); + if (!NODE_SUBTYPES.equals(firstNodeName)) { + throw new XmlPullParserException("Xml doesn't start with subtypes"); + } + final int depth =parser.getDepth(); + String currentImiId = null; + ArrayList tempSubtypesArray = null; + while (((type = parser.next()) != XmlPullParser.END_TAG + || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) + continue; + final String nodeName = parser.getName(); + if (NODE_IMI.equals(nodeName)) { + currentImiId = parser.getAttributeValue(null, ATTR_ID); + if (TextUtils.isEmpty(currentImiId)) { + Slog.w(TAG, "Invalid imi id found in subtypes.xml"); + continue; + } + tempSubtypesArray = new ArrayList(); + allSubtypes.put(currentImiId, tempSubtypesArray); + } else if (NODE_SUBTYPE.equals(nodeName)) { + if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) { + Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId); + continue; + } + final int icon = Integer.valueOf( + parser.getAttributeValue(null, ATTR_ICON)); + final int label = Integer.valueOf( + parser.getAttributeValue(null, ATTR_LABEL)); + final String imeSubtypeLocale = + parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE); + final String imeSubtypeMode = + parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE); + final String imeSubtypeExtraValue = + parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE); + final boolean isAuxiliary = "1".equals(String.valueOf( + parser.getAttributeValue(null, ATTR_IS_AUXILIARY))); + final InputMethodSubtype subtype = + new InputMethodSubtype(label, icon, imeSubtypeLocale, + imeSubtypeMode, imeSubtypeExtraValue, isAuxiliary); + tempSubtypesArray.add(subtype); + } + } + } catch (XmlPullParserException e) { + Slog.w(TAG, "Error reading subtypes: " + e); + return; + } catch (java.io.IOException e) { + Slog.w(TAG, "Error reading subtypes: " + e); + return; + } catch (NumberFormatException e) { + Slog.w(TAG, "Error reading subtypes: " + e); + return; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (java.io.IOException e1) { + Slog.w(TAG, "Failed to close."); + } + } + } + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + + pw.println("Permission Denial: can't dump InputMethodManager from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + IInputMethod method; + ClientState client; + + final Printer p = new PrintWriterPrinter(pw); + + synchronized (mMethodMap) { + p.println("Current Input Method Manager state:"); + int N = mMethodList.size(); + p.println(" Input Methods:"); + for (int i=0; i { + final private static String TAG = "IntentResolver"; + final private static boolean DEBUG = false; + final private static boolean localLOGV = DEBUG || false; + + public void addFilter(F f) { + if (localLOGV) { + Slog.v(TAG, "Adding filter: " + f); + f.dump(new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM), " "); + Slog.v(TAG, " Building Lookup Maps:"); + } + + mFilters.add(f); + int numS = register_intent_filter(f, f.schemesIterator(), + mSchemeToFilter, " Scheme: "); + int numT = register_mime_types(f, " Type: "); + if (numS == 0 && numT == 0) { + register_intent_filter(f, f.actionsIterator(), + mActionToFilter, " Action: "); + } + if (numT != 0) { + register_intent_filter(f, f.actionsIterator(), + mTypedActionToFilter, " TypedAction: "); + } + } + + public void removeFilter(F f) { + removeFilterInternal(f); + mFilters.remove(f); + } + + void removeFilterInternal(F f) { + if (localLOGV) { + Slog.v(TAG, "Removing filter: " + f); + f.dump(new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM), " "); + Slog.v(TAG, " Cleaning Lookup Maps:"); + } + + int numS = unregister_intent_filter(f, f.schemesIterator(), + mSchemeToFilter, " Scheme: "); + int numT = unregister_mime_types(f, " Type: "); + if (numS == 0 && numT == 0) { + unregister_intent_filter(f, f.actionsIterator(), + mActionToFilter, " Action: "); + } + if (numT != 0) { + unregister_intent_filter(f, f.actionsIterator(), + mTypedActionToFilter, " TypedAction: "); + } + } + + boolean dumpMap(PrintWriter out, String titlePrefix, String title, + String prefix, Map map, String packageName, + boolean printFilter) { + String eprefix = prefix + " "; + String fprefix = prefix + " "; + boolean printedSomething = false; + Printer printer = null; + for (Map.Entry e : map.entrySet()) { + F[] a = e.getValue(); + final int N = a.length; + boolean printedHeader = false; + F filter; + for (int i=0; i { + private final Iterator mI; + private F mCur; + + IteratorWrapper(Iterator it) { + mI = it; + } + + public boolean hasNext() { + return mI.hasNext(); + } + + public F next() { + return (mCur = mI.next()); + } + + public void remove() { + if (mCur != null) { + removeFilterInternal(mCur); + } + mI.remove(); + } + + } + + /** + * Returns an iterator allowing filters to be removed. + */ + public Iterator filterIterator() { + return new IteratorWrapper(mFilters.iterator()); + } + + /** + * Returns a read-only set of the filters. + */ + public Set filterSet() { + return Collections.unmodifiableSet(mFilters); + } + + public List queryIntentFromList(Intent intent, String resolvedType, + boolean defaultOnly, ArrayList listCut, int userId) { + ArrayList resultList = new ArrayList(); + + final boolean debug = localLOGV || + ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0); + + FastImmutableArraySet categories = getFastIntentCategories(intent); + final String scheme = intent.getScheme(); + int N = listCut.size(); + for (int i = 0; i < N; ++i) { + buildResolveList(intent, categories, debug, defaultOnly, + resolvedType, scheme, listCut.get(i), resultList, userId); + } + sortResults(resultList); + return resultList; + } + + public List queryIntent(Intent intent, String resolvedType, boolean defaultOnly, + int userId) { + String scheme = intent.getScheme(); + + ArrayList finalList = new ArrayList(); + + final boolean debug = localLOGV || + ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0); + + if (debug) Slog.v( + TAG, "Resolving type=" + resolvedType + " scheme=" + scheme + + " defaultOnly=" + defaultOnly + " userId=" + userId + " of " + intent); + + F[] firstTypeCut = null; + F[] secondTypeCut = null; + F[] thirdTypeCut = null; + F[] schemeCut = null; + + // If the intent includes a MIME type, then we want to collect all of + // the filters that match that MIME type. + if (resolvedType != null) { + int slashpos = resolvedType.indexOf('/'); + if (slashpos > 0) { + final String baseType = resolvedType.substring(0, slashpos); + if (!baseType.equals("*")) { + if (resolvedType.length() != slashpos+2 + || resolvedType.charAt(slashpos+1) != '*') { + // Not a wild card, so we can just look for all filters that + // completely match or wildcards whose base type matches. + firstTypeCut = mTypeToFilter.get(resolvedType); + if (debug) Slog.v(TAG, "First type cut: " + Arrays.toString(firstTypeCut)); + secondTypeCut = mWildTypeToFilter.get(baseType); + if (debug) Slog.v(TAG, "Second type cut: " + + Arrays.toString(secondTypeCut)); + } else { + // We can match anything with our base type. + firstTypeCut = mBaseTypeToFilter.get(baseType); + if (debug) Slog.v(TAG, "First type cut: " + Arrays.toString(firstTypeCut)); + secondTypeCut = mWildTypeToFilter.get(baseType); + if (debug) Slog.v(TAG, "Second type cut: " + + Arrays.toString(secondTypeCut)); + } + // Any */* types always apply, but we only need to do this + // if the intent type was not already */*. + thirdTypeCut = mWildTypeToFilter.get("*"); + if (debug) Slog.v(TAG, "Third type cut: " + Arrays.toString(thirdTypeCut)); + } else if (intent.getAction() != null) { + // The intent specified any type ({@literal *}/*). This + // can be a whole heck of a lot of things, so as a first + // cut let's use the action instead. + firstTypeCut = mTypedActionToFilter.get(intent.getAction()); + if (debug) Slog.v(TAG, "Typed Action list: " + Arrays.toString(firstTypeCut)); + } + } + } + + // If the intent includes a data URI, then we want to collect all of + // the filters that match its scheme (we will further refine matches + // on the authority and path by directly matching each resulting filter). + if (scheme != null) { + schemeCut = mSchemeToFilter.get(scheme); + if (debug) Slog.v(TAG, "Scheme list: " + Arrays.toString(schemeCut)); + } + + // If the intent does not specify any data -- either a MIME type or + // a URI -- then we will only be looking for matches against empty + // data. + if (resolvedType == null && scheme == null && intent.getAction() != null) { + firstTypeCut = mActionToFilter.get(intent.getAction()); + if (debug) Slog.v(TAG, "Action list: " + Arrays.toString(firstTypeCut)); + } + + FastImmutableArraySet categories = getFastIntentCategories(intent); + if (firstTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, + resolvedType, scheme, firstTypeCut, finalList, userId); + } + if (secondTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, + resolvedType, scheme, secondTypeCut, finalList, userId); + } + if (thirdTypeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, + resolvedType, scheme, thirdTypeCut, finalList, userId); + } + if (schemeCut != null) { + buildResolveList(intent, categories, debug, defaultOnly, + resolvedType, scheme, schemeCut, finalList, userId); + } + sortResults(finalList); + + if (debug) { + Slog.v(TAG, "Final result list:"); + for (int i=0; i dest) { + return true; + } + + /** + * Returns whether the object associated with the given filter is + * "stopped," that is whether it should not be included in the result + * if the intent requests to excluded stopped objects. + */ + protected boolean isFilterStopped(F filter, int userId) { + return false; + } + + /** + * Returns whether this filter is owned by this package. This must be + * implemented to provide correct filtering of Intents that have + * specified a package name they are to be delivered to. + */ + protected abstract boolean isPackageForFilter(String packageName, F filter); + + protected abstract F[] newArray(int size); + + @SuppressWarnings("unchecked") + protected R newResult(F filter, int match, int userId) { + return (R)filter; + } + + @SuppressWarnings("unchecked") + protected void sortResults(List results) { + Collections.sort(results, mResolvePrioritySorter); + } + + protected void dumpFilter(PrintWriter out, String prefix, F filter) { + out.print(prefix); out.println(filter); + } + + private final void addFilter(ArrayMap map, String name, F filter) { + F[] array = map.get(name); + if (array == null) { + array = newArray(2); + map.put(name, array); + array[0] = filter; + } else { + final int N = array.length; + int i = N; + while (i > 0 && array[i-1] == null) { + i--; + } + if (i < N) { + array[i] = filter; + } else { + F[] newa = newArray((N*3)/2); + System.arraycopy(array, 0, newa, 0, N); + newa[N] = filter; + map.put(name, newa); + } + } + } + + private final int register_mime_types(F filter, String prefix) { + final Iterator i = filter.typesIterator(); + if (i == null) { + return 0; + } + + int num = 0; + while (i.hasNext()) { + String name = i.next(); + num++; + if (localLOGV) Slog.v(TAG, prefix + name); + String baseName = name; + final int slashpos = name.indexOf('/'); + if (slashpos > 0) { + baseName = name.substring(0, slashpos).intern(); + } else { + name = name + "/*"; + } + + addFilter(mTypeToFilter, name, filter); + + if (slashpos > 0) { + addFilter(mBaseTypeToFilter, baseName, filter); + } else { + addFilter(mWildTypeToFilter, baseName, filter); + } + } + + return num; + } + + private final int unregister_mime_types(F filter, String prefix) { + final Iterator i = filter.typesIterator(); + if (i == null) { + return 0; + } + + int num = 0; + while (i.hasNext()) { + String name = i.next(); + num++; + if (localLOGV) Slog.v(TAG, prefix + name); + String baseName = name; + final int slashpos = name.indexOf('/'); + if (slashpos > 0) { + baseName = name.substring(0, slashpos).intern(); + } else { + name = name + "/*"; + } + + remove_all_objects(mTypeToFilter, name, filter); + + if (slashpos > 0) { + remove_all_objects(mBaseTypeToFilter, baseName, filter); + } else { + remove_all_objects(mWildTypeToFilter, baseName, filter); + } + } + return num; + } + + private final int register_intent_filter(F filter, Iterator i, + ArrayMap dest, String prefix) { + if (i == null) { + return 0; + } + + int num = 0; + while (i.hasNext()) { + String name = i.next(); + num++; + if (localLOGV) Slog.v(TAG, prefix + name); + addFilter(dest, name, filter); + } + return num; + } + + private final int unregister_intent_filter(F filter, Iterator i, + ArrayMap dest, String prefix) { + if (i == null) { + return 0; + } + + int num = 0; + while (i.hasNext()) { + String name = i.next(); + num++; + if (localLOGV) Slog.v(TAG, prefix + name); + remove_all_objects(dest, name, filter); + } + return num; + } + + private final void remove_all_objects(ArrayMap map, String name, + Object object) { + F[] array = map.get(name); + if (array != null) { + int LAST = array.length-1; + while (LAST >= 0 && array[LAST] == null) { + LAST--; + } + for (int idx=LAST; idx>=0; idx--) { + if (array[idx] == object) { + final int remain = LAST - idx; + if (remain > 0) { + System.arraycopy(array, idx+1, array, idx, remain); + } + array[LAST] = null; + LAST--; + } + } + if (LAST < 0) { + map.remove(name); + } else if (LAST < (array.length/2)) { + F[] newa = newArray(LAST+2); + System.arraycopy(array, 0, newa, 0, LAST+1); + map.put(name, newa); + } + } + } + + private static FastImmutableArraySet getFastIntentCategories(Intent intent) { + final Set categories = intent.getCategories(); + if (categories == null) { + return null; + } + return new FastImmutableArraySet(categories.toArray(new String[categories.size()])); + } + + private void buildResolveList(Intent intent, FastImmutableArraySet categories, + boolean debug, boolean defaultOnly, + String resolvedType, String scheme, F[] src, List dest, int userId) { + final String action = intent.getAction(); + final Uri data = intent.getData(); + final String packageName = intent.getPackage(); + + final boolean excludingStopped = intent.isExcludingStopped(); + + final Printer logPrinter; + final PrintWriter logPrintWriter; + if (debug) { + logPrinter = new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM); + logPrintWriter = new FastPrintWriter(logPrinter); + } else { + logPrinter = null; + logPrintWriter = null; + } + + final int N = src != null ? src.length : 0; + boolean hasNonDefaults = false; + int i; + F filter; + for (i=0; i= 0) { + if (debug) Slog.v(TAG, " Filter matched! match=0x" + + Integer.toHexString(match) + " hasDefault=" + + filter.hasCategory(Intent.CATEGORY_DEFAULT)); + if (!defaultOnly || filter.hasCategory(Intent.CATEGORY_DEFAULT)) { + final R oneResult = newResult(filter, match, userId); + if (oneResult != null) { + dest.add(oneResult); + if (debug) { + dumpFilter(logPrintWriter, " ", filter); + logPrintWriter.flush(); + filter.dump(logPrinter, " "); + } + } + } else { + hasNonDefaults = true; + } + } else { + if (debug) { + String reason; + switch (match) { + case IntentFilter.NO_MATCH_ACTION: reason = "action"; break; + case IntentFilter.NO_MATCH_CATEGORY: reason = "category"; break; + case IntentFilter.NO_MATCH_DATA: reason = "data"; break; + case IntentFilter.NO_MATCH_TYPE: reason = "type"; break; + default: reason = "unknown reason"; break; + } + Slog.v(TAG, " Filter did not match: " + reason); + } + } + } + + if (hasNonDefaults) { + if (dest.size() == 0) { + Slog.w(TAG, "resolveIntent failed: found match, but none with CATEGORY_DEFAULT"); + } else if (dest.size() > 1) { + Slog.w(TAG, "resolveIntent: multiple matches, only some with CATEGORY_DEFAULT"); + } + } + } + + // Sorts a List of IntentFilter objects into descending priority order. + @SuppressWarnings("rawtypes") + private static final Comparator mResolvePrioritySorter = new Comparator() { + public int compare(Object o1, Object o2) { + final int q1 = ((IntentFilter) o1).getPriority(); + final int q2 = ((IntentFilter) o2).getPriority(); + return (q1 > q2) ? -1 : ((q1 < q2) ? 1 : 0); + } + }; + + /** + * All filters that have been registered. + */ + private final HashSet mFilters = new HashSet(); + + /** + * All of the MIME types that have been registered, such as "image/jpeg", + * "image/*", or "{@literal *}/*". + */ + private final ArrayMap mTypeToFilter = new ArrayMap(); + + /** + * The base names of all of all fully qualified MIME types that have been + * registered, such as "image" or "*". Wild card MIME types such as + * "image/*" will not be here. + */ + private final ArrayMap mBaseTypeToFilter = new ArrayMap(); + + /** + * The base names of all of the MIME types with a sub-type wildcard that + * have been registered. For example, a filter with "image/*" will be + * included here as "image" but one with "image/jpeg" will not be + * included here. This also includes the "*" for the "{@literal *}/*" + * MIME type. + */ + private final ArrayMap mWildTypeToFilter = new ArrayMap(); + + /** + * All of the URI schemes (such as http) that have been registered. + */ + private final ArrayMap mSchemeToFilter = new ArrayMap(); + + /** + * All of the actions that have been registered, but only those that did + * not specify data. + */ + private final ArrayMap mActionToFilter = new ArrayMap(); + + /** + * All of the actions that have been registered and specified a MIME type. + */ + private final ArrayMap mTypedActionToFilter = new ArrayMap(); +} diff --git a/services/core/java/com/android/server/IoThread.java b/services/core/java/com/android/server/IoThread.java new file mode 100644 index 0000000..09f2af7 --- /dev/null +++ b/services/core/java/com/android/server/IoThread.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.os.Handler; +import android.os.HandlerThread; + +/** + * Shared singleton I/O thread for the system. This is a thread for non-background + * service operations that can potential block briefly on network IO operations + * (not waiting for data itself, but communicating with network daemons). + */ +public final class IoThread extends HandlerThread { + private static IoThread sInstance; + private static Handler sHandler; + + private IoThread() { + super("android.io", android.os.Process.THREAD_PRIORITY_DEFAULT); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new IoThread(); + sInstance.start(); + sHandler = new Handler(sInstance.getLooper()); + sHandler.post(new Runnable() { + @Override + public void run() { + android.os.Process.setCanSelfBackground(false); + } + }); + } + } + + public static IoThread get() { + synchronized (IoThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + + public static Handler getHandler() { + synchronized (IoThread.class) { + ensureThreadLocked(); + return sHandler; + } + } +} diff --git a/services/core/java/com/android/server/LocalServices.java b/services/core/java/com/android/server/LocalServices.java new file mode 100644 index 0000000..deff79d --- /dev/null +++ b/services/core/java/com/android/server/LocalServices.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.util.ArrayMap; + +/** + * This class is used in a similar way as ServiceManager, except the services registered here + * are not Binder objects and are only available in the same process. + * + * Once all services are converted to the SystemService interface, this class can be absorbed + * into SystemServiceManager. + */ +public final class LocalServices { + private LocalServices() {} + + private static final ArrayMap, Object> sLocalServiceObjects = + new ArrayMap, Object>(); + + /** + * Returns a local service instance that implements the specified interface. + * + * @param type The type of service. + * @return The service object. + */ + @SuppressWarnings("unchecked") + public static T getService(Class type) { + synchronized (sLocalServiceObjects) { + return (T) sLocalServiceObjects.get(type); + } + } + + /** + * Adds a service instance of the specified interface to the global registry of local services. + */ + public static void addService(Class type, T service) { + synchronized (sLocalServiceObjects) { + if (sLocalServiceObjects.containsKey(type)) { + throw new IllegalStateException("Overriding service registration"); + } + sLocalServiceObjects.put(type, service); + } + } +} diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java new file mode 100644 index 0000000..8f480dd --- /dev/null +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -0,0 +1,2411 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.AppOpsManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.location.Address; +import android.location.Criteria; +import android.location.GeocoderParams; +import android.location.Geofence; +import android.location.IGpsStatusListener; +import android.location.IGpsStatusProvider; +import android.location.ILocationListener; +import android.location.ILocationManager; +import android.location.INetInitiatedListener; +import android.location.Location; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.location.LocationRequest; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.WorkSource; +import android.provider.Settings; +import android.util.Log; +import android.util.Slog; +import com.android.internal.content.PackageMonitor; +import com.android.internal.location.ProviderProperties; +import com.android.internal.location.ProviderRequest; +import com.android.internal.os.BackgroundThread; +import com.android.server.location.FlpHardwareProvider; +import com.android.server.location.FusedProxy; +import com.android.server.location.GeocoderProxy; +import com.android.server.location.GeofenceProxy; +import com.android.server.location.GeofenceManager; +import com.android.server.location.GpsLocationProvider; +import com.android.server.location.LocationBlacklist; +import com.android.server.location.LocationFudger; +import com.android.server.location.LocationProviderInterface; +import com.android.server.location.LocationProviderProxy; +import com.android.server.location.MockProvider; +import com.android.server.location.PassiveProvider; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The service class that manages LocationProviders and issues location + * updates and alerts. + */ +public class LocationManagerService extends ILocationManager.Stub { + private static final String TAG = "LocationManagerService"; + public static final boolean D = Log.isLoggable(TAG, Log.DEBUG); + + private static final String WAKELOCK_KEY = TAG; + + // Location resolution level: no location data whatsoever + private static final int RESOLUTION_LEVEL_NONE = 0; + // Location resolution level: coarse location data only + private static final int RESOLUTION_LEVEL_COARSE = 1; + // Location resolution level: fine location data + private static final int RESOLUTION_LEVEL_FINE = 2; + + private static final String ACCESS_MOCK_LOCATION = + android.Manifest.permission.ACCESS_MOCK_LOCATION; + private static final String ACCESS_LOCATION_EXTRA_COMMANDS = + android.Manifest.permission.ACCESS_LOCATION_EXTRA_COMMANDS; + private static final String INSTALL_LOCATION_PROVIDER = + android.Manifest.permission.INSTALL_LOCATION_PROVIDER; + + private static final String NETWORK_LOCATION_SERVICE_ACTION = + "com.android.location.service.v3.NetworkLocationProvider"; + private static final String FUSED_LOCATION_SERVICE_ACTION = + "com.android.location.service.FusedLocationProvider"; + + private static final int MSG_LOCATION_CHANGED = 1; + + private static final long NANOS_PER_MILLI = 1000000L; + + // The maximum interval a location request can have and still be considered "high power". + private static final long HIGH_POWER_INTERVAL_MS = 5 * 60 * 1000; + + // Location Providers may sometimes deliver location updates + // slightly faster that requested - provide grace period so + // we don't unnecessarily filter events that are otherwise on + // time + private static final int MAX_PROVIDER_SCHEDULING_JITTER_MS = 100; + + private static final LocationRequest DEFAULT_LOCATION_REQUEST = new LocationRequest(); + + private final Context mContext; + private final AppOpsManager mAppOps; + + // used internally for synchronization + private final Object mLock = new Object(); + + // --- fields below are final after systemReady() --- + private LocationFudger mLocationFudger; + private GeofenceManager mGeofenceManager; + private PackageManager mPackageManager; + private PowerManager mPowerManager; + private GeocoderProxy mGeocodeProvider; + private IGpsStatusProvider mGpsStatusProvider; + private INetInitiatedListener mNetInitiatedListener; + private LocationWorkerHandler mLocationHandler; + private PassiveProvider mPassiveProvider; // track passive provider for special cases + private LocationBlacklist mBlacklist; + + // --- fields below are protected by mLock --- + // Set of providers that are explicitly enabled + private final Set mEnabledProviders = new HashSet(); + + // Set of providers that are explicitly disabled + private final Set mDisabledProviders = new HashSet(); + + // Mock (test) providers + private final HashMap mMockProviders = + new HashMap(); + + // all receivers + private final HashMap mReceivers = new HashMap(); + + // currently installed providers (with mocks replacing real providers) + private final ArrayList mProviders = + new ArrayList(); + + // real providers, saved here when mocked out + private final HashMap mRealProviders = + new HashMap(); + + // mapping from provider name to provider + private final HashMap mProvidersByName = + new HashMap(); + + // mapping from provider name to all its UpdateRecords + private final HashMap> mRecordsByProvider = + new HashMap>(); + + // mapping from provider name to last known location + private final HashMap mLastLocation = new HashMap(); + + // same as mLastLocation, but is not updated faster than LocationFudger.FASTEST_INTERVAL_MS. + // locations stored here are not fudged for coarse permissions. + private final HashMap mLastLocationCoarseInterval = + new HashMap(); + + // all providers that operate over proxy, for authorizing incoming location + private final ArrayList mProxyProviders = + new ArrayList(); + + // current active user on the device - other users are denied location data + private int mCurrentUserId = UserHandle.USER_OWNER; + + public LocationManagerService(Context context) { + super(); + mContext = context; + mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); + + if (D) Log.d(TAG, "Constructed"); + + // most startup is deferred until systemReady() + } + + public void systemRunning() { + synchronized (mLock) { + if (D) Log.d(TAG, "systemReady()"); + + // fetch package manager + mPackageManager = mContext.getPackageManager(); + + // fetch power manager + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + + // prepare worker thread + mLocationHandler = new LocationWorkerHandler(BackgroundThread.get().getLooper()); + + // prepare mLocationHandler's dependents + mLocationFudger = new LocationFudger(mContext, mLocationHandler); + mBlacklist = new LocationBlacklist(mContext, mLocationHandler); + mBlacklist.init(); + mGeofenceManager = new GeofenceManager(mContext, mBlacklist); + + // Monitor for app ops mode changes. + AppOpsManager.OnOpChangedListener callback + = new AppOpsManager.OnOpChangedInternalListener() { + public void onOpChanged(int op, String packageName) { + synchronized (mLock) { + for (Receiver receiver : mReceivers.values()) { + receiver.updateMonitoring(true); + } + applyAllProviderRequirementsLocked(); + } + } + }; + mAppOps.startWatchingMode(AppOpsManager.OP_COARSE_LOCATION, null, callback); + + // prepare providers + loadProvidersLocked(); + updateProvidersLocked(); + } + + // listen for settings changes + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCATION_PROVIDERS_ALLOWED), true, + new ContentObserver(mLocationHandler) { + @Override + public void onChange(boolean selfChange) { + synchronized (mLock) { + updateProvidersLocked(); + } + } + }, UserHandle.USER_ALL); + mPackageMonitor.register(mContext, mLocationHandler.getLooper(), true); + + // listen for user change + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_SWITCHED.equals(action)) { + switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } + } + }, UserHandle.ALL, intentFilter, null, mLocationHandler); + } + + private void ensureFallbackFusedProviderPresentLocked(ArrayList pkgs) { + PackageManager pm = mContext.getPackageManager(); + String systemPackageName = mContext.getPackageName(); + ArrayList> sigSets = ServiceWatcher.getSignatureSets(mContext, pkgs); + + List rInfos = pm.queryIntentServicesAsUser( + new Intent(FUSED_LOCATION_SERVICE_ACTION), + PackageManager.GET_META_DATA, mCurrentUserId); + for (ResolveInfo rInfo : rInfos) { + String packageName = rInfo.serviceInfo.packageName; + + // Check that the signature is in the list of supported sigs. If it's not in + // this list the standard provider binding logic won't bind to it. + try { + PackageInfo pInfo; + pInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + if (!ServiceWatcher.isSignatureMatch(pInfo.signatures, sigSets)) { + Log.w(TAG, packageName + " resolves service " + FUSED_LOCATION_SERVICE_ACTION + + ", but has wrong signature, ignoring"); + continue; + } + } catch (NameNotFoundException e) { + Log.e(TAG, "missing package: " + packageName); + continue; + } + + // Get the version info + if (rInfo.serviceInfo.metaData == null) { + Log.w(TAG, "Found fused provider without metadata: " + packageName); + continue; + } + + int version = rInfo.serviceInfo.metaData.getInt( + ServiceWatcher.EXTRA_SERVICE_VERSION, -1); + if (version == 0) { + // This should be the fallback fused location provider. + + // Make sure it's in the system partition. + if ((rInfo.serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + if (D) Log.d(TAG, "Fallback candidate not in /system: " + packageName); + continue; + } + + // Check that the fallback is signed the same as the OS + // as a proxy for coreApp="true" + if (pm.checkSignatures(systemPackageName, packageName) + != PackageManager.SIGNATURE_MATCH) { + if (D) Log.d(TAG, "Fallback candidate not signed the same as system: " + + packageName); + continue; + } + + // Found a valid fallback. + if (D) Log.d(TAG, "Found fallback provider: " + packageName); + return; + } else { + if (D) Log.d(TAG, "Fallback candidate not version 0: " + packageName); + } + } + + throw new IllegalStateException("Unable to find a fused location provider that is in the " + + "system partition with version 0 and signed with the platform certificate. " + + "Such a package is needed to provide a default fused location provider in the " + + "event that no other fused location provider has been installed or is currently " + + "available. For example, coreOnly boot mode when decrypting the data " + + "partition. The fallback must also be marked coreApp=\"true\" in the manifest"); + } + + private void loadProvidersLocked() { + // create a passive location provider, which is always enabled + PassiveProvider passiveProvider = new PassiveProvider(this); + addProviderLocked(passiveProvider); + mEnabledProviders.add(passiveProvider.getName()); + mPassiveProvider = passiveProvider; + // Create a gps location provider + GpsLocationProvider gpsProvider = new GpsLocationProvider(mContext, this, + mLocationHandler.getLooper()); + + if (GpsLocationProvider.isSupported()) { + mGpsStatusProvider = gpsProvider.getGpsStatusProvider(); + mNetInitiatedListener = gpsProvider.getNetInitiatedListener(); + addProviderLocked(gpsProvider); + mRealProviders.put(LocationManager.GPS_PROVIDER, gpsProvider); + } + + /* + Load package name(s) containing location provider support. + These packages can contain services implementing location providers: + Geocoder Provider, Network Location Provider, and + Fused Location Provider. They will each be searched for + service components implementing these providers. + The location framework also has support for installation + of new location providers at run-time. The new package does not + have to be explicitly listed here, however it must have a signature + that matches the signature of at least one package on this list. + */ + Resources resources = mContext.getResources(); + ArrayList providerPackageNames = new ArrayList(); + String[] pkgs = resources.getStringArray( + com.android.internal.R.array.config_locationProviderPackageNames); + if (D) Log.d(TAG, "certificates for location providers pulled from: " + + Arrays.toString(pkgs)); + if (pkgs != null) providerPackageNames.addAll(Arrays.asList(pkgs)); + + ensureFallbackFusedProviderPresentLocked(providerPackageNames); + + // bind to network provider + LocationProviderProxy networkProvider = LocationProviderProxy.createAndBind( + mContext, + LocationManager.NETWORK_PROVIDER, + NETWORK_LOCATION_SERVICE_ACTION, + com.android.internal.R.bool.config_enableNetworkLocationOverlay, + com.android.internal.R.string.config_networkLocationProviderPackageName, + com.android.internal.R.array.config_locationProviderPackageNames, + mLocationHandler); + if (networkProvider != null) { + mRealProviders.put(LocationManager.NETWORK_PROVIDER, networkProvider); + mProxyProviders.add(networkProvider); + addProviderLocked(networkProvider); + } else { + Slog.w(TAG, "no network location provider found"); + } + + // bind to fused provider + LocationProviderProxy fusedLocationProvider = LocationProviderProxy.createAndBind( + mContext, + LocationManager.FUSED_PROVIDER, + FUSED_LOCATION_SERVICE_ACTION, + com.android.internal.R.bool.config_enableFusedLocationOverlay, + com.android.internal.R.string.config_fusedLocationProviderPackageName, + com.android.internal.R.array.config_locationProviderPackageNames, + mLocationHandler); + if (fusedLocationProvider != null) { + addProviderLocked(fusedLocationProvider); + mProxyProviders.add(fusedLocationProvider); + mEnabledProviders.add(fusedLocationProvider.getName()); + mRealProviders.put(LocationManager.FUSED_PROVIDER, fusedLocationProvider); + } else { + Slog.e(TAG, "no fused location provider found", + new IllegalStateException("Location service needs a fused location provider")); + } + + // bind to geocoder provider + mGeocodeProvider = GeocoderProxy.createAndBind(mContext, + com.android.internal.R.bool.config_enableGeocoderOverlay, + com.android.internal.R.string.config_geocoderProviderPackageName, + com.android.internal.R.array.config_locationProviderPackageNames, + mLocationHandler); + if (mGeocodeProvider == null) { + Slog.e(TAG, "no geocoder provider found"); + } + + // bind to fused provider + FlpHardwareProvider flpHardwareProvider = FlpHardwareProvider.getInstance(mContext); + FusedProxy fusedProxy = FusedProxy.createAndBind( + mContext, + mLocationHandler, + flpHardwareProvider.getLocationHardware(), + com.android.internal.R.bool.config_enableFusedLocationOverlay, + com.android.internal.R.string.config_fusedLocationProviderPackageName, + com.android.internal.R.array.config_locationProviderPackageNames); + if(fusedProxy == null) { + Slog.e(TAG, "No FusedProvider found."); + } + + // bind to geofence provider + GeofenceProxy provider = GeofenceProxy.createAndBind(mContext, + com.android.internal.R.bool.config_enableGeofenceOverlay, + com.android.internal.R.string.config_geofenceProviderPackageName, + com.android.internal.R.array.config_locationProviderPackageNames, + mLocationHandler, + gpsProvider.getGpsGeofenceProxy(), + flpHardwareProvider.getGeofenceHardware()); + if (provider == null) { + Slog.e(TAG, "no geofence provider found"); + } + } + + /** + * Called when the device's active user changes. + * @param userId the new active user's UserId + */ + private void switchUser(int userId) { + mBlacklist.switchUser(userId); + mLocationHandler.removeMessages(MSG_LOCATION_CHANGED); + synchronized (mLock) { + mLastLocation.clear(); + mLastLocationCoarseInterval.clear(); + for (LocationProviderInterface p : mProviders) { + updateProviderListenersLocked(p.getName(), false, mCurrentUserId); + } + mCurrentUserId = userId; + updateProvidersLocked(); + } + } + + /** + * A wrapper class holding either an ILocationListener or a PendingIntent to receive + * location updates. + */ + private final class Receiver implements IBinder.DeathRecipient, PendingIntent.OnFinished { + final int mUid; // uid of receiver + final int mPid; // pid of receiver + final String mPackageName; // package name of receiver + final int mAllowedResolutionLevel; // resolution level allowed to receiver + + final ILocationListener mListener; + final PendingIntent mPendingIntent; + final WorkSource mWorkSource; // WorkSource for battery blame, or null to assign to caller. + final boolean mHideFromAppOps; // True if AppOps should not monitor this receiver. + final Object mKey; + + final HashMap mUpdateRecords = new HashMap(); + + // True if app ops has started monitoring this receiver for locations. + boolean mOpMonitoring; + // True if app ops has started monitoring this receiver for high power (gps) locations. + boolean mOpHighPowerMonitoring; + int mPendingBroadcasts; + PowerManager.WakeLock mWakeLock; + + Receiver(ILocationListener listener, PendingIntent intent, int pid, int uid, + String packageName, WorkSource workSource, boolean hideFromAppOps) { + mListener = listener; + mPendingIntent = intent; + if (listener != null) { + mKey = listener.asBinder(); + } else { + mKey = intent; + } + mAllowedResolutionLevel = getAllowedResolutionLevel(pid, uid); + mUid = uid; + mPid = pid; + mPackageName = packageName; + if (workSource != null && workSource.size() <= 0) { + workSource = null; + } + mWorkSource = workSource; + mHideFromAppOps = hideFromAppOps; + + updateMonitoring(true); + + // construct/configure wakelock + mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY); + if (workSource == null) { + workSource = new WorkSource(mUid, mPackageName); + } + mWakeLock.setWorkSource(workSource); + } + + @Override + public boolean equals(Object otherObj) { + if (otherObj instanceof Receiver) { + return mKey.equals(((Receiver)otherObj).mKey); + } + return false; + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("Reciever["); + s.append(Integer.toHexString(System.identityHashCode(this))); + if (mListener != null) { + s.append(" listener"); + } else { + s.append(" intent"); + } + for (String p : mUpdateRecords.keySet()) { + s.append(" ").append(mUpdateRecords.get(p).toString()); + } + s.append("]"); + return s.toString(); + } + + /** + * Update AppOp monitoring for this receiver. + * + * @param allow If true receiver is currently active, if false it's been removed. + */ + public void updateMonitoring(boolean allow) { + if (mHideFromAppOps) { + return; + } + + boolean requestingLocation = false; + boolean requestingHighPowerLocation = false; + if (allow) { + // See if receiver has any enabled update records. Also note if any update records + // are high power (has a high power provider with an interval under a threshold). + for (UpdateRecord updateRecord : mUpdateRecords.values()) { + if (isAllowedByCurrentUserSettingsLocked(updateRecord.mProvider)) { + requestingLocation = true; + LocationProviderInterface locationProvider + = mProvidersByName.get(updateRecord.mProvider); + ProviderProperties properties = locationProvider != null + ? locationProvider.getProperties() : null; + if (properties != null + && properties.mPowerRequirement == Criteria.POWER_HIGH + && updateRecord.mRequest.getInterval() < HIGH_POWER_INTERVAL_MS) { + requestingHighPowerLocation = true; + break; + } + } + } + } + + // First update monitoring of any location request (including high power). + mOpMonitoring = updateMonitoring( + requestingLocation, + mOpMonitoring, + AppOpsManager.OP_MONITOR_LOCATION); + + // Now update monitoring of high power requests only. + boolean wasHighPowerMonitoring = mOpHighPowerMonitoring; + mOpHighPowerMonitoring = updateMonitoring( + requestingHighPowerLocation, + mOpHighPowerMonitoring, + AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION); + if (mOpHighPowerMonitoring != wasHighPowerMonitoring) { + // Send an intent to notify that a high power request has been added/removed. + Intent intent = new Intent(LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } + } + + /** + * Update AppOps monitoring for a single location request and op type. + * + * @param allowMonitoring True if monitoring is allowed for this request/op. + * @param currentlyMonitoring True if AppOps is currently monitoring this request/op. + * @param op AppOps code for the op to update. + * @return True if monitoring is on for this request/op after updating. + */ + private boolean updateMonitoring(boolean allowMonitoring, boolean currentlyMonitoring, + int op) { + if (!currentlyMonitoring) { + if (allowMonitoring) { + return mAppOps.startOpNoThrow(op, mUid, mPackageName) + == AppOpsManager.MODE_ALLOWED; + } + } else { + if (!allowMonitoring || mAppOps.checkOpNoThrow(op, mUid, mPackageName) + != AppOpsManager.MODE_ALLOWED) { + mAppOps.finishOp(op, mUid, mPackageName); + return false; + } + } + + return currentlyMonitoring; + } + + public boolean isListener() { + return mListener != null; + } + + public boolean isPendingIntent() { + return mPendingIntent != null; + } + + public ILocationListener getListener() { + if (mListener != null) { + return mListener; + } + throw new IllegalStateException("Request for non-existent listener"); + } + + public boolean callStatusChangedLocked(String provider, int status, Bundle extras) { + if (mListener != null) { + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + mListener.onStatusChanged(provider, status, extras); + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (RemoteException e) { + return false; + } + } else { + Intent statusChanged = new Intent(); + statusChanged.putExtras(new Bundle(extras)); + statusChanged.putExtra(LocationManager.KEY_STATUS_CHANGED, status); + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + mPendingIntent.send(mContext, 0, statusChanged, this, mLocationHandler, + getResolutionPermission(mAllowedResolutionLevel)); + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (PendingIntent.CanceledException e) { + return false; + } + } + return true; + } + + public boolean callLocationChangedLocked(Location location) { + if (mListener != null) { + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + mListener.onLocationChanged(new Location(location)); + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (RemoteException e) { + return false; + } + } else { + Intent locationChanged = new Intent(); + locationChanged.putExtra(LocationManager.KEY_LOCATION_CHANGED, new Location(location)); + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + mPendingIntent.send(mContext, 0, locationChanged, this, mLocationHandler, + getResolutionPermission(mAllowedResolutionLevel)); + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (PendingIntent.CanceledException e) { + return false; + } + } + return true; + } + + public boolean callProviderEnabledLocked(String provider, boolean enabled) { + // First update AppOp monitoring. + // An app may get/lose location access as providers are enabled/disabled. + updateMonitoring(true); + + if (mListener != null) { + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + if (enabled) { + mListener.onProviderEnabled(provider); + } else { + mListener.onProviderDisabled(provider); + } + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (RemoteException e) { + return false; + } + } else { + Intent providerIntent = new Intent(); + providerIntent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, enabled); + try { + synchronized (this) { + // synchronize to ensure incrementPendingBroadcastsLocked() + // is called before decrementPendingBroadcasts() + mPendingIntent.send(mContext, 0, providerIntent, this, mLocationHandler, + getResolutionPermission(mAllowedResolutionLevel)); + // call this after broadcasting so we do not increment + // if we throw an exeption. + incrementPendingBroadcastsLocked(); + } + } catch (PendingIntent.CanceledException e) { + return false; + } + } + return true; + } + + @Override + public void binderDied() { + if (D) Log.d(TAG, "Location listener died"); + + synchronized (mLock) { + removeUpdatesLocked(this); + } + synchronized (this) { + clearPendingBroadcastsLocked(); + } + } + + @Override + public void onSendFinished(PendingIntent pendingIntent, Intent intent, + int resultCode, String resultData, Bundle resultExtras) { + synchronized (this) { + decrementPendingBroadcastsLocked(); + } + } + + // this must be called while synchronized by caller in a synchronized block + // containing the sending of the broadcaset + private void incrementPendingBroadcastsLocked() { + if (mPendingBroadcasts++ == 0) { + mWakeLock.acquire(); + } + } + + private void decrementPendingBroadcastsLocked() { + if (--mPendingBroadcasts == 0) { + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + } + + public void clearPendingBroadcastsLocked() { + if (mPendingBroadcasts > 0) { + mPendingBroadcasts = 0; + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + } + } + } + + @Override + public void locationCallbackFinished(ILocationListener listener) { + //Do not use getReceiverLocked here as that will add the ILocationListener to + //the receiver list if it is not found. If it is not found then the + //LocationListener was removed when it had a pending broadcast and should + //not be added back. + synchronized (mLock) { + IBinder binder = listener.asBinder(); + Receiver receiver = mReceivers.get(binder); + if (receiver != null) { + synchronized (receiver) { + // so wakelock calls will succeed + long identity = Binder.clearCallingIdentity(); + receiver.decrementPendingBroadcastsLocked(); + Binder.restoreCallingIdentity(identity); + } + } + } + } + + private void addProviderLocked(LocationProviderInterface provider) { + mProviders.add(provider); + mProvidersByName.put(provider.getName(), provider); + } + + private void removeProviderLocked(LocationProviderInterface provider) { + provider.disable(); + mProviders.remove(provider); + mProvidersByName.remove(provider.getName()); + } + + /** + * Returns "true" if access to the specified location provider is allowed by the current + * user's settings. Access to all location providers is forbidden to non-location-provider + * processes belonging to background users. + * + * @param provider the name of the location provider + * @return + */ + private boolean isAllowedByCurrentUserSettingsLocked(String provider) { + if (mEnabledProviders.contains(provider)) { + return true; + } + if (mDisabledProviders.contains(provider)) { + return false; + } + // Use system settings + ContentResolver resolver = mContext.getContentResolver(); + + return Settings.Secure.isLocationProviderEnabledForUser(resolver, provider, mCurrentUserId); + } + + /** + * Returns "true" if access to the specified location provider is allowed by the specified + * user's settings. Access to all location providers is forbidden to non-location-provider + * processes belonging to background users. + * + * @param provider the name of the location provider + * @param uid the requestor's UID + * @return + */ + private boolean isAllowedByUserSettingsLocked(String provider, int uid) { + if (UserHandle.getUserId(uid) != mCurrentUserId && !isUidALocationProvider(uid)) { + return false; + } + return isAllowedByCurrentUserSettingsLocked(provider); + } + + /** + * Returns the permission string associated with the specified resolution level. + * + * @param resolutionLevel the resolution level + * @return the permission string + */ + private String getResolutionPermission(int resolutionLevel) { + switch (resolutionLevel) { + case RESOLUTION_LEVEL_FINE: + return android.Manifest.permission.ACCESS_FINE_LOCATION; + case RESOLUTION_LEVEL_COARSE: + return android.Manifest.permission.ACCESS_COARSE_LOCATION; + default: + return null; + } + } + + /** + * Returns the resolution level allowed to the given PID/UID pair. + * + * @param pid the PID + * @param uid the UID + * @return resolution level allowed to the pid/uid pair + */ + private int getAllowedResolutionLevel(int pid, int uid) { + if (mContext.checkPermission(android.Manifest.permission.ACCESS_FINE_LOCATION, + pid, uid) == PackageManager.PERMISSION_GRANTED) { + return RESOLUTION_LEVEL_FINE; + } else if (mContext.checkPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION, + pid, uid) == PackageManager.PERMISSION_GRANTED) { + return RESOLUTION_LEVEL_COARSE; + } else { + return RESOLUTION_LEVEL_NONE; + } + } + + /** + * Returns the resolution level allowed to the caller + * + * @return resolution level allowed to caller + */ + private int getCallerAllowedResolutionLevel() { + return getAllowedResolutionLevel(Binder.getCallingPid(), Binder.getCallingUid()); + } + + /** + * Throw SecurityException if specified resolution level is insufficient to use geofences. + * + * @param allowedResolutionLevel resolution level allowed to caller + */ + private void checkResolutionLevelIsSufficientForGeofenceUse(int allowedResolutionLevel) { + if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { + throw new SecurityException("Geofence usage requires ACCESS_FINE_LOCATION permission"); + } + } + + /** + * Return the minimum resolution level required to use the specified location provider. + * + * @param provider the name of the location provider + * @return minimum resolution level required for provider + */ + private int getMinimumResolutionLevelForProviderUse(String provider) { + if (LocationManager.GPS_PROVIDER.equals(provider) || + LocationManager.PASSIVE_PROVIDER.equals(provider)) { + // gps and passive providers require FINE permission + return RESOLUTION_LEVEL_FINE; + } else if (LocationManager.NETWORK_PROVIDER.equals(provider) || + LocationManager.FUSED_PROVIDER.equals(provider)) { + // network and fused providers are ok with COARSE or FINE + return RESOLUTION_LEVEL_COARSE; + } else { + // mock providers + LocationProviderInterface lp = mMockProviders.get(provider); + if (lp != null) { + ProviderProperties properties = lp.getProperties(); + if (properties != null) { + if (properties.mRequiresSatellite) { + // provider requiring satellites require FINE permission + return RESOLUTION_LEVEL_FINE; + } else if (properties.mRequiresNetwork || properties.mRequiresCell) { + // provider requiring network and or cell require COARSE or FINE + return RESOLUTION_LEVEL_COARSE; + } + } + } + } + return RESOLUTION_LEVEL_FINE; // if in doubt, require FINE + } + + /** + * Throw SecurityException if specified resolution level is insufficient to use the named + * location provider. + * + * @param allowedResolutionLevel resolution level allowed to caller + * @param providerName the name of the location provider + */ + private void checkResolutionLevelIsSufficientForProviderUse(int allowedResolutionLevel, + String providerName) { + int requiredResolutionLevel = getMinimumResolutionLevelForProviderUse(providerName); + if (allowedResolutionLevel < requiredResolutionLevel) { + switch (requiredResolutionLevel) { + case RESOLUTION_LEVEL_FINE: + throw new SecurityException("\"" + providerName + "\" location provider " + + "requires ACCESS_FINE_LOCATION permission."); + case RESOLUTION_LEVEL_COARSE: + throw new SecurityException("\"" + providerName + "\" location provider " + + "requires ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission."); + default: + throw new SecurityException("Insufficient permission for \"" + providerName + + "\" location provider."); + } + } + } + + /** + * Throw SecurityException if WorkSource use is not allowed (i.e. can't blame other packages + * for battery). + */ + private void checkDeviceStatsAllowed() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.UPDATE_DEVICE_STATS, null); + } + + private void checkUpdateAppOpsAllowed() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.UPDATE_APP_OPS_STATS, null); + } + + public static int resolutionLevelToOp(int allowedResolutionLevel) { + if (allowedResolutionLevel != RESOLUTION_LEVEL_NONE) { + if (allowedResolutionLevel == RESOLUTION_LEVEL_COARSE) { + return AppOpsManager.OP_COARSE_LOCATION; + } else { + return AppOpsManager.OP_FINE_LOCATION; + } + } + return -1; + } + + boolean reportLocationAccessNoThrow(int uid, String packageName, int allowedResolutionLevel) { + int op = resolutionLevelToOp(allowedResolutionLevel); + if (op >= 0) { + if (mAppOps.noteOpNoThrow(op, uid, packageName) != AppOpsManager.MODE_ALLOWED) { + return false; + } + } + return true; + } + + boolean checkLocationAccess(int uid, String packageName, int allowedResolutionLevel) { + int op = resolutionLevelToOp(allowedResolutionLevel); + if (op >= 0) { + if (mAppOps.checkOp(op, uid, packageName) != AppOpsManager.MODE_ALLOWED) { + return false; + } + } + return true; + } + + /** + * Returns all providers by name, including passive, but excluding + * fused, also including ones that are not permitted to + * be accessed by the calling activity or are currently disabled. + */ + @Override + public List getAllProviders() { + ArrayList out; + synchronized (mLock) { + out = new ArrayList(mProviders.size()); + for (LocationProviderInterface provider : mProviders) { + String name = provider.getName(); + if (LocationManager.FUSED_PROVIDER.equals(name)) { + continue; + } + out.add(name); + } + } + + if (D) Log.d(TAG, "getAllProviders()=" + out); + return out; + } + + /** + * Return all providers by name, that match criteria and are optionally + * enabled. + * Can return passive provider, but never returns fused provider. + */ + @Override + public List getProviders(Criteria criteria, boolean enabledOnly) { + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + ArrayList out; + int uid = Binder.getCallingUid();; + long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + out = new ArrayList(mProviders.size()); + for (LocationProviderInterface provider : mProviders) { + String name = provider.getName(); + if (LocationManager.FUSED_PROVIDER.equals(name)) { + continue; + } + if (allowedResolutionLevel >= getMinimumResolutionLevelForProviderUse(name)) { + if (enabledOnly && !isAllowedByUserSettingsLocked(name, uid)) { + continue; + } + if (criteria != null && !LocationProvider.propertiesMeetCriteria( + name, provider.getProperties(), criteria)) { + continue; + } + out.add(name); + } + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + + if (D) Log.d(TAG, "getProviders()=" + out); + return out; + } + + /** + * Return the name of the best provider given a Criteria object. + * This method has been deprecated from the public API, + * and the whole LocationProvider (including #meetsCriteria) + * has been deprecated as well. So this method now uses + * some simplified logic. + */ + @Override + public String getBestProvider(Criteria criteria, boolean enabledOnly) { + String result = null; + + List providers = getProviders(criteria, enabledOnly); + if (!providers.isEmpty()) { + result = pickBest(providers); + if (D) Log.d(TAG, "getBestProvider(" + criteria + ", " + enabledOnly + ")=" + result); + return result; + } + providers = getProviders(null, enabledOnly); + if (!providers.isEmpty()) { + result = pickBest(providers); + if (D) Log.d(TAG, "getBestProvider(" + criteria + ", " + enabledOnly + ")=" + result); + return result; + } + + if (D) Log.d(TAG, "getBestProvider(" + criteria + ", " + enabledOnly + ")=" + result); + return null; + } + + private String pickBest(List providers) { + if (providers.contains(LocationManager.GPS_PROVIDER)) { + return LocationManager.GPS_PROVIDER; + } else if (providers.contains(LocationManager.NETWORK_PROVIDER)) { + return LocationManager.NETWORK_PROVIDER; + } else { + return providers.get(0); + } + } + + @Override + public boolean providerMeetsCriteria(String provider, Criteria criteria) { + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) { + throw new IllegalArgumentException("provider=" + provider); + } + + boolean result = LocationProvider.propertiesMeetCriteria( + p.getName(), p.getProperties(), criteria); + if (D) Log.d(TAG, "providerMeetsCriteria(" + provider + ", " + criteria + ")=" + result); + return result; + } + + private void updateProvidersLocked() { + boolean changesMade = false; + for (int i = mProviders.size() - 1; i >= 0; i--) { + LocationProviderInterface p = mProviders.get(i); + boolean isEnabled = p.isEnabled(); + String name = p.getName(); + boolean shouldBeEnabled = isAllowedByCurrentUserSettingsLocked(name); + if (isEnabled && !shouldBeEnabled) { + updateProviderListenersLocked(name, false, mCurrentUserId); + changesMade = true; + } else if (!isEnabled && shouldBeEnabled) { + updateProviderListenersLocked(name, true, mCurrentUserId); + changesMade = true; + } + } + if (changesMade) { + mContext.sendBroadcastAsUser(new Intent(LocationManager.PROVIDERS_CHANGED_ACTION), + UserHandle.ALL); + mContext.sendBroadcastAsUser(new Intent(LocationManager.MODE_CHANGED_ACTION), + UserHandle.ALL); + } + } + + private void updateProviderListenersLocked(String provider, boolean enabled, int userId) { + int listeners = 0; + + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) return; + + ArrayList deadReceivers = null; + + ArrayList records = mRecordsByProvider.get(provider); + if (records != null) { + final int N = records.size(); + for (int i = 0; i < N; i++) { + UpdateRecord record = records.get(i); + if (UserHandle.getUserId(record.mReceiver.mUid) == userId) { + // Sends a notification message to the receiver + if (!record.mReceiver.callProviderEnabledLocked(provider, enabled)) { + if (deadReceivers == null) { + deadReceivers = new ArrayList(); + } + deadReceivers.add(record.mReceiver); + } + listeners++; + } + } + } + + if (deadReceivers != null) { + for (int i = deadReceivers.size() - 1; i >= 0; i--) { + removeUpdatesLocked(deadReceivers.get(i)); + } + } + + if (enabled) { + p.enable(); + if (listeners > 0) { + applyRequirementsLocked(provider); + } + } else { + p.disable(); + } + } + + private void applyRequirementsLocked(String provider) { + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) return; + + ArrayList records = mRecordsByProvider.get(provider); + WorkSource worksource = new WorkSource(); + ProviderRequest providerRequest = new ProviderRequest(); + + if (records != null) { + for (UpdateRecord record : records) { + if (UserHandle.getUserId(record.mReceiver.mUid) == mCurrentUserId) { + if (checkLocationAccess(record.mReceiver.mUid, record.mReceiver.mPackageName, + record.mReceiver.mAllowedResolutionLevel)) { + LocationRequest locationRequest = record.mRequest; + providerRequest.locationRequests.add(locationRequest); + if (locationRequest.getInterval() < providerRequest.interval) { + providerRequest.reportLocation = true; + providerRequest.interval = locationRequest.getInterval(); + } + } + } + } + + if (providerRequest.reportLocation) { + // calculate who to blame for power + // This is somewhat arbitrary. We pick a threshold interval + // that is slightly higher that the minimum interval, and + // spread the blame across all applications with a request + // under that threshold. + long thresholdInterval = (providerRequest.interval + 1000) * 3 / 2; + for (UpdateRecord record : records) { + if (UserHandle.getUserId(record.mReceiver.mUid) == mCurrentUserId) { + LocationRequest locationRequest = record.mRequest; + if (locationRequest.getInterval() <= thresholdInterval) { + if (record.mReceiver.mWorkSource != null + && record.mReceiver.mWorkSource.size() > 0 + && record.mReceiver.mWorkSource.getName(0) != null) { + // Assign blame to another work source. + // Can only assign blame if the WorkSource contains names. + worksource.add(record.mReceiver.mWorkSource); + } else { + // Assign blame to caller. + worksource.add( + record.mReceiver.mUid, + record.mReceiver.mPackageName); + } + } + } + } + } + } + + if (D) Log.d(TAG, "provider request: " + provider + " " + providerRequest); + p.setRequest(providerRequest, worksource); + } + + private class UpdateRecord { + final String mProvider; + final LocationRequest mRequest; + final Receiver mReceiver; + Location mLastFixBroadcast; + long mLastStatusBroadcast; + + /** + * Note: must be constructed with lock held. + */ + UpdateRecord(String provider, LocationRequest request, Receiver receiver) { + mProvider = provider; + mRequest = request; + mReceiver = receiver; + + ArrayList records = mRecordsByProvider.get(provider); + if (records == null) { + records = new ArrayList(); + mRecordsByProvider.put(provider, records); + } + if (!records.contains(this)) { + records.add(this); + } + } + + /** + * Method to be called when a record will no longer be used. Calling this multiple times + * must have the same effect as calling it once. + */ + void disposeLocked(boolean removeReceiver) { + // remove from mRecordsByProvider + ArrayList globalRecords = mRecordsByProvider.get(this.mProvider); + if (globalRecords != null) { + globalRecords.remove(this); + } + + if (!removeReceiver) return; // the caller will handle the rest + + // remove from Receiver#mUpdateRecords + HashMap receiverRecords = mReceiver.mUpdateRecords; + if (receiverRecords != null) { + receiverRecords.remove(this.mProvider); + + // and also remove the Receiver if it has no more update records + if (removeReceiver && receiverRecords.size() == 0) { + removeUpdatesLocked(mReceiver); + } + } + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("UpdateRecord["); + s.append(mProvider); + s.append(' ').append(mReceiver.mPackageName).append('('); + s.append(mReceiver.mUid).append(')'); + s.append(' ').append(mRequest); + s.append(']'); + return s.toString(); + } + } + + private Receiver getReceiverLocked(ILocationListener listener, int pid, int uid, + String packageName, WorkSource workSource, boolean hideFromAppOps) { + IBinder binder = listener.asBinder(); + Receiver receiver = mReceivers.get(binder); + if (receiver == null) { + receiver = new Receiver(listener, null, pid, uid, packageName, workSource, + hideFromAppOps); + mReceivers.put(binder, receiver); + + try { + receiver.getListener().asBinder().linkToDeath(receiver, 0); + } catch (RemoteException e) { + Slog.e(TAG, "linkToDeath failed:", e); + return null; + } + } + return receiver; + } + + private Receiver getReceiverLocked(PendingIntent intent, int pid, int uid, String packageName, + WorkSource workSource, boolean hideFromAppOps) { + Receiver receiver = mReceivers.get(intent); + if (receiver == null) { + receiver = new Receiver(null, intent, pid, uid, packageName, workSource, + hideFromAppOps); + mReceivers.put(intent, receiver); + } + return receiver; + } + + /** + * Creates a LocationRequest based upon the supplied LocationRequest that to meets resolution + * and consistency requirements. + * + * @param request the LocationRequest from which to create a sanitized version + * @return a version of request that meets the given resolution and consistency requirements + * @hide + */ + private LocationRequest createSanitizedRequest(LocationRequest request, int resolutionLevel) { + LocationRequest sanitizedRequest = new LocationRequest(request); + if (resolutionLevel < RESOLUTION_LEVEL_FINE) { + switch (sanitizedRequest.getQuality()) { + case LocationRequest.ACCURACY_FINE: + sanitizedRequest.setQuality(LocationRequest.ACCURACY_BLOCK); + break; + case LocationRequest.POWER_HIGH: + sanitizedRequest.setQuality(LocationRequest.POWER_LOW); + break; + } + // throttle + if (sanitizedRequest.getInterval() < LocationFudger.FASTEST_INTERVAL_MS) { + sanitizedRequest.setInterval(LocationFudger.FASTEST_INTERVAL_MS); + } + if (sanitizedRequest.getFastestInterval() < LocationFudger.FASTEST_INTERVAL_MS) { + sanitizedRequest.setFastestInterval(LocationFudger.FASTEST_INTERVAL_MS); + } + } + // make getFastestInterval() the minimum of interval and fastest interval + if (sanitizedRequest.getFastestInterval() > sanitizedRequest.getInterval()) { + request.setFastestInterval(request.getInterval()); + } + return sanitizedRequest; + } + + private void checkPackageName(String packageName) { + if (packageName == null) { + throw new SecurityException("invalid package name: " + packageName); + } + int uid = Binder.getCallingUid(); + String[] packages = mPackageManager.getPackagesForUid(uid); + if (packages == null) { + throw new SecurityException("invalid UID " + uid); + } + for (String pkg : packages) { + if (packageName.equals(pkg)) return; + } + throw new SecurityException("invalid package name: " + packageName); + } + + private void checkPendingIntent(PendingIntent intent) { + if (intent == null) { + throw new IllegalArgumentException("invalid pending intent: " + intent); + } + } + + private Receiver checkListenerOrIntentLocked(ILocationListener listener, PendingIntent intent, + int pid, int uid, String packageName, WorkSource workSource, boolean hideFromAppOps) { + if (intent == null && listener == null) { + throw new IllegalArgumentException("need either listener or intent"); + } else if (intent != null && listener != null) { + throw new IllegalArgumentException("cannot register both listener and intent"); + } else if (intent != null) { + checkPendingIntent(intent); + return getReceiverLocked(intent, pid, uid, packageName, workSource, hideFromAppOps); + } else { + return getReceiverLocked(listener, pid, uid, packageName, workSource, hideFromAppOps); + } + } + + @Override + public void requestLocationUpdates(LocationRequest request, ILocationListener listener, + PendingIntent intent, String packageName) { + if (request == null) request = DEFAULT_LOCATION_REQUEST; + checkPackageName(packageName); + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel, + request.getProvider()); + WorkSource workSource = request.getWorkSource(); + if (workSource != null && workSource.size() > 0) { + checkDeviceStatsAllowed(); + } + boolean hideFromAppOps = request.getHideFromAppOps(); + if (hideFromAppOps) { + checkUpdateAppOpsAllowed(); + } + LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel); + + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + // providers may use public location API's, need to clear identity + long identity = Binder.clearCallingIdentity(); + try { + // We don't check for MODE_IGNORED here; we will do that when we go to deliver + // a location. + checkLocationAccess(uid, packageName, allowedResolutionLevel); + + synchronized (mLock) { + Receiver recevier = checkListenerOrIntentLocked(listener, intent, pid, uid, + packageName, workSource, hideFromAppOps); + requestLocationUpdatesLocked(sanitizedRequest, recevier, pid, uid, packageName); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void requestLocationUpdatesLocked(LocationRequest request, Receiver receiver, + int pid, int uid, String packageName) { + // Figure out the provider. Either its explicitly request (legacy use cases), or + // use the fused provider + if (request == null) request = DEFAULT_LOCATION_REQUEST; + String name = request.getProvider(); + if (name == null) { + throw new IllegalArgumentException("provider name must not be null"); + } + + if (D) Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver)) + + " " + name + " " + request + " from " + packageName + "(" + uid + ")"); + LocationProviderInterface provider = mProvidersByName.get(name); + if (provider == null) { + throw new IllegalArgumentException("provider doesn't exist: " + name); + } + + UpdateRecord record = new UpdateRecord(name, request, receiver); + UpdateRecord oldRecord = receiver.mUpdateRecords.put(name, record); + if (oldRecord != null) { + oldRecord.disposeLocked(false); + } + + boolean isProviderEnabled = isAllowedByUserSettingsLocked(name, uid); + if (isProviderEnabled) { + applyRequirementsLocked(name); + } else { + // Notify the listener that updates are currently disabled + receiver.callProviderEnabledLocked(name, false); + } + // Update the monitoring here just in case multiple location requests were added to the + // same receiver (this request may be high power and the initial might not have been). + receiver.updateMonitoring(true); + } + + @Override + public void removeUpdates(ILocationListener listener, PendingIntent intent, + String packageName) { + checkPackageName(packageName); + + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + + synchronized (mLock) { + WorkSource workSource = null; + boolean hideFromAppOps = false; + Receiver receiver = checkListenerOrIntentLocked(listener, intent, pid, uid, + packageName, workSource, hideFromAppOps); + + // providers may use public location API's, need to clear identity + long identity = Binder.clearCallingIdentity(); + try { + removeUpdatesLocked(receiver); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private void removeUpdatesLocked(Receiver receiver) { + if (D) Log.i(TAG, "remove " + Integer.toHexString(System.identityHashCode(receiver))); + + if (mReceivers.remove(receiver.mKey) != null && receiver.isListener()) { + receiver.getListener().asBinder().unlinkToDeath(receiver, 0); + synchronized (receiver) { + receiver.clearPendingBroadcastsLocked(); + } + } + + receiver.updateMonitoring(false); + + // Record which providers were associated with this listener + HashSet providers = new HashSet(); + HashMap oldRecords = receiver.mUpdateRecords; + if (oldRecords != null) { + // Call dispose() on the obsolete update records. + for (UpdateRecord record : oldRecords.values()) { + record.disposeLocked(false); + } + // Accumulate providers + providers.addAll(oldRecords.keySet()); + } + + // update provider + for (String provider : providers) { + // If provider is already disabled, don't need to do anything + if (!isAllowedByCurrentUserSettingsLocked(provider)) { + continue; + } + + applyRequirementsLocked(provider); + } + } + + private void applyAllProviderRequirementsLocked() { + for (LocationProviderInterface p : mProviders) { + // If provider is already disabled, don't need to do anything + if (!isAllowedByCurrentUserSettingsLocked(p.getName())) { + continue; + } + + applyRequirementsLocked(p.getName()); + } + } + + @Override + public Location getLastLocation(LocationRequest request, String packageName) { + if (D) Log.d(TAG, "getLastLocation: " + request); + if (request == null) request = DEFAULT_LOCATION_REQUEST; + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + checkPackageName(packageName); + checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel, + request.getProvider()); + // no need to sanitize this request, as only the provider name is used + + final int uid = Binder.getCallingUid(); + final long identity = Binder.clearCallingIdentity(); + try { + if (mBlacklist.isBlacklisted(packageName)) { + if (D) Log.d(TAG, "not returning last loc for blacklisted app: " + + packageName); + return null; + } + + if (!reportLocationAccessNoThrow(uid, packageName, allowedResolutionLevel)) { + if (D) Log.d(TAG, "not returning last loc for no op app: " + + packageName); + return null; + } + + synchronized (mLock) { + // Figure out the provider. Either its explicitly request (deprecated API's), + // or use the fused provider + String name = request.getProvider(); + if (name == null) name = LocationManager.FUSED_PROVIDER; + LocationProviderInterface provider = mProvidersByName.get(name); + if (provider == null) return null; + + if (!isAllowedByUserSettingsLocked(name, uid)) return null; + + Location location; + if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { + // Make sure that an app with coarse permissions can't get frequent location + // updates by calling LocationManager.getLastKnownLocation repeatedly. + location = mLastLocationCoarseInterval.get(name); + } else { + location = mLastLocation.get(name); + } + if (location == null) { + return null; + } + if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { + Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); + if (noGPSLocation != null) { + return new Location(mLocationFudger.getOrCreate(noGPSLocation)); + } + } else { + return new Location(location); + } + } + return null; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void requestGeofence(LocationRequest request, Geofence geofence, PendingIntent intent, + String packageName) { + if (request == null) request = DEFAULT_LOCATION_REQUEST; + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + checkResolutionLevelIsSufficientForGeofenceUse(allowedResolutionLevel); + checkPendingIntent(intent); + checkPackageName(packageName); + checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel, + request.getProvider()); + LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel); + + if (D) Log.d(TAG, "requestGeofence: " + sanitizedRequest + " " + geofence + " " + intent); + + // geo-fence manager uses the public location API, need to clear identity + int uid = Binder.getCallingUid(); + if (UserHandle.getUserId(uid) != UserHandle.USER_OWNER) { + // temporary measure until geofences work for secondary users + Log.w(TAG, "proximity alerts are currently available only to the primary user"); + return; + } + long identity = Binder.clearCallingIdentity(); + try { + mGeofenceManager.addFence(sanitizedRequest, geofence, intent, allowedResolutionLevel, + uid, packageName); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void removeGeofence(Geofence geofence, PendingIntent intent, String packageName) { + checkResolutionLevelIsSufficientForGeofenceUse(getCallerAllowedResolutionLevel()); + checkPendingIntent(intent); + checkPackageName(packageName); + + if (D) Log.d(TAG, "removeGeofence: " + geofence + " " + intent); + + // geo-fence manager uses the public location API, need to clear identity + long identity = Binder.clearCallingIdentity(); + try { + mGeofenceManager.removeFence(geofence, intent); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + + @Override + public boolean addGpsStatusListener(IGpsStatusListener listener, String packageName) { + if (mGpsStatusProvider == null) { + return false; + } + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel, + LocationManager.GPS_PROVIDER); + + final int uid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + if (!checkLocationAccess(uid, packageName, allowedResolutionLevel)) { + return false; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + try { + mGpsStatusProvider.addGpsStatusListener(listener); + } catch (RemoteException e) { + Slog.e(TAG, "mGpsStatusProvider.addGpsStatusListener failed", e); + return false; + } + return true; + } + + @Override + public void removeGpsStatusListener(IGpsStatusListener listener) { + synchronized (mLock) { + try { + mGpsStatusProvider.removeGpsStatusListener(listener); + } catch (Exception e) { + Slog.e(TAG, "mGpsStatusProvider.removeGpsStatusListener failed", e); + } + } + } + + @Override + public boolean sendExtraCommand(String provider, String command, Bundle extras) { + if (provider == null) { + // throw NullPointerException to remain compatible with previous implementation + throw new NullPointerException(); + } + checkResolutionLevelIsSufficientForProviderUse(getCallerAllowedResolutionLevel(), + provider); + + // and check for ACCESS_LOCATION_EXTRA_COMMANDS + if ((mContext.checkCallingOrSelfPermission(ACCESS_LOCATION_EXTRA_COMMANDS) + != PackageManager.PERMISSION_GRANTED)) { + throw new SecurityException("Requires ACCESS_LOCATION_EXTRA_COMMANDS permission"); + } + + synchronized (mLock) { + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) return false; + + return p.sendExtraCommand(command, extras); + } + } + + @Override + public boolean sendNiResponse(int notifId, int userResponse) { + if (Binder.getCallingUid() != Process.myUid()) { + throw new SecurityException( + "calling sendNiResponse from outside of the system is not allowed"); + } + try { + return mNetInitiatedListener.sendNiResponse(notifId, userResponse); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException in LocationManagerService.sendNiResponse"); + return false; + } + } + + /** + * @return null if the provider does not exist + * @throws SecurityException if the provider is not allowed to be + * accessed by the caller + */ + @Override + public ProviderProperties getProviderProperties(String provider) { + if (mProvidersByName.get(provider) == null) { + return null; + } + + checkResolutionLevelIsSufficientForProviderUse(getCallerAllowedResolutionLevel(), + provider); + + LocationProviderInterface p; + synchronized (mLock) { + p = mProvidersByName.get(provider); + } + + if (p == null) return null; + return p.getProperties(); + } + + @Override + public boolean isProviderEnabled(String provider) { + // TODO: remove this check in next release, see b/10696351 + checkResolutionLevelIsSufficientForProviderUse(getCallerAllowedResolutionLevel(), + provider); + + // Fused provider is accessed indirectly via criteria rather than the provider-based APIs, + // so we discourage its use + if (LocationManager.FUSED_PROVIDER.equals(provider)) return false; + + int uid = Binder.getCallingUid(); + long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) return false; + + return isAllowedByUserSettingsLocked(provider, uid); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** + * Returns "true" if the UID belongs to a bound location provider. + * + * @param uid the uid + * @return true if uid belongs to a bound location provider + */ + private boolean isUidALocationProvider(int uid) { + if (uid == Process.SYSTEM_UID) { + return true; + } + if (mGeocodeProvider != null) { + if (doesPackageHaveUid(uid, mGeocodeProvider.getConnectedPackageName())) return true; + } + for (LocationProviderProxy proxy : mProxyProviders) { + if (doesPackageHaveUid(uid, proxy.getConnectedPackageName())) return true; + } + return false; + } + + private void checkCallerIsProvider() { + if (mContext.checkCallingOrSelfPermission(INSTALL_LOCATION_PROVIDER) + == PackageManager.PERMISSION_GRANTED) { + return; + } + + // Previously we only used the INSTALL_LOCATION_PROVIDER + // check. But that is system or signature + // protection level which is not flexible enough for + // providers installed oustide the system image. So + // also allow providers with a UID matching the + // currently bound package name + + if (isUidALocationProvider(Binder.getCallingUid())) { + return; + } + + throw new SecurityException("need INSTALL_LOCATION_PROVIDER permission, " + + "or UID of a currently bound location provider"); + } + + private boolean doesPackageHaveUid(int uid, String packageName) { + if (packageName == null) { + return false; + } + try { + ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); + if (appInfo.uid != uid) { + return false; + } + } catch (NameNotFoundException e) { + return false; + } + return true; + } + + @Override + public void reportLocation(Location location, boolean passive) { + checkCallerIsProvider(); + + if (!location.isComplete()) { + Log.w(TAG, "Dropping incomplete location: " + location); + return; + } + + mLocationHandler.removeMessages(MSG_LOCATION_CHANGED, location); + Message m = Message.obtain(mLocationHandler, MSG_LOCATION_CHANGED, location); + m.arg1 = (passive ? 1 : 0); + mLocationHandler.sendMessageAtFrontOfQueue(m); + } + + + private static boolean shouldBroadcastSafe( + Location loc, Location lastLoc, UpdateRecord record, long now) { + // Always broadcast the first update + if (lastLoc == null) { + return true; + } + + // Check whether sufficient time has passed + long minTime = record.mRequest.getFastestInterval(); + long delta = (loc.getElapsedRealtimeNanos() - lastLoc.getElapsedRealtimeNanos()) + / NANOS_PER_MILLI; + if (delta < minTime - MAX_PROVIDER_SCHEDULING_JITTER_MS) { + return false; + } + + // Check whether sufficient distance has been traveled + double minDistance = record.mRequest.getSmallestDisplacement(); + if (minDistance > 0.0) { + if (loc.distanceTo(lastLoc) <= minDistance) { + return false; + } + } + + // Check whether sufficient number of udpates is left + if (record.mRequest.getNumUpdates() <= 0) { + return false; + } + + // Check whether the expiry date has passed + if (record.mRequest.getExpireAt() < now) { + return false; + } + + return true; + } + + private void handleLocationChangedLocked(Location location, boolean passive) { + if (D) Log.d(TAG, "incoming location: " + location); + + long now = SystemClock.elapsedRealtime(); + String provider = (passive ? LocationManager.PASSIVE_PROVIDER : location.getProvider()); + + // Skip if the provider is unknown. + LocationProviderInterface p = mProvidersByName.get(provider); + if (p == null) return; + + // Update last known locations + Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); + Location lastNoGPSLocation = null; + Location lastLocation = mLastLocation.get(provider); + if (lastLocation == null) { + lastLocation = new Location(provider); + mLastLocation.put(provider, lastLocation); + } else { + lastNoGPSLocation = lastLocation.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); + if (noGPSLocation == null && lastNoGPSLocation != null) { + // New location has no no-GPS location: adopt last no-GPS location. This is set + // directly into location because we do not want to notify COARSE clients. + location.setExtraLocation(Location.EXTRA_NO_GPS_LOCATION, lastNoGPSLocation); + } + } + lastLocation.set(location); + + // Update last known coarse interval location if enough time has passed. + Location lastLocationCoarseInterval = mLastLocationCoarseInterval.get(provider); + if (lastLocationCoarseInterval == null) { + lastLocationCoarseInterval = new Location(location); + mLastLocationCoarseInterval.put(provider, lastLocationCoarseInterval); + } + long timeDiffNanos = location.getElapsedRealtimeNanos() + - lastLocationCoarseInterval.getElapsedRealtimeNanos(); + if (timeDiffNanos > LocationFudger.FASTEST_INTERVAL_MS * NANOS_PER_MILLI) { + lastLocationCoarseInterval.set(location); + } + // Don't ever return a coarse location that is more recent than the allowed update + // interval (i.e. don't allow an app to keep registering and unregistering for + // location updates to overcome the minimum interval). + noGPSLocation = + lastLocationCoarseInterval.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); + + // Skip if there are no UpdateRecords for this provider. + ArrayList records = mRecordsByProvider.get(provider); + if (records == null || records.size() == 0) return; + + // Fetch coarse location + Location coarseLocation = null; + if (noGPSLocation != null) { + coarseLocation = mLocationFudger.getOrCreate(noGPSLocation); + } + + // Fetch latest status update time + long newStatusUpdateTime = p.getStatusUpdateTime(); + + // Get latest status + Bundle extras = new Bundle(); + int status = p.getStatus(extras); + + ArrayList deadReceivers = null; + ArrayList deadUpdateRecords = null; + + // Broadcast location or status to all listeners + for (UpdateRecord r : records) { + Receiver receiver = r.mReceiver; + boolean receiverDead = false; + + int receiverUserId = UserHandle.getUserId(receiver.mUid); + if (receiverUserId != mCurrentUserId && !isUidALocationProvider(receiver.mUid)) { + if (D) { + Log.d(TAG, "skipping loc update for background user " + receiverUserId + + " (current user: " + mCurrentUserId + ", app: " + + receiver.mPackageName + ")"); + } + continue; + } + + if (mBlacklist.isBlacklisted(receiver.mPackageName)) { + if (D) Log.d(TAG, "skipping loc update for blacklisted app: " + + receiver.mPackageName); + continue; + } + + if (!reportLocationAccessNoThrow(receiver.mUid, receiver.mPackageName, + receiver.mAllowedResolutionLevel)) { + if (D) Log.d(TAG, "skipping loc update for no op app: " + + receiver.mPackageName); + continue; + } + + Location notifyLocation = null; + if (receiver.mAllowedResolutionLevel < RESOLUTION_LEVEL_FINE) { + notifyLocation = coarseLocation; // use coarse location + } else { + notifyLocation = lastLocation; // use fine location + } + if (notifyLocation != null) { + Location lastLoc = r.mLastFixBroadcast; + if ((lastLoc == null) || shouldBroadcastSafe(notifyLocation, lastLoc, r, now)) { + if (lastLoc == null) { + lastLoc = new Location(notifyLocation); + r.mLastFixBroadcast = lastLoc; + } else { + lastLoc.set(notifyLocation); + } + if (!receiver.callLocationChangedLocked(notifyLocation)) { + Slog.w(TAG, "RemoteException calling onLocationChanged on " + receiver); + receiverDead = true; + } + r.mRequest.decrementNumUpdates(); + } + } + + long prevStatusUpdateTime = r.mLastStatusBroadcast; + if ((newStatusUpdateTime > prevStatusUpdateTime) && + (prevStatusUpdateTime != 0 || status != LocationProvider.AVAILABLE)) { + + r.mLastStatusBroadcast = newStatusUpdateTime; + if (!receiver.callStatusChangedLocked(provider, status, extras)) { + receiverDead = true; + Slog.w(TAG, "RemoteException calling onStatusChanged on " + receiver); + } + } + + // track expired records + if (r.mRequest.getNumUpdates() <= 0 || r.mRequest.getExpireAt() < now) { + if (deadUpdateRecords == null) { + deadUpdateRecords = new ArrayList(); + } + deadUpdateRecords.add(r); + } + // track dead receivers + if (receiverDead) { + if (deadReceivers == null) { + deadReceivers = new ArrayList(); + } + if (!deadReceivers.contains(receiver)) { + deadReceivers.add(receiver); + } + } + } + + // remove dead records and receivers outside the loop + if (deadReceivers != null) { + for (Receiver receiver : deadReceivers) { + removeUpdatesLocked(receiver); + } + } + if (deadUpdateRecords != null) { + for (UpdateRecord r : deadUpdateRecords) { + r.disposeLocked(true); + } + applyRequirementsLocked(provider); + } + } + + private class LocationWorkerHandler extends Handler { + public LocationWorkerHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOCATION_CHANGED: + handleLocationChanged((Location) msg.obj, msg.arg1 == 1); + break; + } + } + } + + private boolean isMockProvider(String provider) { + synchronized (mLock) { + return mMockProviders.containsKey(provider); + } + } + + private void handleLocationChanged(Location location, boolean passive) { + // create a working copy of the incoming Location so that the service can modify it without + // disturbing the caller's copy + Location myLocation = new Location(location); + String provider = myLocation.getProvider(); + + // set "isFromMockProvider" bit if location came from a mock provider. we do not clear this + // bit if location did not come from a mock provider because passive/fused providers can + // forward locations from mock providers, and should not grant them legitimacy in doing so. + if (!myLocation.isFromMockProvider() && isMockProvider(provider)) { + myLocation.setIsFromMockProvider(true); + } + + synchronized (mLock) { + if (isAllowedByCurrentUserSettingsLocked(provider)) { + if (!passive) { + // notify passive provider of the new location + mPassiveProvider.updateLocation(myLocation); + } + handleLocationChangedLocked(myLocation, passive); + } + } + } + + private final PackageMonitor mPackageMonitor = new PackageMonitor() { + @Override + public void onPackageDisappeared(String packageName, int reason) { + // remove all receivers associated with this package name + synchronized (mLock) { + ArrayList deadReceivers = null; + + for (Receiver receiver : mReceivers.values()) { + if (receiver.mPackageName.equals(packageName)) { + if (deadReceivers == null) { + deadReceivers = new ArrayList(); + } + deadReceivers.add(receiver); + } + } + + // perform removal outside of mReceivers loop + if (deadReceivers != null) { + for (Receiver receiver : deadReceivers) { + removeUpdatesLocked(receiver); + } + } + } + } + }; + + // Geocoder + + @Override + public boolean geocoderIsPresent() { + return mGeocodeProvider != null; + } + + @Override + public String getFromLocation(double latitude, double longitude, int maxResults, + GeocoderParams params, List
      addrs) { + if (mGeocodeProvider != null) { + return mGeocodeProvider.getFromLocation(latitude, longitude, maxResults, + params, addrs); + } + return null; + } + + + @Override + public String getFromLocationName(String locationName, + double lowerLeftLatitude, double lowerLeftLongitude, + double upperRightLatitude, double upperRightLongitude, int maxResults, + GeocoderParams params, List
      addrs) { + + if (mGeocodeProvider != null) { + return mGeocodeProvider.getFromLocationName(locationName, lowerLeftLatitude, + lowerLeftLongitude, upperRightLatitude, upperRightLongitude, + maxResults, params, addrs); + } + return null; + } + + // Mock Providers + + private void checkMockPermissionsSafe() { + boolean allowMocks = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ALLOW_MOCK_LOCATION, 0) == 1; + if (!allowMocks) { + throw new SecurityException("Requires ACCESS_MOCK_LOCATION secure setting"); + } + + if (mContext.checkCallingPermission(ACCESS_MOCK_LOCATION) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires ACCESS_MOCK_LOCATION permission"); + } + } + + @Override + public void addTestProvider(String name, ProviderProperties properties) { + checkMockPermissionsSafe(); + + if (LocationManager.PASSIVE_PROVIDER.equals(name)) { + throw new IllegalArgumentException("Cannot mock the passive location provider"); + } + + long identity = Binder.clearCallingIdentity(); + synchronized (mLock) { + MockProvider provider = new MockProvider(name, this, properties); + // remove the real provider if we are replacing GPS or network provider + if (LocationManager.GPS_PROVIDER.equals(name) + || LocationManager.NETWORK_PROVIDER.equals(name) + || LocationManager.FUSED_PROVIDER.equals(name)) { + LocationProviderInterface p = mProvidersByName.get(name); + if (p != null) { + removeProviderLocked(p); + } + } + if (mProvidersByName.get(name) != null) { + throw new IllegalArgumentException("Provider \"" + name + "\" already exists"); + } + addProviderLocked(provider); + mMockProviders.put(name, provider); + mLastLocation.put(name, null); + mLastLocationCoarseInterval.put(name, null); + updateProvidersLocked(); + } + Binder.restoreCallingIdentity(identity); + } + + @Override + public void removeTestProvider(String provider) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.remove(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + long identity = Binder.clearCallingIdentity(); + removeProviderLocked(mProvidersByName.get(provider)); + + // reinstate real provider if available + LocationProviderInterface realProvider = mRealProviders.get(provider); + if (realProvider != null) { + addProviderLocked(realProvider); + } + mLastLocation.put(provider, null); + mLastLocationCoarseInterval.put(provider, null); + updateProvidersLocked(); + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setTestProviderLocation(String provider, Location loc) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + // clear calling identity so INSTALL_LOCATION_PROVIDER permission is not required + long identity = Binder.clearCallingIdentity(); + mockProvider.setLocation(loc); + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void clearTestProviderLocation(String provider) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + mockProvider.clearLocation(); + } + } + + @Override + public void setTestProviderEnabled(String provider, boolean enabled) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + long identity = Binder.clearCallingIdentity(); + if (enabled) { + mockProvider.enable(); + mEnabledProviders.add(provider); + mDisabledProviders.remove(provider); + } else { + mockProvider.disable(); + mEnabledProviders.remove(provider); + mDisabledProviders.add(provider); + } + updateProvidersLocked(); + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void clearTestProviderEnabled(String provider) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + long identity = Binder.clearCallingIdentity(); + mEnabledProviders.remove(provider); + mDisabledProviders.remove(provider); + updateProvidersLocked(); + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setTestProviderStatus(String provider, int status, Bundle extras, long updateTime) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + mockProvider.setStatus(status, extras, updateTime); + } + } + + @Override + public void clearTestProviderStatus(String provider) { + checkMockPermissionsSafe(); + synchronized (mLock) { + MockProvider mockProvider = mMockProviders.get(provider); + if (mockProvider == null) { + throw new IllegalArgumentException("Provider \"" + provider + "\" unknown"); + } + mockProvider.clearStatus(); + } + } + + private void log(String log) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Slog.d(TAG, log); + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump LocationManagerService from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + synchronized (mLock) { + pw.println("Current Location Manager state:"); + pw.println(" Location Listeners:"); + for (Receiver receiver : mReceivers.values()) { + pw.println(" " + receiver); + } + pw.println(" Records by Provider:"); + for (Map.Entry> entry : mRecordsByProvider.entrySet()) { + pw.println(" " + entry.getKey() + ":"); + for (UpdateRecord record : entry.getValue()) { + pw.println(" " + record); + } + } + pw.println(" Last Known Locations:"); + for (Map.Entry entry : mLastLocation.entrySet()) { + String provider = entry.getKey(); + Location location = entry.getValue(); + pw.println(" " + provider + ": " + location); + } + + pw.println(" Last Known Locations Coarse Intervals:"); + for (Map.Entry entry : mLastLocationCoarseInterval.entrySet()) { + String provider = entry.getKey(); + Location location = entry.getValue(); + pw.println(" " + provider + ": " + location); + } + + mGeofenceManager.dump(pw); + + if (mEnabledProviders.size() > 0) { + pw.println(" Enabled Providers:"); + for (String i : mEnabledProviders) { + pw.println(" " + i); + } + + } + if (mDisabledProviders.size() > 0) { + pw.println(" Disabled Providers:"); + for (String i : mDisabledProviders) { + pw.println(" " + i); + } + } + pw.append(" "); + mBlacklist.dump(pw); + if (mMockProviders.size() > 0) { + pw.println(" Mock Providers:"); + for (Map.Entry i : mMockProviders.entrySet()) { + i.getValue().dump(pw, " "); + } + } + + pw.append(" fudger: "); + mLocationFudger.dump(fd, pw, args); + + if (args.length > 0 && "short".equals(args[0])) { + return; + } + for (LocationProviderInterface provider: mProviders) { + pw.print(provider.getName() + " Internal State"); + if (provider instanceof LocationProviderProxy) { + LocationProviderProxy proxy = (LocationProviderProxy) provider; + pw.print(" (" + proxy.getConnectedPackageName() + ")"); + } + pw.println(":"); + provider.dump(fd, pw, args); + } + } + } +} diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java new file mode 100644 index 0000000..35e7afa --- /dev/null +++ b/services/core/java/com/android/server/LockSettingsService.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.ActivityManagerNative; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; + +import static android.content.Context.USER_SERVICE; +import static android.Manifest.permission.READ_PROFILE; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.media.AudioManager; +import android.media.AudioService; +import android.os.Binder; +import android.os.Environment; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.provider.Settings.SettingNotFoundException; +import android.security.KeyStore; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.widget.ILockSettings; +import com.android.internal.widget.LockPatternUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.List; + +/** + * Keeps the lock pattern/password data and related settings for each user. + * Used by LockPatternUtils. Needs to be a service because Settings app also needs + * to be able to save lockscreen information for secondary users. + * @hide + */ +public class LockSettingsService extends ILockSettings.Stub { + + private static final String PERMISSION = "android.permission.ACCESS_KEYGUARD_SECURE_STORAGE"; + private final DatabaseHelper mOpenHelper; + private static final String TAG = "LockSettingsService"; + + private static final String TABLE = "locksettings"; + private static final String COLUMN_KEY = "name"; + private static final String COLUMN_USERID = "user"; + private static final String COLUMN_VALUE = "value"; + + private static final String[] COLUMNS_FOR_QUERY = { + COLUMN_VALUE + }; + + private static final String SYSTEM_DIRECTORY = "/system/"; + private static final String LOCK_PATTERN_FILE = "gesture.key"; + private static final String LOCK_PASSWORD_FILE = "password.key"; + + private final Context mContext; + private LockPatternUtils mLockPatternUtils; + + public LockSettingsService(Context context) { + mContext = context; + // Open the database + mOpenHelper = new DatabaseHelper(mContext); + + mLockPatternUtils = new LockPatternUtils(context); + } + + public void systemReady() { + migrateOldData(); + } + + private void migrateOldData() { + try { + // These Settings moved before multi-user was enabled, so we only have to do it for the + // root user. + if (getString("migrated", null, 0) == null) { + final ContentResolver cr = mContext.getContentResolver(); + for (String validSetting : VALID_SETTINGS) { + String value = Settings.Secure.getString(cr, validSetting); + if (value != null) { + setString(validSetting, value, 0); + } + } + // No need to move the password / pattern files. They're already in the right place. + setString("migrated", "true", 0); + Slog.i(TAG, "Migrated lock settings to new location"); + } + + // These Settings changed after multi-user was enabled, hence need to be moved per user. + if (getString("migrated_user_specific", null, 0) == null) { + final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); + final ContentResolver cr = mContext.getContentResolver(); + List users = um.getUsers(); + for (int user = 0; user < users.size(); user++) { + // Migrate owner info + final int userId = users.get(user).id; + final String OWNER_INFO = Secure.LOCK_SCREEN_OWNER_INFO; + String ownerInfo = Settings.Secure.getStringForUser(cr, OWNER_INFO, userId); + if (ownerInfo != null) { + setString(OWNER_INFO, ownerInfo, userId); + Settings.Secure.putStringForUser(cr, ownerInfo, "", userId); + } + + // Migrate owner info enabled. Note there was a bug where older platforms only + // stored this value if the checkbox was toggled at least once. The code detects + // this case by handling the exception. + final String OWNER_INFO_ENABLED = Secure.LOCK_SCREEN_OWNER_INFO_ENABLED; + boolean enabled; + try { + int ivalue = Settings.Secure.getIntForUser(cr, OWNER_INFO_ENABLED, userId); + enabled = ivalue != 0; + setLong(OWNER_INFO_ENABLED, enabled ? 1 : 0, userId); + } catch (SettingNotFoundException e) { + // Setting was never stored. Store it if the string is not empty. + if (!TextUtils.isEmpty(ownerInfo)) { + setLong(OWNER_INFO_ENABLED, 1, userId); + } + } + Settings.Secure.putIntForUser(cr, OWNER_INFO_ENABLED, 0, userId); + } + // No need to move the password / pattern files. They're already in the right place. + setString("migrated_user_specific", "true", 0); + Slog.i(TAG, "Migrated per-user lock settings to new location"); + } + } catch (RemoteException re) { + Slog.e(TAG, "Unable to migrate old data", re); + } + } + + private final void checkWritePermission(int userId) { + mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsWrite"); + } + + private final void checkPasswordReadPermission(int userId) { + mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsRead"); + } + + private final void checkReadPermission(String requestedKey, int userId) { + final int callingUid = Binder.getCallingUid(); + for (int i = 0; i < READ_PROFILE_PROTECTED_SETTINGS.length; i++) { + String key = READ_PROFILE_PROTECTED_SETTINGS[i]; + if (key.equals(requestedKey) && mContext.checkCallingOrSelfPermission(READ_PROFILE) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("uid=" + callingUid + + " needs permission " + READ_PROFILE + " to read " + + requestedKey + " for user " + userId); + } + } + } + + @Override + public void setBoolean(String key, boolean value, int userId) throws RemoteException { + checkWritePermission(userId); + + writeToDb(key, value ? "1" : "0", userId); + } + + @Override + public void setLong(String key, long value, int userId) throws RemoteException { + checkWritePermission(userId); + + writeToDb(key, Long.toString(value), userId); + } + + @Override + public void setString(String key, String value, int userId) throws RemoteException { + checkWritePermission(userId); + + writeToDb(key, value, userId); + } + + @Override + public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException { + checkReadPermission(key, userId); + + String value = readFromDb(key, null, userId); + return TextUtils.isEmpty(value) ? + defaultValue : (value.equals("1") || value.equals("true")); + } + + @Override + public long getLong(String key, long defaultValue, int userId) throws RemoteException { + checkReadPermission(key, userId); + + String value = readFromDb(key, null, userId); + return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); + } + + @Override + public String getString(String key, String defaultValue, int userId) throws RemoteException { + checkReadPermission(key, userId); + + return readFromDb(key, defaultValue, userId); + } + + private String getLockPatternFilename(int userId) { + String dataSystemDirectory = + android.os.Environment.getDataDirectory().getAbsolutePath() + + SYSTEM_DIRECTORY; + if (userId == 0) { + // Leave it in the same place for user 0 + return dataSystemDirectory + LOCK_PATTERN_FILE; + } else { + return new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE) + .getAbsolutePath(); + } + } + + private String getLockPasswordFilename(int userId) { + String dataSystemDirectory = + android.os.Environment.getDataDirectory().getAbsolutePath() + + SYSTEM_DIRECTORY; + if (userId == 0) { + // Leave it in the same place for user 0 + return dataSystemDirectory + LOCK_PASSWORD_FILE; + } else { + return new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE) + .getAbsolutePath(); + } + } + + @Override + public boolean havePassword(int userId) throws RemoteException { + // Do we need a permissions check here? + + return new File(getLockPasswordFilename(userId)).length() > 0; + } + + @Override + public boolean havePattern(int userId) throws RemoteException { + // Do we need a permissions check here? + + return new File(getLockPatternFilename(userId)).length() > 0; + } + + private void maybeUpdateKeystore(String password, int userId) { + if (userId == UserHandle.USER_OWNER) { + final KeyStore keyStore = KeyStore.getInstance(); + // Conditionally reset the keystore if empty. If non-empty, we are just + // switching key guard type + if (TextUtils.isEmpty(password) && keyStore.isEmpty()) { + keyStore.reset(); + } else { + // Update the keystore password + keyStore.password(password); + } + } + } + + @Override + public void setLockPattern(String pattern, int userId) throws RemoteException { + checkWritePermission(userId); + + maybeUpdateKeystore(pattern, userId); + + final byte[] hash = LockPatternUtils.patternToHash( + LockPatternUtils.stringToPattern(pattern)); + writeFile(getLockPatternFilename(userId), hash); + } + + @Override + public void setLockPassword(String password, int userId) throws RemoteException { + checkWritePermission(userId); + + maybeUpdateKeystore(password, userId); + + writeFile(getLockPasswordFilename(userId), mLockPatternUtils.passwordToHash(password)); + } + + @Override + public boolean checkPattern(String pattern, int userId) throws RemoteException { + checkPasswordReadPermission(userId); + try { + // Read all the bytes from the file + RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r"); + final byte[] stored = new byte[(int) raf.length()]; + int got = raf.read(stored, 0, stored.length); + raf.close(); + if (got <= 0) { + return true; + } + // Compare the hash from the file with the entered pattern's hash + final byte[] hash = LockPatternUtils.patternToHash( + LockPatternUtils.stringToPattern(pattern)); + final boolean matched = Arrays.equals(stored, hash); + if (matched && !TextUtils.isEmpty(pattern)) { + maybeUpdateKeystore(pattern, userId); + } + return matched; + } catch (FileNotFoundException fnfe) { + Slog.e(TAG, "Cannot read file " + fnfe); + } catch (IOException ioe) { + Slog.e(TAG, "Cannot read file " + ioe); + } + return true; + } + + @Override + public boolean checkPassword(String password, int userId) throws RemoteException { + checkPasswordReadPermission(userId); + + try { + // Read all the bytes from the file + RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r"); + final byte[] stored = new byte[(int) raf.length()]; + int got = raf.read(stored, 0, stored.length); + raf.close(); + if (got <= 0) { + return true; + } + // Compare the hash from the file with the entered password's hash + final byte[] hash = mLockPatternUtils.passwordToHash(password); + final boolean matched = Arrays.equals(stored, hash); + if (matched && !TextUtils.isEmpty(password)) { + maybeUpdateKeystore(password, userId); + } + return matched; + } catch (FileNotFoundException fnfe) { + Slog.e(TAG, "Cannot read file " + fnfe); + } catch (IOException ioe) { + Slog.e(TAG, "Cannot read file " + ioe); + } + return true; + } + + @Override + public void removeUser(int userId) { + checkWritePermission(userId); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + try { + File file = new File(getLockPasswordFilename(userId)); + if (file.exists()) { + file.delete(); + } + file = new File(getLockPatternFilename(userId)); + if (file.exists()) { + file.delete(); + } + + db.beginTransaction(); + db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void writeFile(String name, byte[] hash) { + try { + // Write the hash to file + RandomAccessFile raf = new RandomAccessFile(name, "rw"); + // Truncate the file if pattern is null, to clear the lock + if (hash == null || hash.length == 0) { + raf.setLength(0); + } else { + raf.write(hash, 0, hash.length); + } + raf.close(); + } catch (IOException ioe) { + Slog.e(TAG, "Error writing to file " + ioe); + } + } + + private void writeToDb(String key, String value, int userId) { + writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId); + } + + private void writeToDb(SQLiteDatabase db, String key, String value, int userId) { + ContentValues cv = new ContentValues(); + cv.put(COLUMN_KEY, key); + cv.put(COLUMN_USERID, userId); + cv.put(COLUMN_VALUE, value); + + db.beginTransaction(); + try { + db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", + new String[] {key, Integer.toString(userId)}); + db.insert(TABLE, null, cv); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private String readFromDb(String key, String defaultValue, int userId) { + Cursor cursor; + String result = defaultValue; + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, + COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", + new String[] { Integer.toString(userId), key }, + null, null, null)) != null) { + if (cursor.moveToFirst()) { + result = cursor.getString(0); + } + cursor.close(); + } + return result; + } + + class DatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "LockSettingsDB"; + private static final String DATABASE_NAME = "locksettings.db"; + + private static final int DATABASE_VERSION = 2; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + setWriteAheadLoggingEnabled(true); + } + + private void createTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE + " (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + COLUMN_KEY + " TEXT," + + COLUMN_USERID + " INTEGER," + + COLUMN_VALUE + " TEXT" + + ");"); + } + + @Override + public void onCreate(SQLiteDatabase db) { + createTable(db); + initializeDefaults(db); + } + + private void initializeDefaults(SQLiteDatabase db) { + // Get the lockscreen default from a system property, if available + boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default", + false); + if (lockScreenDisable) { + writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { + int upgradeVersion = oldVersion; + if (upgradeVersion == 1) { + // Set the initial value for {@link LockPatternUtils#LOCKSCREEN_WIDGETS_ENABLED} + // during upgrade based on whether each user previously had widgets in keyguard. + maybeEnableWidgetSettingForUsers(db); + upgradeVersion = 2; + } + + if (upgradeVersion != DATABASE_VERSION) { + Log.w(TAG, "Failed to upgrade database!"); + } + } + + private void maybeEnableWidgetSettingForUsers(SQLiteDatabase db) { + final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); + final ContentResolver cr = mContext.getContentResolver(); + final List users = um.getUsers(); + for (int i = 0; i < users.size(); i++) { + final int userId = users.get(i).id; + final boolean enabled = mLockPatternUtils.hasWidgetsEnabledInKeyguard(userId); + Log.v(TAG, "Widget upgrade uid=" + userId + ", enabled=" + + enabled + ", w[]=" + mLockPatternUtils.getAppWidgets()); + loadSetting(db, LockPatternUtils.LOCKSCREEN_WIDGETS_ENABLED, userId, enabled); + } + } + + private void loadSetting(SQLiteDatabase db, String key, int userId, boolean value) { + SQLiteStatement stmt = null; + try { + stmt = db.compileStatement( + "INSERT OR REPLACE INTO locksettings(name,user,value) VALUES(?,?,?);"); + stmt.bindString(1, key); + stmt.bindLong(2, userId); + stmt.bindLong(3, value ? 1 : 0); + stmt.execute(); + } finally { + if (stmt != null) stmt.close(); + } + } + } + + private static final String[] VALID_SETTINGS = new String[] { + LockPatternUtils.LOCKOUT_PERMANENT_KEY, + LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE, + LockPatternUtils.PATTERN_EVER_CHOSEN_KEY, + LockPatternUtils.PASSWORD_TYPE_KEY, + LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY, + LockPatternUtils.LOCK_PASSWORD_SALT_KEY, + LockPatternUtils.DISABLE_LOCKSCREEN_KEY, + LockPatternUtils.LOCKSCREEN_OPTIONS, + LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, + LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY, + LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS, + LockPatternUtils.PASSWORD_HISTORY_KEY, + Secure.LOCK_PATTERN_ENABLED, + Secure.LOCK_BIOMETRIC_WEAK_FLAGS, + Secure.LOCK_PATTERN_VISIBLE, + Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED + }; + + // These are protected with a read permission + private static final String[] READ_PROFILE_PROTECTED_SETTINGS = new String[] { + Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, + Secure.LOCK_SCREEN_OWNER_INFO + }; +} diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java new file mode 100644 index 0000000..86f57d1 --- /dev/null +++ b/services/core/java/com/android/server/MasterClearReceiver.java @@ -0,0 +1,55 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.RecoverySystem; +import android.util.Log; +import android.util.Slog; + +import java.io.IOException; + +public class MasterClearReceiver extends BroadcastReceiver { + private static final String TAG = "MasterClear"; + + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(Intent.ACTION_REMOTE_INTENT)) { + if (!"google.com".equals(intent.getStringExtra("from"))) { + Slog.w(TAG, "Ignoring master clear request -- not from trusted server."); + return; + } + } + + Slog.w(TAG, "!!! FACTORY RESET !!!"); + // The reboot call is blocking, so we need to do it on another thread. + Thread thr = new Thread("Reboot") { + @Override + public void run() { + try { + RecoverySystem.rebootWipeUserData(context); + Log.wtf(TAG, "Still running after master clear?!"); + } catch (IOException e) { + Slog.e(TAG, "Can't perform master clear/factory reset", e); + } + } + }; + thr.start(); + } +} diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java new file mode 100644 index 0000000..e60231a --- /dev/null +++ b/services/core/java/com/android/server/MountService.java @@ -0,0 +1,2833 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.Manifest; +import android.app.AppOpsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.ObbInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.hardware.usb.UsbManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Environment; +import android.os.Environment.UserEnvironment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.storage.IMountService; +import android.os.storage.IMountServiceListener; +import android.os.storage.IMountShutdownObserver; +import android.os.storage.IObbActionListener; +import android.os.storage.OnObbStateChangeListener; +import android.os.storage.StorageResultCode; +import android.os.storage.StorageVolume; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IMediaContainerService; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; +import com.android.server.NativeDaemonConnector.Command; +import com.android.server.NativeDaemonConnector.SensitiveArg; +import com.android.server.am.ActivityManagerService; +import com.android.server.pm.PackageManagerService; +import com.android.server.pm.UserManagerService; +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.ArrayList; +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.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** + * MountService implements back-end services for platform storage + * management. + * @hide - Applications should use android.os.storage.StorageManager + * to access the MountService. + */ +class MountService extends IMountService.Stub + implements INativeDaemonConnectorCallbacks, Watchdog.Monitor { + + // TODO: listen for user creation/deletion + + private static final boolean LOCAL_LOGD = false; + private static final boolean DEBUG_UNMOUNT = false; + private static final boolean DEBUG_EVENTS = false; + private static final boolean DEBUG_OBB = false; + + // Disable this since it messes up long-running cryptfs operations. + private static final boolean WATCHDOG_ENABLE = false; + + private static final String TAG = "MountService"; + + private static final String VOLD_TAG = "VoldConnector"; + + /** Maximum number of ASEC containers allowed to be mounted. */ + private static final int MAX_CONTAINERS = 250; + + /* + * Internal vold volume state constants + */ + class VolumeState { + public static final int Init = -1; + public static final int NoMedia = 0; + public static final int Idle = 1; + public static final int Pending = 2; + public static final int Checking = 3; + public static final int Mounted = 4; + public static final int Unmounting = 5; + public static final int Formatting = 6; + public static final int Shared = 7; + public static final int SharedMnt = 8; + } + + /* + * Internal vold response code constants + */ + class VoldResponseCode { + /* + * 100 series - Requestion action was initiated; expect another reply + * before proceeding with a new command. + */ + public static final int VolumeListResult = 110; + public static final int AsecListResult = 111; + public static final int StorageUsersListResult = 112; + + /* + * 200 series - Requestion action has been successfully completed. + */ + public static final int ShareStatusResult = 210; + public static final int AsecPathResult = 211; + public static final int ShareEnabledResult = 212; + + /* + * 400 series - Command was accepted, but the requested action + * did not take place. + */ + public static final int OpFailedNoMedia = 401; + public static final int OpFailedMediaBlank = 402; + public static final int OpFailedMediaCorrupt = 403; + public static final int OpFailedVolNotMounted = 404; + public static final int OpFailedStorageBusy = 405; + public static final int OpFailedStorageNotFound = 406; + + /* + * 600 series - Unsolicited broadcasts. + */ + public static final int VolumeStateChange = 605; + public static final int VolumeUuidChange = 613; + public static final int VolumeUserLabelChange = 614; + public static final int VolumeDiskInserted = 630; + public static final int VolumeDiskRemoved = 631; + public static final int VolumeBadRemoval = 632; + + /* + * 700 series - fstrim + */ + public static final int FstrimCompleted = 700; + } + + private Context mContext; + private NativeDaemonConnector mConnector; + + private final Object mVolumesLock = new Object(); + + /** When defined, base template for user-specific {@link StorageVolume}. */ + private StorageVolume mEmulatedTemplate; + + // TODO: separate storage volumes on per-user basis + + @GuardedBy("mVolumesLock") + private final ArrayList mVolumes = Lists.newArrayList(); + /** Map from path to {@link StorageVolume} */ + @GuardedBy("mVolumesLock") + private final HashMap mVolumesByPath = Maps.newHashMap(); + /** Map from path to state */ + @GuardedBy("mVolumesLock") + private final HashMap mVolumeStates = Maps.newHashMap(); + + private volatile boolean mSystemReady = false; + + private PackageManagerService mPms; + private boolean mUmsEnabling; + private boolean mUmsAvailable = false; + // Used as a lock for methods that register/unregister listeners. + final private ArrayList mListeners = + new ArrayList(); + private final CountDownLatch mConnectedSignal = new CountDownLatch(1); + private final CountDownLatch mAsecsScanned = new CountDownLatch(1); + private boolean mSendUmsConnectedOnBoot = false; + + /** + * Private hash of currently mounted secure containers. + * Used as a lock in methods to manipulate secure containers. + */ + final private HashSet mAsecMountSet = new HashSet(); + + /** + * The size of the crypto algorithm key in bits for OBB files. Currently + * Twofish is used which takes 128-bit keys. + */ + private static final int CRYPTO_ALGORITHM_KEY_SIZE = 128; + + /** + * The number of times to run SHA1 in the PBKDF2 function for OBB files. + * 1024 is reasonably secure and not too slow. + */ + private static final int PBKDF2_HASH_ROUNDS = 1024; + + /** + * Mounted OBB tracking information. Used to track the current state of all + * OBBs. + */ + final private Map> mObbMounts = new HashMap>(); + + /** Map from raw paths to {@link ObbState}. */ + final private Map mObbPathToStateMap = new HashMap(); + + class ObbState implements IBinder.DeathRecipient { + public ObbState(String rawPath, String canonicalPath, int callingUid, + IObbActionListener token, int nonce) { + this.rawPath = rawPath; + this.canonicalPath = canonicalPath.toString(); + + final int userId = UserHandle.getUserId(callingUid); + this.ownerPath = buildObbPath(canonicalPath, userId, false); + this.voldPath = buildObbPath(canonicalPath, userId, true); + + this.ownerGid = UserHandle.getSharedAppGid(callingUid); + this.token = token; + this.nonce = nonce; + } + + final String rawPath; + final String canonicalPath; + final String ownerPath; + final String voldPath; + + final int ownerGid; + + // Token of remote Binder caller + final IObbActionListener token; + + // Identifier to pass back to the token + final int nonce; + + public IBinder getBinder() { + return token.asBinder(); + } + + @Override + public void binderDied() { + ObbAction action = new UnmountObbAction(this, true); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action)); + } + + public void link() throws RemoteException { + getBinder().linkToDeath(this, 0); + } + + public void unlink() { + getBinder().unlinkToDeath(this, 0); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ObbState{"); + sb.append("rawPath=").append(rawPath); + sb.append(",canonicalPath=").append(canonicalPath); + sb.append(",ownerPath=").append(ownerPath); + sb.append(",voldPath=").append(voldPath); + sb.append(",ownerGid=").append(ownerGid); + sb.append(",token=").append(token); + sb.append(",binder=").append(getBinder()); + sb.append('}'); + return sb.toString(); + } + } + + // OBB Action Handler + final private ObbActionHandler mObbActionHandler; + + // OBB action handler messages + private static final int OBB_RUN_ACTION = 1; + private static final int OBB_MCS_BOUND = 2; + private static final int OBB_MCS_UNBIND = 3; + private static final int OBB_MCS_RECONNECT = 4; + private static final int OBB_FLUSH_MOUNT_STATE = 5; + + /* + * Default Container Service information + */ + static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( + "com.android.defcontainer", "com.android.defcontainer.DefaultContainerService"); + + final private DefaultContainerConnection mDefContainerConn = new DefaultContainerConnection(); + + class DefaultContainerConnection implements ServiceConnection { + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG_OBB) + Slog.i(TAG, "onServiceConnected"); + IMediaContainerService imcs = IMediaContainerService.Stub.asInterface(service); + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_MCS_BOUND, imcs)); + } + + public void onServiceDisconnected(ComponentName name) { + if (DEBUG_OBB) + Slog.i(TAG, "onServiceDisconnected"); + } + }; + + // Used in the ObbActionHandler + private IMediaContainerService mContainerService = null; + + // Handler messages + private static final int H_UNMOUNT_PM_UPDATE = 1; + private static final int H_UNMOUNT_PM_DONE = 2; + private static final int H_UNMOUNT_MS = 3; + private static final int H_SYSTEM_READY = 4; + + private static final int RETRY_UNMOUNT_DELAY = 30; // in ms + private static final int MAX_UNMOUNT_RETRIES = 4; + + class UnmountCallBack { + final String path; + final boolean force; + final boolean removeEncryption; + int retries; + + UnmountCallBack(String path, boolean force, boolean removeEncryption) { + retries = 0; + this.path = path; + this.force = force; + this.removeEncryption = removeEncryption; + } + + void handleFinished() { + if (DEBUG_UNMOUNT) Slog.i(TAG, "Unmounting " + path); + doUnmountVolume(path, true, removeEncryption); + } + } + + class UmsEnableCallBack extends UnmountCallBack { + final String method; + + UmsEnableCallBack(String path, String method, boolean force) { + super(path, force, false); + this.method = method; + } + + @Override + void handleFinished() { + super.handleFinished(); + doShareUnshareVolume(path, method, true); + } + } + + class ShutdownCallBack extends UnmountCallBack { + IMountShutdownObserver observer; + ShutdownCallBack(String path, IMountShutdownObserver observer) { + super(path, true, false); + this.observer = observer; + } + + @Override + void handleFinished() { + int ret = doUnmountVolume(path, true, removeEncryption); + if (observer != null) { + try { + observer.onShutDownComplete(ret); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException when shutting down"); + } + } + } + } + + class MountServiceHandler extends Handler { + ArrayList mForceUnmounts = new ArrayList(); + boolean mUpdatingStatus = false; + + MountServiceHandler(Looper l) { + super(l); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case H_UNMOUNT_PM_UPDATE: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_PM_UPDATE"); + UnmountCallBack ucb = (UnmountCallBack) msg.obj; + mForceUnmounts.add(ucb); + if (DEBUG_UNMOUNT) Slog.i(TAG, " registered = " + mUpdatingStatus); + // Register only if needed. + if (!mUpdatingStatus) { + if (DEBUG_UNMOUNT) Slog.i(TAG, "Updating external media status on PackageManager"); + mUpdatingStatus = true; + mPms.updateExternalMediaStatus(false, true); + } + break; + } + case H_UNMOUNT_PM_DONE: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_PM_DONE"); + if (DEBUG_UNMOUNT) Slog.i(TAG, "Updated status. Processing requests"); + mUpdatingStatus = false; + int size = mForceUnmounts.size(); + int sizeArr[] = new int[size]; + int sizeArrN = 0; + // Kill processes holding references first + ActivityManagerService ams = (ActivityManagerService) + ServiceManager.getService("activity"); + for (int i = 0; i < size; i++) { + UnmountCallBack ucb = mForceUnmounts.get(i); + String path = ucb.path; + boolean done = false; + if (!ucb.force) { + done = true; + } else { + int pids[] = getStorageUsers(path); + if (pids == null || pids.length == 0) { + done = true; + } else { + // Eliminate system process here? + ams.killPids(pids, "unmount media", true); + // Confirm if file references have been freed. + pids = getStorageUsers(path); + if (pids == null || pids.length == 0) { + done = true; + } + } + } + if (!done && (ucb.retries < MAX_UNMOUNT_RETRIES)) { + // Retry again + Slog.i(TAG, "Retrying to kill storage users again"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(H_UNMOUNT_PM_DONE, + ucb.retries++), + RETRY_UNMOUNT_DELAY); + } else { + if (ucb.retries >= MAX_UNMOUNT_RETRIES) { + Slog.i(TAG, "Failed to unmount media inspite of " + + MAX_UNMOUNT_RETRIES + " retries. Forcibly killing processes now"); + } + sizeArr[sizeArrN++] = i; + mHandler.sendMessage(mHandler.obtainMessage(H_UNMOUNT_MS, + ucb)); + } + } + // Remove already processed elements from list. + for (int i = (sizeArrN-1); i >= 0; i--) { + mForceUnmounts.remove(sizeArr[i]); + } + break; + } + case H_UNMOUNT_MS: { + if (DEBUG_UNMOUNT) Slog.i(TAG, "H_UNMOUNT_MS"); + UnmountCallBack ucb = (UnmountCallBack) msg.obj; + ucb.handleFinished(); + break; + } + case H_SYSTEM_READY: { + try { + handleSystemReady(); + } catch (Exception ex) { + Slog.e(TAG, "Boot-time mount exception", ex); + } + break; + } + } + } + }; + + private final Handler mHandler; + + void waitForAsecScan() { + waitForLatch(mAsecsScanned); + } + + private void waitForReady() { + waitForLatch(mConnectedSignal); + } + + private void waitForLatch(CountDownLatch latch) { + for (;;) { + try { + if (latch.await(5000, TimeUnit.MILLISECONDS)) { + return; + } else { + Slog.w(TAG, "Thread " + Thread.currentThread().getName() + + " still waiting for MountService ready..."); + } + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupt while waiting for MountService to be ready."); + } + } + } + + private void handleSystemReady() { + // Snapshot current volume states since it's not safe to call into vold + // while holding locks. + final HashMap snapshot; + synchronized (mVolumesLock) { + snapshot = new HashMap(mVolumeStates); + } + + for (Map.Entry entry : snapshot.entrySet()) { + final String path = entry.getKey(); + final String state = entry.getValue(); + + if (state.equals(Environment.MEDIA_UNMOUNTED)) { + int rc = doMountVolume(path); + if (rc != StorageResultCode.OperationSucceeded) { + Slog.e(TAG, String.format("Boot-time mount failed (%d)", + rc)); + } + } else if (state.equals(Environment.MEDIA_SHARED)) { + /* + * Bootstrap UMS enabled state since vold indicates + * the volume is shared (runtime restart while ums enabled) + */ + notifyVolumeStateChange(null, path, VolumeState.NoMedia, + VolumeState.Shared); + } + } + + // Push mounted state for all emulated storage + synchronized (mVolumesLock) { + for (StorageVolume volume : mVolumes) { + if (volume.isEmulated()) { + updatePublicVolumeState(volume, Environment.MEDIA_MOUNTED); + } + } + } + + /* + * If UMS was connected on boot, send the connected event + * now that we're up. + */ + if (mSendUmsConnectedOnBoot) { + sendUmsIntent(true); + mSendUmsConnectedOnBoot = false; + } + } + + private final BroadcastReceiver mUserReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId == -1) return; + final UserHandle user = new UserHandle(userId); + + final String action = intent.getAction(); + if (Intent.ACTION_USER_ADDED.equals(action)) { + synchronized (mVolumesLock) { + createEmulatedVolumeForUserLocked(user); + } + + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + synchronized (mVolumesLock) { + final List toRemove = Lists.newArrayList(); + for (StorageVolume volume : mVolumes) { + if (user.equals(volume.getOwner())) { + toRemove.add(volume); + } + } + for (StorageVolume volume : toRemove) { + removeVolumeLocked(volume); + } + } + } + } + }; + + private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + boolean available = (intent.getBooleanExtra(UsbManager.USB_CONNECTED, false) && + intent.getBooleanExtra(UsbManager.USB_FUNCTION_MASS_STORAGE, false)); + notifyShareAvailabilityChange(available); + } + }; + + private final BroadcastReceiver mIdleMaintenanceReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + waitForReady(); + String action = intent.getAction(); + // Since fstrim will be run on a daily basis we do not expect + // fstrim to be too long, so it is not interruptible. We will + // implement interruption only in case we see issues. + if (Intent.ACTION_IDLE_MAINTENANCE_START.equals(action)) { + try { + // This method runs on the handler thread, + // so it is safe to directly call into vold. + mConnector.execute("fstrim", "dotrim"); + EventLogTags.writeFstrimStart(SystemClock.elapsedRealtime()); + } catch (NativeDaemonConnectorException ndce) { + Slog.e(TAG, "Failed to run fstrim!"); + } + } + } + }; + + private final class MountServiceBinderListener implements IBinder.DeathRecipient { + final IMountServiceListener mListener; + + MountServiceBinderListener(IMountServiceListener listener) { + mListener = listener; + + } + + public void binderDied() { + if (LOCAL_LOGD) Slog.d(TAG, "An IMountServiceListener has died!"); + synchronized (mListeners) { + mListeners.remove(this); + mListener.asBinder().unlinkToDeath(this, 0); + } + } + } + + private void doShareUnshareVolume(String path, String method, boolean enable) { + // TODO: Add support for multiple share methods + if (!method.equals("ums")) { + throw new IllegalArgumentException(String.format("Method %s not supported", method)); + } + + try { + mConnector.execute("volume", enable ? "share" : "unshare", path, method); + } catch (NativeDaemonConnectorException e) { + Slog.e(TAG, "Failed to share/unshare", e); + } + } + + private void updatePublicVolumeState(StorageVolume volume, String state) { + final String path = volume.getPath(); + final String oldState; + synchronized (mVolumesLock) { + oldState = mVolumeStates.put(path, state); + volume.setState(state); + } + + if (state.equals(oldState)) { + Slog.w(TAG, String.format("Duplicate state transition (%s -> %s) for %s", + state, state, path)); + return; + } + + Slog.d(TAG, "volume state changed for " + path + " (" + oldState + " -> " + state + ")"); + + // Tell PackageManager about changes to primary volume state, but only + // when not emulated. + if (volume.isPrimary() && !volume.isEmulated()) { + if (Environment.MEDIA_UNMOUNTED.equals(state)) { + mPms.updateExternalMediaStatus(false, false); + + /* + * Some OBBs might have been unmounted when this volume was + * unmounted, so send a message to the handler to let it know to + * remove those from the list of mounted OBBS. + */ + mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage( + OBB_FLUSH_MOUNT_STATE, path)); + } else if (Environment.MEDIA_MOUNTED.equals(state)) { + mPms.updateExternalMediaStatus(true, false); + } + } + + synchronized (mListeners) { + for (int i = mListeners.size() -1; i >= 0; i--) { + MountServiceBinderListener bl = mListeners.get(i); + try { + bl.mListener.onStorageStateChanged(path, oldState, state); + } catch (RemoteException rex) { + Slog.e(TAG, "Listener dead"); + mListeners.remove(i); + } catch (Exception ex) { + Slog.e(TAG, "Listener failed", ex); + } + } + } + } + + /** + * Callback from NativeDaemonConnector + */ + public void onDaemonConnected() { + /* + * Since we'll be calling back into the NativeDaemonConnector, + * we need to do our work in a new thread. + */ + new Thread("MountService#onDaemonConnected") { + @Override + public void run() { + /** + * Determine media state and UMS detection status + */ + try { + final String[] vols = NativeDaemonEvent.filterMessageList( + mConnector.executeForList("volume", "list"), + VoldResponseCode.VolumeListResult); + for (String volstr : vols) { + String[] tok = volstr.split(" "); + // FMT: