diff --git a/Android.mk b/Android.mk index 1b5c70b6ae1deeeb2ced4188aa2b59f271b7fc7c..5034c7ef517ac2b048a745da9f11d859fd5b7c36 100644 --- a/Android.mk +++ b/Android.mk @@ -65,8 +65,11 @@ endif ## READ ME: ######################################################## LOCAL_SRC_FILES += \ core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl \ - core/java/android/accessibilityservice/IEventListener.aidl \ - core/java/android/accounts/IAccountsService.aidl \ + core/java/android/accessibilityservice/IEventListener.aidl \ + core/java/android/accounts/IAccountManager.aidl \ + core/java/android/accounts/IAccountManagerResponse.aidl \ + core/java/android/accounts/IAccountAuthenticator.aidl \ + core/java/android/accounts/IAccountAuthenticatorResponse.aidl \ core/java/android/app/IActivityController.aidl \ core/java/android/app/IActivityPendingResult.aidl \ core/java/android/app/IActivityWatcher.aidl \ @@ -80,15 +83,16 @@ LOCAL_SRC_FILES += \ core/java/android/app/IStatusBar.aidl \ core/java/android/app/IThumbnailReceiver.aidl \ core/java/android/app/ITransientNotification.aidl \ - core/java/android/app/IWallpaperService.aidl \ - core/java/android/app/IWallpaperServiceCallback.aidl \ + core/java/android/app/IWallpaperManager.aidl \ + core/java/android/app/IWallpaperManagerCallback.aidl \ core/java/android/backup/IBackupManager.aidl \ core/java/android/backup/IRestoreObserver.aidl \ core/java/android/backup/IRestoreSession.aidl \ + core/java/android/bluetooth/IBluetooth.aidl \ core/java/android/bluetooth/IBluetoothA2dp.aidl \ - core/java/android/bluetooth/IBluetoothDevice.aidl \ - core/java/android/bluetooth/IBluetoothDeviceCallback.aidl \ + core/java/android/bluetooth/IBluetoothCallback.aidl \ core/java/android/bluetooth/IBluetoothHeadset.aidl \ + core/java/android/bluetooth/IBluetoothPbap.aidl \ core/java/android/content/IContentService.aidl \ core/java/android/content/IIntentReceiver.aidl \ core/java/android/content/IIntentSender.aidl \ @@ -111,6 +115,9 @@ LOCAL_SRC_FILES += \ core/java/android/os/IParentalControlCallback.aidl \ core/java/android/os/IPermissionController.aidl \ core/java/android/os/IPowerManager.aidl \ + core/java/android/service/wallpaper/IWallpaperConnection.aidl \ + core/java/android/service/wallpaper/IWallpaperEngine.aidl \ + core/java/android/service/wallpaper/IWallpaperService.aidl \ core/java/android/text/IClipboard.aidl \ core/java/android/view/accessibility/IAccessibilityManager.aidl \ core/java/android/view/accessibility/IAccessibilityManagerClient.aidl \ @@ -137,13 +144,13 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/view/IInputMethodClient.aidl \ core/java/com/android/internal/view/IInputMethodManager.aidl \ core/java/com/android/internal/view/IInputMethodSession.aidl \ - im/java/android/im/IImPlugin.aidl \ location/java/android/location/IGeocodeProvider.aidl \ location/java/android/location/IGpsStatusListener.aidl \ location/java/android/location/IGpsStatusProvider.aidl \ location/java/android/location/ILocationListener.aidl \ location/java/android/location/ILocationManager.aidl \ location/java/android/location/ILocationProvider.aidl \ + location/java/android/location/INetInitiatedListener.aidl \ media/java/android/media/IAudioService.aidl \ media/java/android/media/IMediaScannerListener.aidl \ media/java/android/media/IMediaScannerService.aidl \ @@ -195,9 +202,13 @@ framework_built := $(LOCAL_BUILT_MODULE) # relative to the root of the build tree. # ============================================================ aidl_files := \ - frameworks/base/core/java/android/accounts/IAccountsService.aidl \ + frameworks/base/core/java/android/accounts/IAccountManager.aidl \ + frameworks/base/core/java/android/accounts/IAccountManagerResponse.aidl \ + frameworks/base/core/java/android/accounts/IAccountAuthenticator.aidl \ + frameworks/base/core/java/android/accounts/IAccountAuthenticatorResponse.aidl \ frameworks/base/core/java/android/app/Notification.aidl \ frameworks/base/core/java/android/app/PendingIntent.aidl \ + frameworks/base/core/java/android/bluetooth/BluetoothDevice.aidl \ frameworks/base/core/java/android/content/ComponentName.aidl \ frameworks/base/core/java/android/content/Intent.aidl \ frameworks/base/core/java/android/content/IntentSender.aidl \ @@ -207,6 +218,7 @@ aidl_files := \ frameworks/base/core/java/android/net/Uri.aidl \ frameworks/base/core/java/android/os/Bundle.aidl \ frameworks/base/core/java/android/os/ParcelFileDescriptor.aidl \ + frameworks/base/core/java/android/os/ParcelUuid.aidl \ frameworks/base/core/java/android/view/KeyEvent.aidl \ frameworks/base/core/java/android/view/MotionEvent.aidl \ frameworks/base/core/java/android/view/Surface.aidl \ @@ -221,7 +233,6 @@ aidl_files := \ frameworks/base/graphics/java/android/graphics/Bitmap.aidl \ frameworks/base/graphics/java/android/graphics/Rect.aidl \ frameworks/base/graphics/java/android/graphics/Region.aidl \ - frameworks/base/im/java/android/im/IImPlugin.aidl \ frameworks/base/location/java/android/location/Criteria.aidl \ frameworks/base/location/java/android/location/Location.aidl \ frameworks/base/telephony/java/android/telephony/ServiceState.aidl \ @@ -333,6 +344,7 @@ framework_docs_LOCAL_DROIDDOC_OPTIONS := \ -since ./frameworks/base/api/2.xml 2 \ -since ./frameworks/base/api/3.xml 3 \ -since ./frameworks/base/api/4.xml 4 \ + -since ./frameworks/base/api/5.xml 5 \ -error 1 -error 2 -warning 3 -error 4 -error 6 -error 8 \ -overview $(LOCAL_PATH)/core/java/overview.html @@ -361,18 +373,56 @@ web_docs_sample_code_flags := \ ## SDK version identifiers used in the published docs # major[.minor] version for current SDK. (full releases only) -framework_docs_SDK_VERSION:=1.5 +framework_docs_SDK_VERSION:=2.0 # release version (ie "Release x") (full releases only) -framework_docs_SDK_REL_ID:=3 +framework_docs_SDK_REL_ID:=1 # name of current SDK directory (full releases only) framework_docs_SDK_CURRENT_DIR:=$(framework_docs_SDK_VERSION)_r$(framework_docs_SDK_REL_ID) # flag to build offline docs for a preview release -framework_docs_SDK_PREVIEW:=true +framework_docs_SDK_PREVIEW:=0 + +## Latest ADT version identifiers, for reference from published docs +framework_docs_ADT_VERSION:=0.9.4 +framework_docs_ADT_DOWNLOAD:=ADT-0.9.4.zip +framework_docs_ADT_BYTES:=3367536 +framework_docs_ADT_CHECKSUM:=4cdecd72b3e28022d8a55891f13e7d43 framework_docs_LOCAL_DROIDDOC_OPTIONS += \ -hdf sdk.version $(framework_docs_SDK_VERSION) \ -hdf sdk.rel.id $(framework_docs_SDK_REL_ID) \ - -hdf sdk.current $(framework_docs_SDK_CURRENT_DIR) + -hdf sdk.current $(framework_docs_SDK_CURRENT_DIR) \ + -hdf adt.zip.version $(framework_docs_ADT_VERSION) \ + -hdf adt.zip.download $(framework_docs_ADT_DOWNLOAD) \ + -hdf adt.zip.bytes $(framework_docs_ADT_BYTES) \ + -hdf adt.zip.checksum $(framework_docs_ADT_CHECKSUM) + +# ==== the api stubs and current.xml =========================== +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:=$(framework_docs_LOCAL_SRC_FILES) +LOCAL_INTERMEDIATE_SOURCES:=$(framework_docs_LOCAL_INTERMEDIATE_SOURCES) +LOCAL_JAVA_LIBRARIES:=$(framework_docs_LOCAL_JAVA_LIBRARIES) +LOCAL_MODULE_CLASS:=$(framework_docs_LOCAL_MODULE_CLASS) +LOCAL_DROIDDOC_SOURCE_PATH:=$(framework_docs_LOCAL_DROIDDOC_SOURCE_PATH) +LOCAL_DROIDDOC_HTML_DIR:=$(framework_docs_LOCAL_DROIDDOC_HTML_DIR) +LOCAL_ADDITIONAL_JAVA_DIR:=$(framework_docs_LOCAL_ADDITIONAL_JAVA_DIR) + +LOCAL_MODULE := api-stubs + +LOCAL_DROIDDOC_OPTIONS:=\ + $(framework_docs_LOCAL_DROIDDOC_OPTIONS) \ + -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android_stubs_current_intermediates/src \ + -apixml $(INTERNAL_PLATFORM_API_FILE) \ + -nodocs + +LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR:=build/tools/droiddoc/templates-sdk +LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk + +include $(BUILD_DROIDDOC) + +$(full_target): $(framework_built) +$(INTERNAL_PLATFORM_API_FILE): $(full_target) +$(call dist-for-goals,sdk,$(INTERNAL_PLATFORM_API_FILE)) # ==== static html in the sdk ================================== include $(CLEAR_VARS) @@ -392,10 +442,7 @@ LOCAL_DROIDDOC_OPTIONS:=\ -title "Android SDK" \ -proofread $(OUT_DOCS)/$(LOCAL_MODULE)-proofread.txt \ -todo $(OUT_DOCS)/$(LOCAL_MODULE)-docs-todo.html \ - -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android_stubs_current_intermediates/src \ - -apixml $(INTERNAL_PLATFORM_API_FILE) \ -sdkvalues $(OUT_DOCS) \ - -warning 3 \ -hdf android.whichdoc offline ifeq ($(framework_docs_SDK_PREVIEW),true) @@ -415,8 +462,6 @@ $(static_doc_index_redirect): \ $(full_target): $(static_doc_index_redirect) $(full_target): $(framework_built) -$(INTERNAL_PLATFORM_API_FILE): $(full_target) -$(call dist-for-goals,sdk,$(INTERNAL_PLATFORM_API_FILE)) # ==== docs for the web (on the google app engine server) ======================= @@ -445,6 +490,8 @@ LOCAL_DROIDDOC_CUSTOM_ASSET_DIR:=assets-sdk include $(BUILD_DROIDDOC) +# explicitly specify that online-sdk depends on framework-res. +$(full_target): framework-res-package-target # ==== docs that have all of the stuff that's @hidden ======================= include $(CLEAR_VARS) @@ -501,5 +548,3 @@ include $(BUILD_JAVA_LIBRARY) ifeq (,$(ONE_SHOT_MAKEFILE)) include $(call first-makefiles-under,$(LOCAL_PATH)) endif - - diff --git a/api/4.xml b/api/4.xml index fc54859f736925ca4d08c9512505c853c8bb8756..96890721dca60e45c5acc24c4bffb72c18a64803 100644 --- a/api/4.xml +++ b/api/4.xml @@ -51157,7 +51157,7 @@ + + + + + + + + + + + + @@ -277334,7 +277400,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/6.xml b/api/6.xml new file mode 100644 index 0000000000000000000000000000000000000000..32d0f1ed5201be929846e09a7094fb27b496d1bf --- /dev/null +++ b/api/6.xml @@ -0,0 +1,371336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/current.xml b/api/current.xml index fc54859f736925ca4d08c9512505c853c8bb8756..32d0f1ed5201be929846e09a7094fb27b496d1bf 100644 --- a/api/current.xml +++ b/api/current.xml @@ -122,6 +122,28 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + - - + - + + + + + + + + + - + + + + + + + + - + + + + - - + - - - + + + - - + + + + + + + + + - - + - - - - - + + + + + + + - - + + + + + + + + + - + - - - - + + - - + + + + + - + - - + - - - - - - + + + - + + - - + + + + - - - - + + + - + - - + + - - + + + - + + + + - - + + - + + + + - + + + - - + + + + + + + + + + + + + + - - + + + + + - + + + + + + - + + + + + + + + + + + + - + + - + + + + + + + + + + - + + + + + + + + - + + - - + + - + + + + + + + + - + + + + + + + + + + + + - + + + + + + + + + + - - + - + - + - + - + + + + + + + - - - - - - - - - + - - + - + - - + + + - + + + + - - + + + + + - - + - - + + + + + - - + - - - + - + + + + + + - - + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + - - - + - - - + + + + + + - - + - + + + + + + + - - - - - - - - - + - - - - - - + + - - - - - - - - + - + - + + - - + - + - + - + + + + - + + + - - + + + + + + + + - + + + + - - + + + + + + + + + + + - - - - + - - + - + - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + - + - + - + + - - + - + + - + + - - + - + + - - + + - - + - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - + - + + - - - - - - - - + + + - - + - - - - - + - - + - - - - + - - + - + - + - - - - - - - + - - + - - - - - - + - + - - + - - + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - + - + - - + + + + + - + - - + + + + + + + + + - + + + + - - - + + + + + - - + + + - + + - - - - + - - - + + - - + + + - - - - - - - - - + - - - - - - - + - - + + + + + + + - - + + + + + - - - - - - - - - - + + + - - + - - - + - - - + + + - - + - - - - - + - + - - - - - - - - - - - - - - - + + + - - + + + - - + - - + - - + + + - - - + + + - - - - - + + + - - + + + - - - - + - - - + + + - - + + + - - + + + - - - + + + - - - - - + + + - - - + - + - - + + + - - + + + - - + - + - - - - - - - - - - - + + + + + - - + + + + + - - + + + - - + + + - - + + + - - + + + - - - + + + - - - - - + + + + + - - + - - + - + - - - - - - - - - - - - - - - - - + + + - - + + + - - + + + - - - + + + - - - - - + + + + + - - + - - + - + - - - - - - - - - + + + + + + + - - + + + + + - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - - + + + - + + + - + + + + + + + - - - - - - + - - - - - - - - + - - - - - - + - + - - + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - + - - - + + - - - - - - - - - - - - - + - - - - + - - - - - - - + - + + + - - - - - - - - - + + + - - - - + + + - - + + + - - + - + + + - - - - + + - - - - + - - - - + - - - - - - - - + - + - - - + - - - - + - - + - - - - - - + + - - - - + + - - - - - - - - + - - - - + - - + + + - + + + + + + + + - - - - + - - - - - - + - - - - - - + - - - - + - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - + + + + + - - - - - - + - - - - - - - - + - - + - + - + + - - - - + - - - - + - - - - + + - - - - - - + + - - - - - - + - + - + - + - - + - - - - - - - - - - - - - - + - - - - + - + - - - - - + - - - - + - - - - + - - - + + + + + + + + + + + - + - - - + - - - + - - + - - - - + - - + - - + - - - + - - - - - + + - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + - - - + - - - - - - - - - - - + - - - - - - - - - + - - - - + - - - - - - + - - - - - - - - + - - - - - - + - - + - - + - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - + - - + - - + + + + + + + - - + - + + - + + + + - + + - - + - - + - - - - + + + + + + + + + + - - - - - + - - + - + - + - - + + + + + + + - - + - + + + + + - - + - + + + + + + + + + + + + + + + + + + + - - - - - - + + - - - - - + - - + - + - + - - + - + - + - - + + + - - - + - - - - - + + + + + - - + - + + + - - + + + - - - - + - + - - + + + - + + + + - + + + + - + + + + - + + - - + - - + - - + - - + - - + + + + + + + + + + - + + - + - - + - - - - + + + - + - - + + + + + - - + - - + - - - - + - - + - - + - + - - + - + - - - - + - - - - + - - + + + + + - - + + + + + - - + + + + + + + - - + + + - - + + + - - + + + - - + + + - + + - - + - - + - - - - - - - - - - - - - + + + + - + + + + - + + + + + + - + + + + + + + + - - - - - - + - + - + - - + + + + + - - + - - + - - - - - - + - - - - - - + - - - - + + - - - - - - - - + - - - - - + - - - - - - - - - - - - - - + - - - - + + + + - + - + - - + + + + + + + - + - - + + + + + + + + + + + - + - + - - + + + + + + + - - + - + + + - + - - - - - + + + + + + + - + - - - - + + + + + + + + - + - - + - + - + - - + + + + + + + + + + + + + - - - - - - - - + + - + + - + + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - + + + + + - - - - + - - + + + - - + - - - + + + - + + + + - - + + + + + - + + + + - + + + + - + + + + - + + - + + - - + + + - + + - - + + + + + - - - - + - - - + - - - + - + - - - - - - - - + - - - - - + - - + + + - + + - + + - + + - - + + - - + - - - - - - + - - + + + - + + - - - + + + - + - - + - + + + - - + - - - + + + + + - + + + - + + + + - - + + + - - - + + + + + - + + - - - - + - - - + + + - - + + + + + - - - - - + - + + - - + + + + + + + + - - - - - - + - - + - - + - - + + + + + + + + + + + - - + + + - - + + + - - + + + - + + + + + + + + + + + - - - + + - + + + + + + - + + + + + + - + + + + - - - - + - + + + - + - - + - - + + + - - - + + + - + - + - - + - - - + - - + + + - - + - - + - + + - + + - - + + + - - + + + + - - - - - - - - - - - - + - - + - + - + - + - - + - + - - + + + + + - + + - + - + - + + + + + + + + + + + + + + + - - + - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + + + + + + + + + + - - + + + - - + - - + + + + + - - + - - + + + - - + + + - - - + + + - + + + - + - + + - - + - + - - - - - + + - + + - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + - - + - + - - - - - - - - - - - - - + + + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - - - + - + + - + - - + + + - - + - + + - - + - + - - + - - + - + - - - - - + - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + + - - - - - + + - - - - + - - + - - - - - - - - - - - - - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + - - - - - - + - - - - + - - - - - - - - + - - + - + - + - + + + - - - - + - - + - + - - - - + + + + + + + + + - - + - - - - + - - + - - - - - - + - - - - - - - - - + - - - - - - + + + - - - - + - - - - - - - - - - - - - - - - - + - + - + - + + + - - - - - - - + - + - + + + - - + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + - + - + - - - - - - + - - + - - - - - - - - + - - + - - - - + - - + - - - - - - - - + - - - - - - + - - + - - - + - + - - - - + - - - - - - - - + + - - + + - + + + + - - - + + + - - + - + + + - + + + + - - - - - + - + - + + + + + - - + + + + + + + - - + + + + + + + + + - - - + - - - - - + + - - - - + + + - - + - + + + + + - - - - - - - - - + + + + + + + + + - - + + + + + + + + + + + + + - - + + + + + - - + + + + + - - - - - - - - - - + - - + - + - - + + - - - - - + - - - + - + - + - - + - - - + + - - + + + - - - - - - - - - - - + - - + + + - - + + + - - + - - + - - + + + - - + + + - - - - - + + + - + + + - - + + + - + - - - + - + + + - + - - + + + + + - + + + + + + + - - + + + + + + + + + - - + - + - + + + + + + + - + + + + + + + + + + - - - - - + - - - - - - + - - - - - - + - - - - - - - - - - + - - + - + - + - + + + - - - - - - - - - - - - - - - - - - - - - - - + - + - + - - - + - - + - - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + - - - - - + + - + + - + + + + + + - - + + - + + - - - - - - - - - + + + + + + - + - + + + - - + - - + - - - - + - + - - + - - - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + - + - + + - - + - + - + - - + - + + + + + + + - - + - + + + + + + + + + - - + - + - - + - + - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - - - - - - + + + - + + + + - - + - - + - + + + - + - + - - - - - - - - - - + + - - - - + - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + + - + + + + - + + + + - - - - + - + - - + - + - - - - + - + + + - - - - - - - - - - + - + - - - - - + + + + - + + - - + + + - - - - - - + - + - + - - - + - - - - - - - + - - + - + + + + + + - - + + + - - - - - - - - + - + - - + + + - - - - - - - - - - - - - - - - - - - - - + - - - - + - + - - - - + - - - + - - - - - - - - - + + + - - - - + - - - - - - + - - - - - - - - - - - - - - - - - - - + - - + - + - - + + + - - - - - - + - + - - - - - + - - - - - - - - - - - - - - + - + - - + - - + - + + + + + + + + + + + + + - - - - + - - - - - + + + + + - + + - - - - + - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + + + + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + + + + + - - + - - - - + - - - - - - + - - - - + - - + - - - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - + - - - - - - - - + - - + - - - - + - - - - + - - - - - - + + + + - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - + - - - + + - - - - + - - + - - + - - + - - + - - - - + - - - - - - + - - - - + - - + - - + - - + - - + - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - + - - + - - + - - + - - + - - - - - - - - - - + - - - - + - - - - - - - - + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - - - + - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - + - - - - - - + - - - - + - - - - - - - - + - - - - + - - - - + - - - - + - - - - + - - - - - - - - + + + + - - - - - - - - - - - + + + + - - - - - - - - - - + + - + - - + + + - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + - - - - - - - - + + - - - - - - - - - - - - + + + - - + - - - - - - - - + + + + - - - + + - - - + + - - - - - + + - - - - - - - - + + + - - - - - - - - - + - + - - + - - - - - - - - - - - - - - - - - - - - - + - - - - - + - + - + - + - - - - - + + - - - - + - - + + + + - - - - + + + + + - - + - - + - + + + - - + + + + + - + + + + + + - + + + + + + - + + + + + + + + + + - + + + + + + + + - + + + + + + + + + + + + + + - + + + + + + + + + + + + - + - - - - - - - - + - - - - + - - + - - + - - + - - + - - - - - - - - + - - - - - - + - - - - - - - - + - - - - - - - - + + - - - - - + - - + + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - + + - - - - - - + - - - - + - - + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - - - + - - + - - - - - - + - - - - - - - - - - + + - - - - + - - - + - - + - + - - + - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - + - - + + - - + + + - - + - + - + + + - - - - + + - - - - + - - - + + + - + + + + - - + + + - - + - + + + - - - - - - + + - - - - - + - + - - + + - - - - - - - - - - + + + + + - - + - + + + - - + + + + + - - + + + + + + + + + - - - - + - - - - + - + + + + - + - + + + + + - + + + + + + + + - - + + + + + - - + + + + + + + - - - - + + + + - - + + + + + - - + + + + + + + - - + - + + + + + - - + + + + + + + + + + + - - + + + + + + + + + + + - + + + + - + + + + + + + - - + + - - - - - - - - - - - - + - - - - - - - - + - - - - - - + - - - - + - - + - + - - + - + - - + - + - + + + + - - - - - - - - + - - + - + - - + - + - - + - + - - + - + - - - - + - - - - + - - - - + - + - + + + + - + + + - - + + + + + - - - - - - + - + - + + + + + + + - + + - + - - + + + + + - - - - - - - - - - - - - + - - - - - - - - - + - + + + + + + - + - - + - - + - + - - + - + + + + + - - + - + - - + - - + + + - - + + + + + - - + + + - - - - - - - - + - + - - + - + + + + + + - + + + - - + + + - - + + + - - + - + + + - - + - + + + - - + - + + + - - + + + - - + + + - - + - + + + - - + + + + + + + + + - - + + + + + - - + - - + + + + + - + + + + + + - - + - - + - + + + - - - + + + + + - - + + + - - + + + + + + + + + - - + + + - - - - - - - - - - - - - + - - - - + - - + + + + + - - + + + - - + + + + + + - - + + - - + - - + + + - - + + + - - + - - + + + - - + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + - - + + + - - + + + - - + - - + - - + + + + + - - + - - - - + - - + + + + + + + - - + + + - - + + + + + - - + + + - - + + + + + - - + + + + + + + - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - + + + + + + + - - + - - + + + - - + + + - - + + + + + - - + + + + + + + - - + + + + + - - + + + + + + + - - + + + + + + + + + - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + - - + - - + - - + - - + - - + - - + - - + - - + + + - - + + + + + - - + + + - - + - - + - - + - - + - - + - - + + + + + - - + + + - - + + + + + - - + + + - - + + + - - + + + + + + + - - + + + + + + + - - + - - + + + - - + + + + + + + - - + + + + + - - + + + + + + + + + - - + + + + + - - + + + + + + + - - + + + + + + + + + - - + + + + + - - + + + + + + + + + - - + + + - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + - + + + - + - - + + + + + + + - - - - - + + + + + - - + + + + + + + - - + - + + + - - + - + + + + + - - - + + + + + + + + + + + + + - - + + + - - - + + + + + + + - - - + - - + + + - + - - + - + - + - - - - + - + - - + + + + + + + + + - - + + + + + - - + + + + + + + - - + - + + + + + + + - - + - + + + + + + + + + + + - - - - - - - - - - - - - - - + + - + + + + - - - - + - - - - - - - - - - + + - - - - - - - - - - + + + - - + - - - - - - - - - - - - + + + + + - - + + + - - + + + + + - - - - + - + - + - - + + + - + - + + + - + - + - + - - + - - + + + - - - - - - + - - + + + - + + + + - - + + + + + + + + + + + + + - - - - - + + - - + + + + + + + + + + + - + + - - - - + + + - - + - - - + + + - - + + + + + + + - - + + + + + + + + + + + + + - - + + + - - + + + - - + + + - - + + + + - - - - + - - + - - - - - - - + - - - - + - - + + - - + + + + - + + - + - - + - - - + + + + + + + + + - + - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - - - - - - - - - - - + + - + - + - - + + + - - - - + + + - + - - - - - - - - - - - - - + + + - - + + + + + - - + - + - - - + - + + + + + - - - - + - + - - - + + + + + - + - - - + - + - - + - - - - - - - - + - - + - + - - - - - - - + + + - + + + - + - - - - + - - - - + - + - + + + - - - + + + + + - - - + + + + + - + + + - + - + - - - - - - + + + - + - + + - - - - - + - - - - + - - + - + - - + - + + + - - + - + + + + + - - + - - + + + + + + + + + + + - - - + + + + + + + + + + + - + + + + - - + - + - - + - + - - + - - + - - + - + - - + + + - - - + + + + + - + - + - - - + + + + + - + - - + + + - - - + - + - - + + + - - - - - - + + + + + - - - + + + + + - - + + + + + - + - - + + + + + + + - - + - + - + + + + - - - + + + + + - - + + + + + - - + + + - - + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - + - - + - - - - - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - - + - + - - + - - - - - - - - + - - + - - - - - - - - + - - + - - - - + - - + - - + - - - - - - + - - - - - - - - - - - - - + - - + - - - - + - + + + + + + + + + + + + + + + - - - - - - - - - - - + - - + - - + - - - - - - + - - - - - - - - - - - - - + + + + + + + + - - - - - - + - - + - - - - + - - - - + - - - - - - + - - - - - - + - - - - + - - - - + - - - - - - + - - - - - - + - - - - - + + + + + + + + + + + + + + - - - - - + - - - - - - - + + - - - - - + + + - - - - - - + - + - + + - - - - - - + - - - - - - + + - - - - + + - - - - - - + - + - - - - + - + - + - + - - + - + - - + - - - - + - - + - - - - + - + - - - - + + + - - + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - + - + - - - - - - + - - - - - - - - + - - + - - - - + - - - - + - - - - + - - - - - - - - - - - + - - - + + - - - - - - + - - - - - - + - + + - - - - + - - + - + - + + + - - + - + + + + + + + + + - - - - + - - - - - - - - + - - + - + + + - - - - + - - - - - - - + + + + + - - - - - - - - - + - - - - - - - - - + - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + + + + + - - + - - + - + + + - - - - - - - - - + - + - - + - + - - + + + + + + + + + + + + + + + - + - + - + - - - - + + + + + + + + + + + + - - - - - + - + + + - - - - - - - + - + - - - - - - + + + + + - - - - - - - - + - - - + + + + + - + - - + - - + + + - - + + + + + - + - - - - + + - + + - - - + + + + + + - - + - - - + + + + + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - + + + + + - - + + + + - - - - - - - - + + + - - - - + - - - - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + - - + + + + - - - - - + - - - + - - + + + + + + + + + - - + - + - + + + + - - + + + - - + - + + - + - + - + - - + + - - + + + + + + + + + - - + + + - - + - - + - - + + + + + - - + - - + + + + + - - - - - - - - - - - + - - - + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + - - - - - - - + - - - - - - - - + - - - - + - - - - + + + + + - - - - + - - + + - - + + - - + - - + + + - - + @@ -36212,331 +38916,236 @@ visibility="public" > - - - - + - - - - - - - + - + - - - - - - - + - + - - + - - + - - - - - - + + - - - - - - - - - + - - - + - - - - - - - + - - - - + - + - + - - - - - + - - - - - - + - - - - + + - - - - - - - - + + - - - - + - - - - + + + - - + + + - - - - - - - - - - - - + - - - - - - - - - - + - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + - - + - - + - - - - + - - - - + - - - - - + - - - - - - - - - - - - - - - - - + - - + - - - - + - - - - + - - - - - - + - - + - - - - + - - - - - - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - + - - - - - - - - + - - + + - - + + - + - - - - + + + + + - + - - - - - - - - - - - - + - + - - - - + + + + + - - - - - - - + - - - - - - + - - - - - - + - - + + - - - - - - + + - - - - - - - - + - - - - - + - - + - - - - - - - - - - - + - - - - - + - - - - - - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - - - + + - - - - - - - - + + - - - - - - + - - - + - - + - - - - - - - - - - - - - - - - - + - + - - - - - - - - - + - - + - - - - - - + - - - - - - + - - - - - - + - - - - - - - - + + - - - - - - + + - - - - - - - - - - + - - - + - - - - - - + - - - - - - - + - - + - - - - - + - + - - - - - - + - - - - - - - - + - - - - + + - - - - - - - - - + - + - + - + - - + - + - + - + - - - + - - - - + - + - + - - + + + - + - + - - + + + - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - - - - - - + + - - - - + + - - + - - - - - - + - + - - - - - - - + - - - - - - + - - - - + - - + - - - - - - + - - + - - - - + - - - - + - - - - + - - - - - - + - - - - + - - + - - - - + - - - + - - - - - + - - - - - + - - - + - - + + - - - - + - - - + - - + - - + + + - + - + - - + + + - + + - - + - - + + + - - + + + - - - + - - - - - - + - - + - - - - + - - - - + - - - - + + - - - - - - + + - + - - + - + + + - + - - - - - - - - + - - - + - - - - - - + - - + - - - - - - - - - + - - - - - + + + - - + - + - - + - - - + - - - - + + + - - - - + - - + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - + - - + - - + - - + + + - - + - - - - - + + + - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + - - - - - - - - - - + - - + - - - - - - - - + - + + + + + + - - - - + - - - + + - - + - + - + + + + - - + + + + + - + + - - + + + + + - - + - + + + - - + + + + + - + + - + + + + + + - - + + + - + + + + - - + + + - - + + + + + - - - - - - + + + + + - - + + + + + - + + - - + + + + - + + + + + + - + + + + - + + + + - - + + + + + + + - - + + + - + + + + + + - + + - + + - - + + + - + + + + - - + + + - - + + + + + - + + + + + + - - - - - + - - - - - - + - - - - + - - - - - + - + - - - - - - + - - - - - + - - - - - + - - - - - - - - - - + - - + - - + - - + - - - + - + - - - - + - - - + - - - + - - - - + - - + - - + - - + - - - - - - - - + - - + - - + - - - - - - + - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + - - - + - - + + - - + + - - - + - - + - - + + + - - - + - - + - - - - - - + - + - - - - - + - - + - - - - - - - - + - - - - - - - - + - - - - - - - - + + - - - - - - - + - + - + - - + + + - + - - + - - - - - - - - - + - - - - - - + + + - - - - - + + - - - - - + - - + - - - - + + - - - - - - + + - - - - + - - + + + - - + - - - + - - - - + - - - - + - - - - + - - + - - - - + - - - - + - - + - - + + - - + + - - + - - + + + - + + - - - - + - - - - + - - + + + - - + - - + - - - - + + + + + + - - + + + + - - - - + + + - - - + - - - - + - - + + + - + + - - - - - + - + - - + - - + - - - + - + - - + - - + - - - + - + - - + - - - - - - + + - - - - - - + + - - - - - + - - + - - - - - + - + - + - - - - - - - - - - + + - - - - - - - - + - - - - - + - - + - - - - - - - - - - - - + - + - + + - - - - - - - - - - + + - - - - - - + + - - - - - + - - + - - - - - - - + - - + - - - - - - - - - - - - - - - - - - + - + - - - - - + + + - - - - - - - + + + + - + + + + - + - - + + + - - + + - - - + + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - + - - - - - + - - - - - + + + - + - - - - - - - - - + - - + + + + - - - - - - - - - - - - - - - - - - - + + + - - - - + + + - - + + + + + - + + + + - + + + + - - + + + + + - - + + + - + + + + + + - - - + - - - - + - - - + + - - - - - - - - - - + + + - - + + - - - - + + - - - - + + - - + + + - - + + + + + + + - - - + + - - + + + - + - - - - + + - + - - + + + + + + + + + + + - - - + - + + + - - - + - - - - + + + - + + - - + + + - + + + + + + + + - + + + - - + - + - - - - - + - - + - - + + + - - - - - - - - + + - - + + + - - - - + - - + - - - - - + - + - - + - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - - - - + - + - - + - - + - - + - - + - - + - - - + - + - - + - - - + - - - - - + - + - + - - - + + + + - - - - - - + + + + - + + + + - - + + + - - - - - - - + + + - - - + + + + + + + + + - - - + - + - - + + + + + - - + - + - + - + + + + - - + - + - - - - + - + - - + + + - + + + + + + + + - + + + + + + - + + + + + + - + + + + - + + + + - + + + + - - - - - - + + + - - - - - - + - + - + + + + + + - + + + + - - + + + + - - + - + - - - - - - + + + - - + + + + + + + - - - - + - + - + + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - + - + + + - - - - + + + - - - - + - + - + + + - + + + + + + + + - - - - + - + - + + - - - - - - - - - - + - + - - + + - - - - - + + + - - + + + + + - + + - - + + + + + - - + - + - + - + - + + + + + + - + + - - - - - - - - - - - - - - - - - + - + - - - + + + + + - - + + + - + - - - + + + - + + + + + - - + + + + + - + - - - + - + + + + + - - + + + + + + + + + - + - - - + - + - - + + + + + - + - - - + + + - + + + + + - - + + + + + - + - - - + - + - - + + + + + - - - - - + - + + + - - - - - - - + - - + + + - + + + + - + + - - - - - + - - - - - - - - - + + + + - - - - - + + + - - - - - - + + + + - - - - - - - - - + - + - - + + + + - - - - + - - - - - + - - - - + - - - - + - - - - - - - - - - + - - + + - - - - - + + + - - - + + + - - + - + + + - - + + + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - + + + + + + + + + - + + - + - - + + + - - + - - + - - - + + + - + + + + - + + - + + - - - - + + + + + + + + + + + + + - - - + + + + - - - - - - + + + - + + - + + - + + - + + - + + - - - - - - - - - - - - - - - - - - - + - + + - - + - - - - - - - - + - - - - - - - - - - + - - - - + - - - - + - - + - + - - + - - - - - - + - - - - - + - - - - - - + - + + - - + - + - - - - + + + - + + - - + - + - - - - + + - + - + - - - + + + - + + - - - - - - - + + + + + - - - - + - - - + - - + - - - - - - - - + - + - - + + + - - + - - - - + - - + + - + + - + + - - - + - + + - - - - - - - + + - + + - - - - + + - - - + - - + - - - - - + - - + + + - - + - - + - + - + + - - + + + - + - - + + + + + + + - + - - + + + + + + + - - - - - - - - - - - - + + - + - - - - - - - - - - + - - - + + - + + + - - - - - - - - - - - - - - - - + - + + + - - - + - - - - - - - - + + + - - + + + - - - + + + + + - - - + - + - - + - - + + + + + - - + - - - + + + + + - + - - + + + + + - + - + - + + + + - - + + + - + + + + - + + + + - + + - - - - - - - + - + + + - - - - + + + + + - - + + + + + - - + - + - - + + + + + - - - - - - + - - - - + - - + + + - - - - - - - - - + + - + - - + + + + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + + + - + + + + + - - - - - + - - - + - + - - - + + - - - - - + - - - - - + - - - - + - - + - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + - - - - - - - - + + - - - - - + - - - - - - - - - - - - - - + - - - + - - + + + - - + - - + - + - - - - + + - - - - + + + + - - + - - + + + + + + - + + + + - - + + + + + - - + - + - + + + - - - - - - - - + - + - + - + - - + - + + + - - - - - - - + + + + + + + - + + + + + + + - + + + + + + - - - - + + + + + + + - + + + + - - - - - + + + + + + + - + - + - + - + - - + - + - + + + - - - - + - - + + + - - - - + - + - - + - - - - - - + - - + - + - - - - - - - - + - + - - - - - - + - - + - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + - + + + - - - - - + - - - + + + - + + + + + - - - + - - - + - + + + + + + + - + + + - + + + + - - + + + + - - + + + - - - - + - + - - + + + - + + + + - - + + + - - + + + - - + + + - + + - - - - - - - - - - - - + - + - - + - + - - + - - - - - - - - - - - + - + + + - - - - - - - + - - + + + - + - - + - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - + - - + - - - - - - - - + - - - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - + - - - - - - - - + - - - - + - - - - + - - + - - - - + - - + - + + + + - - - + - - - - - - - - - - - + - + + + - - - - - - - - - - - - - + + + - - + + - - - - - - - - - + + + + - - + + + + - - - - - + + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - + + + + - - + + - + + - + - + - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + - + + - + - - - + + - - - - + + + + - + + - + + + + + + - - - - - + + + - - + - + + + - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + - + - + - - - - - - - - + - + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + - - + - - - - - + - + + + - - + - + - - + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + - + + + - + + + + + + + + - - - + - - + + + + + + + - + + + + + + + - - - - + - - - - - - - - + - - + - - - - - - - - + - - - - - - - + - - + + + + + + + - - - - - - - - - - - - + - - - - - - + - - - - + + + + + + + + + + - - - - + - - + + + + - - - + + + + + - - + + + + - - - - + - + - + + + + - + - - + + + + + + + + + + + + + + + + + + - - - - - - - - + - - + - - + - + + + - + - - - + - + - - + + + - - - - - - + - + - + + - - - - + - - + + + - - - - - + - + - + - + - - - + - + - - - + - + - - + - - - + - + - - - + - + + + + + - + - - - + - + + + + + - - - + - - - + - + - - - - - - - + - + - - - + - + + - - - - - - - - - - - - - - - + - + - - + + + - + + - - + + + + + + + + + + + + + - - + - + + + + + + + + + + + + + - - + - + - + - - + - + - + - + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + - - + - + + + + + + + + + + + + + - - - + + + - + - + + - - - + + + + - - - - - - - - - - - - - - + - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - + - + + + - - - - - - + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + - - + + + + + + + - - - - - - - - - - - - - - - + - - + + + + + + + - - + + + + + + + + + - - - - + - + - + + + + - + - - - - - - - - + - - + - + - - - - - - + - + - + + + + - + - + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - + - + - - + + - - + - + + + - - - - - - - - - - - + - + + + - + - - + - + - + - + - - + - - - - - + - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + - - + - - + - - - + - + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + - - + - + + - - - + + + - - - - - + - - + - - - + - - - - + - - - - - - + - + + - + + - - + - + + - + + - - + - - + - - - + - - + - - - - + - - - - + + - - - - - - - - - + - + - - - - - - - - - - + - + - - - - - - + - + - + + + + - + - - + + + + - + - - + + - - - - + - + - - - + - - + + + + + - - + - - - - + - - + + - - - - + + + + + - - + + - - - - + + + + - + + + + + + - + + - - - - - - - - + - - - - - - + - + - - - - - - - - - - - - + + + - - - - - - - - - - + - + + + - + + - + + - + + + + + + + + + - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - + + + + + + + - - - - + - - + - - + + + + + + + + + - - + + + + + + + + + + + + + + + + - - + + - - + - - + + - + - + - + - - - - + - + - - + - - - - - - + - + + - - + + + + + + + + + - - + - + - + - + + + + + + + - - + + + + + - - + + + + + + + + + + + - - + + + + + + + - - + + + + + + + - - - - - + - - - - - + + + - - - - - - - - - - - - - - + + + - - + - + - + - + + + + + + + - - - - - + - - + - - + - - + - - + - - + - - - - + + + + + + - - - - - - - + - - + + + + + - - + + + + + + + + + + + + + - - + + + - - - + - - - - + + + - - + + - - - - + + + - - + + + + + - - - + + + + + + + + + - + + + + + + + - + + + - + + - - - + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + - - - - - - + - - + - - - - - - - + - + + + + + + + + + - - + + + + + + - + + + + + + - - - - - - - - - - - - - + - - + - + - + - + - - - - - - - - + - - - - - - - - - - - + - - + - - - - + - - - - - - - + - + - + - + + + + + + + - + + + - - + - - - - - + - + - - - - - - + - - - - - - + - + - - - - + - + - + + - - - - + - - - - + - + - + - + - + - + + + + + + + + - - + + + + + + + - + + + + - + + - - + - - + - + + + + + - - + - + + + + + + + - - + - + - + + + - - + - + + + + + - - + - + - + - + - - + - + - + - + - + - + + + + + + + - - + - + + + + + + + + + + + + + + + - - + - + + + - - + - + - + - + + + + + + + + + - + + + + + + + + - + + - - + + + - - + + + + + + + + + - - + - + + + + + - + + + + - - + - + - - + - - - - + + + - + + - - - - + + + - + + + + - - - + + + + + + + - + - + - + - + - - - + - + + + + + - + + + + + + + + + - + + + + + + - - - + + + + + + + - - - + + + + + - + + + + + - - + + + + + + + + + + + - + - + - - + + + + + + + + + + + + + + + + + - - + - + - + + + - - + - + - + + + + + + + - - + - + - + - + + + + + - + + + + + + + + + + + + + + - + + + + + + + + + + - - + - + + + + + + + + + + + + + + + + + + + + + - - - + - - - + + + - + - - + - - - - + - - - - - - - - - + - - - - - - - - + + + + + - - + + + + + - + + + + + + + + + - - - - + + + - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - + + - - - - + - - - - - - - - - - - - - - - - - + - - + - + - - + - + - + - - - + - + - + + - - + - + + + + + - - - - - - + - - - - - - + + + - + + + - - + + + + - - - - - - - - - - + + + + + + + - + + + - - - + - - - - - - - - - - - + - + - + - - - - - - - - - + + - + - - - - - - + + + + + + + + - + - - - - - - - + + - - + + + - - - - - - + - - - - - - + - + - - + + + + + - + + + + + + + + - + + + + - + + - + + + + - + + - - - + - - - - - - - - - - + - - - - + + - - - + - + - + - - - - + - + - - - + - + - - + + - - - - - - - + - - + + + + + + + + + + + + + - + - + - + - + - - + + + + + + + + - - - - + + + - - + + + + + + - - - + - + + - - - + - + - + - + + + - - + + + - - - - + - + + - - + - + - - - - - - - + + + - - + + + + + + + + + + + - - - + + + + + - - - + + + - - + + + - - - + - + - + - - + - + - - + - + - - + + + + + + + + + + + - + + + + + + + + - - + - + - - - - - - + - + - - + - + + + + + + - + + - - + - + + + + + - - - - - - + - + - - + + + + + + + - - - - - - + - + - - + - + - - - - - - - - - - + - - + + + - - + + + + + + + + + - - + + + + + - - - - + - + - + - - + - - + + + + + + + - + + + + - - + - + + + + + - + + + + - - + - + - + - + - - + + + - - + - + - + + - - - - - - - - - - - - - - - + - - + - - - - + - - - - + + - - - - - - - - - - - - - + - + + + + + + + + + + - + + - - + - + + + + + + + + - - + + + + + - - - - - - - + - - - - - - - - - - - - + - - - - - - - + - + - + - - + - - - + - + - - + + + - - - - + - + - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - + + - + - - - - - - - + + + - + + + - - + - - - - - - - - + + + + + + + + + - - + - + + + + + + + + + - - - - - - + + + + + + + - - - - - + - - - - + - - - - - + - - + - + + + - - - - - - - + - - - - - + - - - + - - - + - + - - - - - - - - - - + - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + - - - - - - - - + - - + - + - + + + + + - - + + + + + + + - - + + + + + + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + + + + - + + + + + + - + + - + + + + + + - - - + - - + + + + + + + - - + - - + - - + - - + - - + - - + - - - - + - - + - - - - - + + + - - - - - + - - + - + - + + - - + - - + - + - + + + - - + - - + - - - - + - - - - - - - - - - + - - + - + - + + - + + - - + - - + - - - - - + + - - + - + + - - + - - - - - - - - + - - - - - - - + + + + + + + + + + + + + + + + + + + - - - + - - - - - - - - - - - - - - - - + + - + - - + - - - - + - - + - - + - - + - - + - - + + - - - - + - - - - + - - + - - + - - - - - - - - - - - - + - - + + + + + - - + - - - + - - - - + - - - - - + - + - - + + - - - - - - - + + + - - + - - - + - + - + - - + + + + + + + - - + + + - - + + + + + - - + - - + - + - - + + + - - + + + + + + + + + - - + + + + + + + - - + + + + + - - + - + + + + + - - + + + + + - - - - - - - - - + - + - - + - + + + + + + + + + - - - - - - + + - - - - - - - + - - - + + - - + + + - - - - + - + - - + + + + + - - + + + - - + + + + + + + - - + + + + + + + + + + + - - + - + - - - - + + + - + + + + + + + + - - - + + - + + - + + + + - - + + + + - + + - + - - - - - + - - + - - - + - - - - - + + + + + + + + - + - - - - - - - + - - + - - - - - - - - + + - - - - - - - - + - + - - + + + - - - - - - + + + + + - - + + + + + - - + + + + + + + - - - - - - - - - - - - + - + - + + + + + + + + + + + - + + + + + - + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - + + + + - - - - + - - - - + - + + + + + + + + + - - - - - - - + - + - - - - - - - + + + + - + - - + - - + + + + + - - - - - - - + - + - - + + + - - - - - + - - - - - + - - - - - - - - + - - + + + - + - - + - + + + - - - - + + + + - - - + + + - - - - + - + - - + - - - - - + - - - - - - + + - - - - - - + + + + - - - - - - - - - + - - - - - - - + + + + - + - - - - - + + - + - - - + - + - + - + + + + + - - + - + - + - + - + + + - + + - - - + + + + - + + + - + + + + + - - + - - + - - - + + + - - + + + + + + + - - + - - - - - - - - - - - + - - - - + - + + + - - + + + + + + + - - - - + - - + - + + + + + + + - + + + + - - - + + + - - + + + - - + - - - - - + + + + + + + + + - - + + + - - - + + + + - - - - - - + - + - + + + - - + - - + + + - - - - - - - - - - + + + + + - + + + + + + + + + + + + + + + + + - + - + - + - - + - - + - - + + + - - - - - - - + - + - - + + + + + + + - - + - - - - - - - - - - - - - - + - + + + - + + + + + + + + - + + - + + + + + + + + - + + + + - - - - - - - - - - - - - + - + - - + + + - - + - - + - - + - - + - + - + - + - - + - - + - - - - - - - - - + - + - - - - - + - + - + + + + + - - - - - - + - - + + + + + - - - + + + - + + - - - - + - - - - - - - + - + - - - - - + - - + + - - - + + + - + - + + + - + + + + + - - + - + - - - + - - + - - - + - - - - - + - - - - - + - - - + - - - + - - + - - - - + - + - + + + + + + + + + + - + + + + - + + + + + + - - + - + + + - - - - + - - - - - + + + + + + + + + - + - + + + + + + + + - + + - + + - - + - - + + + + + + + - - - - - + + + - - + + + - - - + + + + + + - + + - - - - + - + - - + - - - - + + + + + + - - - - - - - - + + + - - - + - - + - - - + - + - - + - + - - + + - - + - - - - + + + + + + - - - - - + - + - - + + + + + - + - + - - + + + + + + - - - - - + - + - - + + + - - + + + - - - - + - - + - - - + + + - - - - - - - + - - - - - - - + - - - + - + - - - - - - + - + + + + + + - - - - - - - - - - - + + + - - - - - + - - + + + + + + + + + + - + + - - - + + + - - - - + + - - - - - - - + - - - - + - - + + + - - + + + - - + + + + + - - + + + - - - + + + + + - + + + - - + + + + + - - + + + - - + - - - + - + - - - - - - - - - - + - - - - - - - - + - - - + + + - + - - + - - - - + - - - - - - - - + - - + - + + - - + - + - + + + + + - + + + + - + - + - - + - - - - - + - - - - - - - - - - - - - - - - - + - - + - + + + - - + - - - - + - - + + + - - - + - - - - + - - + + + - - + + + - - + - - + - - + + + - - + + + + - - - - + - - + - - + + + - - + - - + + + - - + + + + + + + + + - - + + + + + + + + + + + - - + + + + + - - + + + + + + + + + - - - - - - + + + + + + + + + + + - - + + + - - + - - + - - + - - + - - + - - + - - + - - - - - - - - - - - - - - + - - + - - + - - + + + - - - - - - - - - - - - - - - - + - - + - - + + + + + + + + + + + - - + - - - - - - + - - - - - + - - - + - - + + + - + - + + - + + + + - - + + + - - - - - + + + - - + + + + + + + + + - + - - + - - - - + - - - - - + + + - - + + + + + - - - - - + - - - - - - + - + + - + + - + + + + - - + - - - + - - + - + - + - - + + + - - - + - + + + + + - - - + + - - + + - + + - - + + - + + + + + + - + + - + + - + + - - + - + + + + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + - + + - + - - - - - - + - + - + - - + - - - + - - - + - - + - + + - - + - + + - - + - + - - - - - - + - - - - + - - - - + - - + - + - - + - + - - + - + + + + + - + + - + + + + + + + + + + + + + + + + - + - - - - - - + - - - - + + - + - - + + + - - - + - + - + - + - + - - + + + - - - + - - + + + + + - - + - - + - - + + + - + - - - - + + - + - - + - + - - + - - + - - + - - - - - + + - - - - - + + + + + + + - + - - + - - + - + - - + - + - - + - + + + + + + + - - + - + - + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + - - - + - - - - - - - - + - - + - - - - + - - - - - - - - - - + - + + + - - - - - + + + - - + + + - - + + + + - - - - + + + - - - + + + - + - - - - - - + - - - - - + - - + - - - - - - - - - + - - + - - - - - - - - - + - + + + + + + - - - + - - - + + - + + + + + + - + + - + + - + + + + - + - + + + + - + - - + + + + + + + - - - - + - - + + + + + - - + - - + - - + + + - - - - - - - + - - - - + - - + + + - - + + + - - + - - + - - - + - + - + - - + + + + + + + - - - - - + - + - + - - + + + - - - - + - + + - - + - - + - + + + + + - - - - - - - + + + - - + + + - - + + + + - - - - + - - + + + + + + - - - - + - - + + + + + + - - + + - - + + + - - + - - + - - + + + - - + + + - + + + - + - - - - - - - - + - + - - - + - - - - + + - - - - + + + + + + + - + + + + + - - - - - - - - - - - - + - + - - + - + - - - - + + + + + + + + - - + + - - + - - + + - + + + + - - - - - - + + + + - - + + + - + + + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - + - + + + + - - - - - - - + + + + + + + - + - + + + + + - - - - - - - - + - - - - + + - - + + + + - + - - + + + + - - - - - - - - - - - + + - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + - - + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - - - - - - - - - - - - - + - - - - - - - + + + + - + + - + + + + - + + - - + - - + + + - - + - - + - - + - - + - - - - - - - + - - - - - - - - - + - - - - - - + - - - - + - - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - + - + - - + - - + - - + - - + - - + - - + - - - + - + - - + - - + - - + - - - - - - + - - - - - + - - - + - - - - + - - - - + - - - - + - - - - - - + - - - - + - - + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + - + - + - - + + + + - + - - - + - - - - + + + + + - - + - - + + - - - - + + + + + + + + + - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - + - - - + + - - - - - - + - - - - + - - - - + - - - - - - - - - + + - - + + + - - - - - - - - - - + - + + - - - - - - - - - - - - - - - - - + - - + + + - + - - - - - + - + + - - + - + - - + + + + + + + - - + - - - - - + + + - - + - + + + - - + + + + + - - + + + + + + + - - + + + + + + + - - + + + - - + - - + + + - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - + - - - - - - + - - - - - - + - - - - - - - - - - + - - - - - - - - + - - + - - + - - + - - + - - - - + - - + - - + - - + - - + - - + - - + - - - - - - - + - - - - + - - - - - - - - - - + - - - + - + + + + + + + + + + + + + - - + - - - + + + + + + + + + + + + + + - - - - + + + + - + + - + + + + - - - - - - - - - - - - + - - - - - - - + + - + + + + + + - + - - + - - + + + - + + + + + + - + + + + + - + + + + + + - - - - + + - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + - - + + - - + - + - + - - - - - - - - - - - + - - - - - - - - - - - - - + + - - + + + - - + + + - - - - - - - + - - - + + - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + - + - - - - + + + + + - + + + + + + - + + + + - + + + + - + + + + - - - - + - + - + + + + - - - + - + + + + - + + - + + - + + + + + + + + + + + + - - - + + - + + - - - + + - - - + + - + + - + + - - - - - - + - - + - - + - - + - - - - + - - + - - - - - - - + + + + - - - + - - - + + - - - - + - + - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - + + + - - - - - + - - - - - - - - - + - + - - + + + + - + + + + + - + + - - + + + - - + + + + + + + + + + + - + + + + - - - - - - + - - + + - - - - - - + + + + + - - + + + + + + + - - + + + + + + + + + + + - + + + + + + + + + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - + - - - + + + - - + - - - - - - + - - - - - - - - + - - - - - - + - - + - - + - - + - - + - - + - - + + - - - - - - - - - - + + + - - - - + + + + + + + + + + + - + + + + - - - - - - - + - - + - + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - - - + - - - - - - + - - - - + - - - - + - + + + + + + + + + + + + + + - - - - - - - + + - + - + - - - + - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + + - - - - - - - + - - - - + - - + + + - - - - + + + + + + + - - - - + - - + - - + + + - - - - + - - - - + - - - - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - + + + - - - - - + - - - - - - - - - - - - + + + - - - - - - - + - - - + - + - - + + + - - - - + - - - - + - - - - - - - + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - + - - - - + - - - - - - - - - - - - - - - - - + - + - - + - - + - + + - - + - + + - - + - - - - - - + - - + - - + - - + - + + - - - - - - - - - - - - - - - - - - - - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + + + - - + + - - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - - - + - - - - - - - - - - - - - - + - - - - + - + + + + - - - - - - - - - - + - - - - + + + + + - - + + + - - + + + + + + + + + - - + + + + + + + - - + + + + + + + + + + + + + + + + - - - - + - - + - - + - - + - - + - - + - - + + - - - - + - - + - - + - - - - + - - - - + + + + - - - - + + + + + + - - + + - - + + + - - + + + - - + + + - - + + + + + - - + + + - - + - - - - - - - - - - - - - - - - + + + + + + + + + + + - - + + + - - + + + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + + + - - - - + - - - - + + + - - + + + - - + + + - - + + + + + + + + - - - - + + + - - + + + - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + - - + + + - - + + + - - + + + - - + - - + + + + + - - + + + - - - - - + + + - + + + + + - + + + + - + + - - + - + + - - + - - - - + - - + - - - - + - - + - + + + + + - - - + + + + + + + + + + + - - + + + + + + + + + - - + + + + + + + - - + + + + + - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + - - + + + - - - - - - - - + - - + - - + - - + - - - + + - - - - - - - - + - - - - - + + + + + + + + - - - - + - - - - - + + + + + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + - - + + + + + - - - - + - + - - - - - - + + - - - - + + - - - + + - - - + + - - - + + - - - - - - - - - - - - + + - - + + + - - + - - - - + - - - - + - - - - - + + - - - + + - - - + + + - + - + - - + + + - + - - + + + + + - + + + + - - - - + + + + + - - - - - + + + + - + + - + + - - - - + - - - - + - - - - - - + - - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - - - - + - + - - - - + - - - + - + - - - - + - - - + - + - - - + - - + - - + + + + + + + + + + + + + + + - - + - - - - + - - - - + - - - + - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - + - - - - + - - + - + - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + - + - + - - - - - - + + + + + + - + + + + + + - + + + + - - + + - + + - + + - + + + + - + + - - - + + - - - - - + + + + + + + + + + + + + + + + - - + + - + + - + - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - + - + + + - - - - - - - - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + - - - - - - - - + + + + + - - - + + + - - + + + - + - - + - + - + - + + - - + - + + - + + + + - + + - + + + + + + - + + + + + + - - - + - - + - - - - + - - + - - + - - + - - + - - + - - + - - + - - - - + - - - - + + - - - - - + - - - - + - + - - - - - + - - + + + - - - + - - + - + - - + - - - - - - - - - + + - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - + + - - - - + - + + + - - - + + + + + + - - - - + - - - - - - - + + + - - - - - - - + - - + - - - - + + + + + + + + - - - + + - - - - - - - - - - + + + - + - - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + - - + - + - - - + + + + + - - + + + + + + + - - + + + + + - - + + + + + - - - + + + + + + + + + + + + + - - - + + + + + + + + + - - + + + + + - + + + + - + + + - + + + + + + - - + - + - - - - - + - - + - - - - - - - - + - - + - - + + + - - + + + + + - - + + + - - + - - + - - + + + + + - - + + + - - + + + + + + + + + + + + + - - + + + + + + + + + - - + + + + + + + + + - - + + + + + + + + + + + + + - - - + + + - - - - - + + + - - + + + - - + + + - - - + + + - + + + - - + + + - - + + + - - + + + - - + + + - - + + + + + - - - + + + + + - + + + - - + + + - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - + + + + - - - + + + + + + + + - - - - + + + + + + + + - - - - - - + + - + - - - + - + - - + + + + + + - + - - - - + - + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + - - - - + - - + + + - - + + + - - - - - - + - - - - - - - + + - + + + + - + + + + - + + + + - + + - + + + + - - + + + - + + + + + + - - + + + - + + - - - + + - - - - - + + - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - + + - - - - + + - - - + + + + + + + + - - - - + + + - - - + + + + + + + - + - - - - + + + - - - - + - - - - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - + - - - - - - + - - - - - - - - - + - - + - + - - + - - - - - + - + - - - - - - - - - - - + - - + - - - - - + - - - - - - + - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - + - + - - + - - - - - - + - - - - + - - - - - - - - + - - - - - - + - - - - - - + - - - - + - - - - + - - - - - - - - + - - - - - - - - - - + - - - - + - - - - + - - + - - + - - - - - - + - - - - - - - - + - - - - - - + - - - - - - + - - - - - - - - + + - - - - - - - + - - + + + + + - - - - - - - - - + - + - - - - - - - - + - + - + - - - - + - + - - + - + + + + + - + + - - + - + - + + + + + + + - - - - - - - + - - + - + - - + - + - - - - + - + - - + - + + + - - - - + - - - - - - + - + - - - - - - - - - + - + - - + - - - - - - - - - - - - - - + - - - - + - + - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - + - - - - - - - - + - - - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - + - - - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - + - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - + - - + - - + - - + + + - - + - - + + + - - + + + + + - - + + + - - + + + + + - - + + + + + - - - - - - - - - - - + - - - - + + + + + + + - - + - - + - - + + - - + + - - + - - + - - + + + + + - - - - - - - + - - - - + + + + + - - + + + + + - - + + + - - + + + - - + - - + + - - - - + - - + + + + + - - + + + - - + + + + + - - + + + + + + + - - + + + - - + - - + + + - - + - - + + + - - + + + - - + + + - - + - - + + + - - + + + - - + - - + - - + - - + - - + - - + + + - - + + + + + - - + + + + + - - + + + + + - - + + + - - + + + + - - - - + + + + + - - + + + - - + - - + + - - - - + + + - - - - + + - - - - + - - + - - + - - + - - + - - + + + - - + + + + + - + - - + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + + + + - - - - + + + - - + + - - - - + + + - - + + - - - + - - - - + - - + - - + - - + + + - - + + + - + - - - - + + + + + + + - - + + + + + + + + + + + - - + + + + + + + + + + + - - + + + + + + + - - + + + + + + + + + + + - - + + + + + + + - - + + + - - + - - + + - - + + + + - - + - - + + + - - + + + - - + + + - - + + + + + - - + + + - - + + + + + + + - - + - - + - - + - - + - - + - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + - - + + + - - + - - + - - + - - + - - + - - + - - - + - + + + - - + - - - - - + - + - - + - + + + + + - - - - - - + - - - - - - - - + - - - - - - + - + - - - - - - + - - - - - - - - - - - + - - - - + - - - - - - - - + - - - - - - - - + - - - - - - + - - - - + - - - - - - - - + - - - - - - + - - - - + - - - - - - + - + - - - + + + + - - + - - - - - + - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - - - - - - - + - - - - - - + - - - - - - + - + - - - - - - + - - - - - - - - + - - - - + - + - - - - - - + - + - - - - - - + - - - - - - - - + - - - - - - + - - + - + - + + + + - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - + - - + + + + + + - - - + + - + + + - + + - + - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + - - - - + - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + - - - - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - + - + - + + + + + + - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - + - - - - - - - + - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + - - - - - - + - - + + - - + - - + - - + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + + + - + - - - - + + + - - + + + + + - - + + + + + - - + + + - - + + + + + - - + + + - - + + + - - + - - + + + + + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + + + - - + - - + + + - - + - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + - + - - + + + - - - - + - + - - + - + - - + - + - + + + - - + - + + + + + - - - + - - - - - - + - - - - + - - - - - - + - - + - + + + + + - - + - + + + + + - - + - + + + + + - - - - + - + - + - + - - + - + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + - + + + + + + - - + + - - + - + + + + + + + + + + + + + - - + - - - - - - + - + - + - - + - + - - + - - - - - - - - - - + - - + - + - - + + + - - - - + - - - - + - - + - + + + - - + - + - + + + - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + - + - + - - - - + - + - - + - + - - + - + - - + - + - - + - - + - + - + + + + + + + - - + - + + + + + + + + + - - + - + - - + - + - + + - - - - - - - - - - - + - - - - - - + - - - - - - + - - - - + + + - - - - - - + - - - - + - - - @@ -85849,7 +87961,7 @@ - - - - - + - - - + - - + - + + + - - + - - - + - - + - + - - + @@ -85957,7 +88065,7 @@ - - + - - - - - - - - + - - - - - - + - - - - - - - - - - - - - + - + - - + - - + + + - - - - + - - - - - - + - + + - + - - + - + + + - - + - - - + - - + - + - - + + + + + + + - - + - + + + - - + - + + + + - - - - - - + - - - - - - + - - - - - - + - - + - + - + - + + + - - + - + + + + + + + - - + + + + + - - - - - - + - + - + - - + - + - + - - - - - - - - - + - - - - + - - + + + - - - - - - - - - - - + + - - - + + + + + + + + + - - + - + + + + + - + + @@ -86553,7 +88658,7 @@ - - - - + - + - - - - - - + - + - - + - + - + - - + - + - + - - + - + - + - + - - - - - - + - - + - + - + - - - - - - - - + - - + - + - + - - + - + - + + + - - + - - - + - - + - + + + - - + - + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + - - - + - - - - + - - - - - - + - - + - - + - - + - - + - - - - + - - + - - - - + - - - - + - - - - + - - - - - - - - - - - - - - + - - - - + - - - - + - - - - + - - - - - - - - - - + - - - - + - - - - + - - - - - - + - - - - - - - + - + - - - - - + - + - - - - + - - - - - - - - + - - - - - - - + - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - + - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - + - + - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - + - + - - + - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + - + - - + - - - - - - - - + - - - - + - - - - - - + - - - - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - + - + - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - - - - - - - - - - - - - - - - + - + - - + - - - - - - - - - - - - - - - - - + - - + - - - - - - - + - - + - - - - - - - - - - - - + - - + + + + + + + - + - - + + + + - - - - - - + + + + + - - + + + + + + + - + + + + + + + + - - + + + + + - - + + + - - + - + + + - - + + + - - + + + + + + + - - + + + + + + + - - + - + + + - - + + + - - + + + + + + + - - + - + + + - - + + + - - + - - - + + + - - + - + - - + - - - + - - - + - + - - + - - - + + + - - + - + - - - + - - + - - - + - - + - + + + - - + + + - - + + + + + - - + + + - - + + + + + - - + + + - - + + + + + + + - - + - + + + - - + + + + + + + - - + + + + + - - + + + + + + + - - + + + + + - - + + + + + + + - + + + + + + - + + + + + + + + - + + + + + + - - + + + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - - - + - - - - + - - - - + - - + - + + + - - + - + - - + - + + + - - + - + - - + - + - - + - + + + - - + - + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + + + - - + - + + + - - + - + + + - - + + + + + + + - - + + + + + - - + + + + + + + - + + + + + + - - + - + + + + + - - - - - - - + - - - - + - - + - - - - + - - + - - - + - + - - - - - - + - - + - - + - - - - + - - - + - + - - - + - - - - - + - - - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - + - - - - + - - + - - + - - - - + - - - - - - + - - - - - - - - + - - + - - + - - + - - + - - + - - + - - + - - - - - - + - - + - - + - - + - - + - - - + - + - - + - - - - - - - - - - - - - - - + - - + - - + - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + - + - - + + + - + - + - - + - + - - + - + - + + + + + + + + - - + - - + + + + + + + - - - - + - + + - - + + + + + - - + - + - - + - + - + - - + - + - + + + - + + + + + - - + - - + + + + + - - + - + - - + - + - + - - + - + - - + + + - - + - + - - + + + + + + + + + - - + + + - - - - + - - + + + + + + + + + - - + - + - - + - + + + + + + + + + + + - - + + + - - + - - + - + + + + + + + - - + - + - - - + + + - + + + - - - + + + + + - + - - + + + - + - + - - + + + + + + + - + + + + + + + + - + + + + + + + + + + - - - + + + + + + + + + + + + + - + - + + + + + + + + + - - - - - + + + + + + - + + + + - - + - + + + - - - + + + + - - + - - - - + - + - + - - - - + - + - - - - - - - + + + + + + + - - + + + + + - - + + + + + + + - - - + + + + + - + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + - - - + + + + + + + + + - + - + + + - + + + + + + + + - + + + + + + - + + + + + + + + - + + + + + + - + + + + + + + + - + + + + + + - + + + + + + + + - + + + + + + - - + + + + + + + - - - + + + + + + + - + - + - - - - + + + + + - + + + + + - - - + + - + + - + + + + - + + + + + + - + + + + - - - - + - + - + - - - - + - + - + - + - - - - - - - - - + + + + + + + - + - + + + + - + + - - + + + + + + - + + + + + + + + - + + + + + + - - + + + + + + + - - + + + - - - - + - - + - + + + + + + + - - + - + - + - - + - + - + - + + + + + - - + - + - + - + - + + + - + + + + - + + + + + + - + + + + - - + - - + + + - - + - + + + + + - - - - - - - + + + + + + + + + - - + + + + + - - + + + + + + + - - - + + + + + + + - + + + + + + + + + - + + + + + - - + + + + + - - - + + + + + + + + + + + + + + + - - - - - - + - - + + + + + - + - - + + + + + + + + + + + + + - + + + + + + - - + + + + + + + - - + + + + + - - - - + - + + + - - + - + + + + + - - - - - + + + + + + + - + - + - + - + + + + + + + + - - - + - - + - - + - - + - - + - - + - - + - - + - - + - - + - - - - + - - - - + - - + - - + - - + - - + - - - - + - - + - - + - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - + - - - - + - - + - - - - + - - - - + - - + - - - - + - - + - - - - - - + - - + - - + - - - - + - - - - + - - + - - - - + - - - - - - + - - + - - - - + - - - - - - + - - - - + - - - - + - - + - - - - + - - + - - + - - - - + - - - - + - - + - - - - - - + - - - - - - + - - - - + - - + - - - - + - - - - + - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - + - - - - - - + - - - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - - - + - - - - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + - - - - - - - - - - - + - - - - + + + - - + + + + - - + + + - - - + - - - - - - - - + - - - + + + - + - + - - + - - - - - + + + - + - + - - + + + + + + + + + - + - - + + + - + + - + + - - + - - + - + + + + + - + + + + + + + + - - - - - - + + + - + + - + - + + + - - + - + + + - + - - - - - - - - + + + + + + + + + - - + + + + + + + + - - - - + + - + - - - - + + - + + + + + + - - + + + - - + - - + + - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + - - - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + - + + + + - + + - - + - + + + + + + + - - + - + + + + + + + + + - - - - - - + + + + + + + - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + - + + + + + + + + - - + + + + + - - + + + + + + + + + + + - + + + + + + + + + + + + - - + + + + + + + + + + + + + + + - - + - + + + + + + + + + + + + + - - + + + + + + + + + + + - - + - + + + + + + + + + + + - - + + + + + + + + + - - - - - - + + + + + - - - - + - - + + + + + + + + + + + + + + + + + + + + + - - + - + + + + + + + + + + + - - + - + + + + + + + - - + - + + + + + - - - - - - - + + + + + + + + + + - + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + - + + + - - - - - - - + - + + - + + - - + - - - + + + + - - - - - - + - + - - + + + + + + + - - - - - - - - - - - + + - - - + - + + - - - - - + - - - - - - - - - - - - - - + - - - - + - + - - - - - - + - - + - - + - - + - - - - + - - - - + - - - - + - - - - + - - - - + - - - - - + - - - - - - - + - - - - - - - - + - - - - - - + - - + - - + - - + - - + - - + - - + - - + + + + + + + + + + - - + + - - - - + - + - + - - - + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + - - - - - + - - + - - + + + + + + + + + - - + + + - - - - - - - - - - - - - - - + - - - - + - - + - - + - - - - - - + - - - - - + - + - - - - - - + - - - - + - - - - + - - + - - + - - + - - + - - - - + + - - - - - - - - + - - - - + - - - - + - - - + + + + + - + - + + + + + + + + + + + + + + + + + + - + + - - - - - - - - + - + + + - - + - + + + + - + - - + + + - - + + + + - + + - + + - + + - + + + + - + + - + + + + + + + + - + + - + + - - + - + + + + + + + + + + - + - + + + + + + + + + + - + - + + + + + + + + + + + + + + - + - - + + + - + + - + + - + + - + + - + + - - + - + + + + + - + + - + + + + + + - + + - + + + + - - - - + - + + + + - + + + + - - + + + - - + + + - + + + + - - + + + - - + - + - - + - + - + + + + - - + + + - + + + + - - + - + - - + + + - - + + + - - + + + - + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - + + - - + + + - - + + + - - + + + - - + - - + - - + - + + - + + + - + + + + + + + + + + + + - + + + - - - - - - - + + + + + + + + + + - + + + - + + + + + + + + + + + + - + + + - - + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + - - - - - - - - - - + - + - - - + - - - - - - - - + + + + - - - - + - - - - + - - + + - - - - - - - - + - - - - - + - + - - - - - - - - + - - - - - - + - - - - - + + - - - - - - - - - + - - - - - - + - - - - + + - - - - + + + + - + - - - + - - - + - - + + + + + + + - - - - - - - - - - - + - - + - - - - + - - - + - - - - - - + - - - - + - - - - + - - + + - - - - + - - - - - - - - + - - - - - - - - - - - - + - - - - + + - - - - + + - - - - + - - - + - + - - - - - - - - - + - + - - - + - - - - - + + - - - - - - + - - + - - + - - - - + + - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + - - - - + - - + + - - + + - - + + + + - + - + - + - - + - - - + - - - - + - - - - + - - - - + - - - - - - - + - + - - - - + - - + + - - - - - - + + + + - + - - - - - + - - - - + - - - - - - + - - - - - - - - + - - - - - - + + - - - - - - + + + + + + + + + - - - - - - - - + + + + - + - + - + - - - - + - - + + + + + - - - - - - + - - - - - - - - - - - - - - - - - + - - + + + + - - - - - - - + - - - - - + + + + - - - + - + + - - + + + + + + + + + + + - - - - - - - - - - - - - + - - + - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - - - - + - - - - + - - - - - - - - - - + + - - + + - - + - - + + + + - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - + - + - + - - - - - + + + + + + + - - + + + - - - - - - - - - - - - - + - - - - - - - - - - - + - + - - - - - - - - - + - + + - - - - - - - + - + + - - - - - - - + - - - + + + + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - + + + + + - - - - - - - - - - + - - + + + + - + - - + + + + - + - - + + + + - + - - - - - - - - + - + + - - + + + + + - - + + - - - - - - - - - - - + - - - - - - - - - - - - - - - + - - - - - - - - + - - - - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - + + + + + + - - + + - - - - - - - - - - - - - + - - - + + - - - - - + - - - - - - - - - - - - - - - - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - + - + - - - + + + + - - - - - - - + + - - + + + - - - - - - - - - - + + - + - + - + + + + + - - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -109559,7 +124663,7 @@ type="android.telephony.NeighboringCellInfo" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -109599,6 +124703,39 @@ visibility="public" > + + + + + + @@ -109630,7 +124767,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -109819,6 +124956,23 @@ + + + + + + + + @@ -111729,6 +126883,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -116958,6 +132395,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -127706,6 +143310,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -135939,6 +151656,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -138826,6 +154831,21 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -139020,6 +155168,19 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -139946,7 +156230,7 @@ value="40" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -139957,7 +156241,7 @@ value="16" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -140307,7 +156591,7 @@ value="2" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -140318,7 +156602,7 @@ value="1" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -140493,6 +156777,32 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + @@ -149722,7 +166246,7 @@ value="1" static="true" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -150104,7 +166628,7 @@ type="int" transient="false" volatile="false" - value="2008" + value="2014" static="true" final="true" deprecated="not deprecated" @@ -150166,6 +166690,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -157449,7 +174266,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157462,7 +174279,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157473,7 +174290,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157484,7 +174301,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157495,7 +174312,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157506,7 +174323,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157519,7 +174336,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157532,7 +174349,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157545,7 +174362,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157558,7 +174375,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157591,14 +174408,14 @@ abstract="false" static="false" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157617,7 +174434,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157628,7 +174445,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157639,7 +174456,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157650,7 +174467,7 @@ synchronized="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157660,14 +174477,14 @@ abstract="false" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157678,7 +174495,7 @@ synchronized="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157691,7 +174508,7 @@ synchronized="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157702,7 +174519,7 @@ synchronized="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157713,7 +174530,7 @@ synchronized="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -157728,13 +174545,51 @@ synchronized="true" static="false" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > + + + + + + + + + + + + + + @@ -158044,7 +174899,7 @@ abstract="false" static="false" final="true" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -158092,7 +174947,7 @@ synchronized="true" static="true" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -158105,7 +174960,7 @@ synchronized="true" static="true" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -158118,7 +174973,7 @@ synchronized="true" static="true" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > @@ -158131,11 +174986,32 @@ synchronized="true" static="true" final="false" - deprecated="not deprecated" + deprecated="deprecated" visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -158916,6 +176038,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159522,6 +176774,170 @@ > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -160289,7 +177716,7 @@ visibility="public" > - + + + + + + @@ -161058,6 +178500,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - + - + - + - + - + - + - + + + @@ -269590,6 +287554,8 @@ deprecated="not deprecated" visibility="public" > + + - - @@ -269610,8 +287574,6 @@ deprecated="not deprecated" visibility="public" > - - - + - + @@ -271041,7 +289003,7 @@ deprecated="not deprecated" visibility="public" > - + @@ -271101,7 +289063,20 @@ deprecated="not deprecated" visibility="public" > - + + + + + - + - + @@ -271191,7 +289166,7 @@ deprecated="not deprecated" visibility="public" > - + @@ -271207,6 +289182,19 @@ visibility="public" > + + + + - + - + - + - + - - - + - + + + - - - + - + + + - + - + - + @@ -272454,12 +290442,6 @@ deprecated="not deprecated" visibility="public" > - - - - - - + + + + + + - + - + @@ -273572,7 +291560,7 @@ deprecated="not deprecated" visibility="public" > - + - + @@ -273777,7 +291765,7 @@ deprecated="not deprecated" visibility="public" > - + - + @@ -273846,7 +291834,7 @@ deprecated="not deprecated" visibility="public" > - + - + @@ -275295,9 +293283,9 @@ deprecated="not deprecated" visibility="public" > - + - + - + + + + + + + + + + + + @@ -284906,7 +302937,11 @@ deprecated="not deprecated" visibility="public" > - + + + + + - - - - - + @@ -303751,7 +321782,7 @@ return="javax.net.ServerSocketFactory" abstract="false" native="false" - synchronized="false" + synchronized="true" static="true" final="false" deprecated="not deprecated" @@ -304541,7 +322572,7 @@ return="javax.net.SocketFactory" abstract="false" native="false" - synchronized="false" + synchronized="true" static="true" final="false" deprecated="not deprecated" diff --git a/camera/libcameraservice/Android.mk b/camera/libcameraservice/Android.mk index 96cc512dc5d63e3a7c247c60a984840ee7e36b43..ecaebffc2347b07af6518873b4a61e32a621e2fb 100644 --- a/camera/libcameraservice/Android.mk +++ b/camera/libcameraservice/Android.mk @@ -25,6 +25,10 @@ LOCAL_SRC_FILES:= \ LOCAL_MODULE:= libcamerastub +ifeq ($(TARGET_SIMULATOR),true) +LOCAL_CFLAGS += -DSINGLE_PROCESS +endif + LOCAL_SHARED_LIBRARIES:= libui include $(BUILD_STATIC_LIBRARY) @@ -42,12 +46,17 @@ LOCAL_SRC_FILES:= \ LOCAL_SHARED_LIBRARIES:= \ libui \ libutils \ + libbinder \ libcutils \ libmedia LOCAL_MODULE:= libcameraservice -LOCAL_CFLAGS+=-DLOG_TAG=\"CameraService\" +LOCAL_CFLAGS += -DLOG_TAG=\"CameraService\" + +ifeq ($(TARGET_SIMULATOR),true) +LOCAL_CFLAGS += -DSINGLE_PROCESS +endif ifeq ($(USE_CAMERA_STUB), true) LOCAL_STATIC_LIBRARIES += libcamerastub diff --git a/camera/libcameraservice/CameraHardwareStub.cpp b/camera/libcameraservice/CameraHardwareStub.cpp index a7af57c426ee8f6f8efdf5ad92e32d6729491999..8ad1f692bd91e2bfccda488b1f2d90958e53258a 100644 --- a/camera/libcameraservice/CameraHardwareStub.cpp +++ b/camera/libcameraservice/CameraHardwareStub.cpp @@ -33,13 +33,11 @@ CameraHardwareStub::CameraHardwareStub() mRawHeap(0), mFakeCamera(0), mPreviewFrameSize(0), - mRawPictureCallback(0), - mJpegPictureCallback(0), - mPictureCallbackCookie(0), - mPreviewCallback(0), - mPreviewCallbackCookie(0), - mAutoFocusCallback(0), - mAutoFocusCallbackCookie(0), + mNotifyCb(0), + mDataCb(0), + mDataCbTimestamp(0), + mCallbackCookie(0), + mMsgEnabled(0), mCurrentPreviewFrame(0) { initDefaultParameters(); @@ -112,6 +110,36 @@ sp CameraHardwareStub::getRawHeap() const return mRawHeap; } +void CameraHardwareStub::setCallbacks(notify_callback notify_cb, + data_callback data_cb, + data_callback_timestamp data_cb_timestamp, + void* user) +{ + Mutex::Autolock lock(mLock); + mNotifyCb = notify_cb; + mDataCb = data_cb; + mDataCbTimestamp = data_cb_timestamp; + mCallbackCookie = user; +} + +void CameraHardwareStub::enableMsgType(int32_t msgType) +{ + Mutex::Autolock lock(mLock); + mMsgEnabled |= msgType; +} + +void CameraHardwareStub::disableMsgType(int32_t msgType) +{ + Mutex::Autolock lock(mLock); + mMsgEnabled &= ~msgType; +} + +bool CameraHardwareStub::msgTypeEnabled(int32_t msgType) +{ + Mutex::Autolock lock(mLock); + return (mMsgEnabled & msgType); +} + // --------------------------------------------------------------------------- int CameraHardwareStub::previewThread() @@ -150,7 +178,8 @@ int CameraHardwareStub::previewThread() //LOGV("previewThread: generated frame to buffer %d", mCurrentPreviewFrame); // Notify the client of a new frame. - mPreviewCallback(buffer, mPreviewCallbackCookie); + if (mMsgEnabled & CAMERA_MSG_PREVIEW_FRAME) + mDataCb(CAMERA_MSG_PREVIEW_FRAME, buffer, mCallbackCookie); // Advance the buffer pointer. mCurrentPreviewFrame = (mCurrentPreviewFrame + 1) % kBufferCount; @@ -162,15 +191,13 @@ int CameraHardwareStub::previewThread() return NO_ERROR; } -status_t CameraHardwareStub::startPreview(preview_callback cb, void* user) +status_t CameraHardwareStub::startPreview() { Mutex::Autolock lock(mLock); if (mPreviewThread != 0) { // already running return INVALID_OPERATION; } - mPreviewCallback = cb; - mPreviewCallbackCookie = user; mPreviewThread = new PreviewThread(this); return NO_ERROR; } @@ -197,7 +224,7 @@ bool CameraHardwareStub::previewEnabled() { return mPreviewThread != 0; } -status_t CameraHardwareStub::startRecording(recording_callback cb, void* user) +status_t CameraHardwareStub::startRecording() { return UNKNOWN_ERROR; } @@ -225,30 +252,24 @@ int CameraHardwareStub::beginAutoFocusThread(void *cookie) int CameraHardwareStub::autoFocusThread() { - if (mAutoFocusCallback != NULL) { - mAutoFocusCallback(true, mAutoFocusCallbackCookie); - mAutoFocusCallback = NULL; - return NO_ERROR; - } - return UNKNOWN_ERROR; + if (mMsgEnabled & CAMERA_MSG_FOCUS) + mNotifyCb(CAMERA_MSG_FOCUS, true, 0, mCallbackCookie); + return NO_ERROR; } -status_t CameraHardwareStub::autoFocus(autofocus_callback af_cb, - void *user) +status_t CameraHardwareStub::autoFocus() { Mutex::Autolock lock(mLock); - - if (mAutoFocusCallback != NULL) { - return mAutoFocusCallback == af_cb ? NO_ERROR : INVALID_OPERATION; - } - - mAutoFocusCallback = af_cb; - mAutoFocusCallbackCookie = user; if (createThread(beginAutoFocusThread, this) == false) return UNKNOWN_ERROR; return NO_ERROR; } +status_t CameraHardwareStub::cancelAutoFocus() +{ + return NO_ERROR; +} + /*static*/ int CameraHardwareStub::beginPictureThread(void *cookie) { CameraHardwareStub *c = (CameraHardwareStub *)cookie; @@ -257,10 +278,10 @@ status_t CameraHardwareStub::autoFocus(autofocus_callback af_cb, int CameraHardwareStub::pictureThread() { - if (mShutterCallback) - mShutterCallback(mPictureCallbackCookie); + if (mMsgEnabled & CAMERA_MSG_SHUTTER) + mNotifyCb(CAMERA_MSG_SHUTTER, 0, 0, mCallbackCookie); - if (mRawPictureCallback) { + if (mMsgEnabled & CAMERA_MSG_RAW_IMAGE) { //FIXME: use a canned YUV image! // In the meantime just make another fake camera picture. int w, h; @@ -268,42 +289,28 @@ int CameraHardwareStub::pictureThread() sp mem = new MemoryBase(mRawHeap, 0, w * 2 * h); FakeCamera cam(w, h); cam.getNextFrameAsYuv422((uint8_t *)mRawHeap->base()); - if (mRawPictureCallback) - mRawPictureCallback(mem, mPictureCallbackCookie); + mDataCb(CAMERA_MSG_RAW_IMAGE, mem, mCallbackCookie); } - if (mJpegPictureCallback) { + if (mMsgEnabled & CAMERA_MSG_COMPRESSED_IMAGE) { sp heap = new MemoryHeapBase(kCannedJpegSize); sp mem = new MemoryBase(heap, 0, kCannedJpegSize); memcpy(heap->base(), kCannedJpeg, kCannedJpegSize); - if (mJpegPictureCallback) - mJpegPictureCallback(mem, mPictureCallbackCookie); + mDataCb(CAMERA_MSG_COMPRESSED_IMAGE, mem, mCallbackCookie); } return NO_ERROR; } -status_t CameraHardwareStub::takePicture(shutter_callback shutter_cb, - raw_callback raw_cb, - jpeg_callback jpeg_cb, - void* user) +status_t CameraHardwareStub::takePicture() { stopPreview(); - mShutterCallback = shutter_cb; - mRawPictureCallback = raw_cb; - mJpegPictureCallback = jpeg_cb; - mPictureCallbackCookie = user; if (createThread(beginPictureThread, this) == false) return -1; return NO_ERROR; } -status_t CameraHardwareStub::cancelPicture(bool cancel_shutter, - bool cancel_raw, - bool cancel_jpeg) +status_t CameraHardwareStub::cancelPicture() { - if (cancel_shutter) mShutterCallback = NULL; - if (cancel_raw) mRawPictureCallback = NULL; - if (cancel_jpeg) mJpegPictureCallback = NULL; return NO_ERROR; } @@ -361,6 +368,12 @@ CameraParameters CameraHardwareStub::getParameters() const return mParameters; } +status_t CameraHardwareStub::sendCommand(int32_t command, int32_t arg1, + int32_t arg2) +{ + return BAD_VALUE; +} + void CameraHardwareStub::release() { } diff --git a/camera/libcameraservice/CameraHardwareStub.h b/camera/libcameraservice/CameraHardwareStub.h index 0d26d47ececb3db38b855542d32ebf5a8888e346..8a670249544021794f9d528edbbbea2e7a4bd72d 100644 --- a/camera/libcameraservice/CameraHardwareStub.h +++ b/camera/libcameraservice/CameraHardwareStub.h @@ -21,8 +21,8 @@ #include "FakeCamera.h" #include #include -#include -#include +#include +#include #include namespace android { @@ -32,26 +32,33 @@ public: virtual sp getPreviewHeap() const; virtual sp getRawHeap() const; - virtual status_t startPreview(preview_callback cb, void* user); + virtual void setCallbacks(notify_callback notify_cb, + data_callback data_cb, + data_callback_timestamp data_cb_timestamp, + void* user); + + virtual void enableMsgType(int32_t msgType); + virtual void disableMsgType(int32_t msgType); + virtual bool msgTypeEnabled(int32_t msgType); + + virtual status_t startPreview(); virtual void stopPreview(); virtual bool previewEnabled(); - virtual status_t startRecording(recording_callback cb, void* user); + virtual status_t startRecording(); virtual void stopRecording(); virtual bool recordingEnabled(); virtual void releaseRecordingFrame(const sp& mem); - virtual status_t autoFocus(autofocus_callback, void *user); - virtual status_t takePicture(shutter_callback, - raw_callback, - jpeg_callback, - void* user); - virtual status_t cancelPicture(bool cancel_shutter, - bool cancel_raw, - bool cancel_jpeg); + virtual status_t autoFocus(); + virtual status_t cancelAutoFocus(); + virtual status_t takePicture(); + virtual status_t cancelPicture(); virtual status_t dump(int fd, const Vector& args) const; virtual status_t setParameters(const CameraParameters& params); virtual CameraParameters getParameters() const; + virtual status_t sendCommand(int32_t command, int32_t arg1, + int32_t arg2); virtual void release(); static sp createInstance(); @@ -67,8 +74,15 @@ private: class PreviewThread : public Thread { CameraHardwareStub* mHardware; public: - PreviewThread(CameraHardwareStub* hw) - : Thread(false), mHardware(hw) { } + PreviewThread(CameraHardwareStub* hw) : +#ifdef SINGLE_PROCESS + // In single process mode this thread needs to be a java thread, + // since we won't be calling through the binder. + Thread(true), +#else + Thread(false), +#endif + mHardware(hw) { } virtual void onFirstRef() { run("CameraPreviewThread", PRIORITY_URGENT_DISPLAY); } @@ -102,18 +116,15 @@ private: bool mPreviewRunning; int mPreviewFrameSize; - shutter_callback mShutterCallback; - raw_callback mRawPictureCallback; - jpeg_callback mJpegPictureCallback; - void *mPictureCallbackCookie; - // protected by mLock sp mPreviewThread; - preview_callback mPreviewCallback; - void *mPreviewCallbackCookie; - autofocus_callback mAutoFocusCallback; - void *mAutoFocusCallbackCookie; + notify_callback mNotifyCb; + data_callback mDataCb; + data_callback_timestamp mDataCbTimestamp; + void *mCallbackCookie; + + int32_t mMsgEnabled; // only used from PreviewThread int mCurrentPreviewFrame; diff --git a/camera/libcameraservice/CameraService.cpp b/camera/libcameraservice/CameraService.cpp index e4b6791c2bba738edc9e87e96b920c90549241a7..df59dcff0b29e915c4cbb492d79948e264c1243e 100644 --- a/camera/libcameraservice/CameraService.cpp +++ b/camera/libcameraservice/CameraService.cpp @@ -20,12 +20,12 @@ #define LOG_TAG "CameraService" #include -#include -#include +#include +#include #include #include -#include -#include +#include +#include #include #include @@ -33,7 +33,6 @@ #include "CameraService.h" #include -#include namespace android { @@ -60,6 +59,7 @@ extern "C" { #define DEBUG_DUMP_PREVIEW_FRAME_TO_FILE 0 /* n-th frame to write */ #define DEBUG_DUMP_JPEG_SNAPSHOT_TO_FILE 0 #define DEBUG_DUMP_YUV_SNAPSHOT_TO_FILE 0 +#define DEBUG_DUMP_POSTVIEW_SNAPSHOT_TO_FILE 0 #if DEBUG_DUMP_PREVIEW_FRAME_TO_FILE static int debug_frame_cnt; @@ -195,17 +195,11 @@ void CameraService::decUsers() { android_atomic_dec(&mUsers); } -static sp newMediaPlayer(const char *file) +static sp newMediaPlayer(const char *file) { sp mp = new MediaPlayer(); if (mp->setDataSource(file) == NO_ERROR) { - char value[PROPERTY_VALUE_MAX]; - property_get("ro.camera.sound.forced", value, "0"); - if (atoi(value)) { - mp->setAudioStreamType(AudioSystem::ENFORCED_AUDIBLE); - } else { - mp->setAudioStreamType(AudioSystem::SYSTEM); - } + mp->setAudioStreamType(AudioSystem::ENFORCED_AUDIBLE); mp->prepare(); } else { mp.clear(); @@ -225,8 +219,20 @@ CameraService::Client::Client(const sp& cameraService, mHardware = openCameraHardware(); mUseOverlay = mHardware->useOverlay(); + mHardware->setCallbacks(notifyCallback, + dataCallback, + dataCallbackTimestamp, + mCameraService.get()); + + // Enable zoom, error, and focus messages by default + mHardware->enableMsgType(CAMERA_MSG_ERROR | + CAMERA_MSG_ZOOM | + CAMERA_MSG_FOCUS); + mMediaPlayerClick = newMediaPlayer("/system/media/audio/ui/camera_click.ogg"); mMediaPlayerBeep = newMediaPlayer("/system/media/audio/ui/VideoRecord.ogg"); + mOverlayW = 0; + mOverlayH = 0; // Callback is disabled by default mPreviewCallbackFlag = FRAME_CALLBACK_FLAG_NOOP; @@ -261,7 +267,7 @@ status_t CameraService::Client::lock() status_t CameraService::Client::unlock() { int callingPid = getCallingPid(); - LOGD("unlock from pid %d (mClientPid %d)", callingPid, mClientPid); + LOGD("unlock from pid %d (mClientPid %d)", callingPid, mClientPid); Mutex::Autolock _l(mLock); // allow anyone to use camera status_t result = checkPid(); @@ -303,7 +309,7 @@ status_t CameraService::Client::connect(const sp& client) oldClient = mCameraClient; // did the client actually change? - if (client->asBinder() == mCameraClient->asBinder()) { + if ((mCameraClient != NULL) && (client->asBinder() == mCameraClient->asBinder())) { LOGD("Connect to the same client"); return NO_ERROR; } @@ -396,9 +402,20 @@ void CameraService::Client::disconnect() // idle state. mHardware->stopPreview(); // Cancel all picture callbacks. - mHardware->cancelPicture(true, true, true); + mHardware->disableMsgType(CAMERA_MSG_SHUTTER | + CAMERA_MSG_POSTVIEW_FRAME | + CAMERA_MSG_RAW_IMAGE | + CAMERA_MSG_COMPRESSED_IMAGE); + mHardware->cancelPicture(); + // Turn off remaining messages. + mHardware->disableMsgType(CAMERA_MSG_ALL_MSGS); // Release the hardware resources. mHardware->release(); + // Release the held overlay resources. + if (mUseOverlay) + { + mOverlayRef = 0; + } mHardware.clear(); mCameraService->removeClient(mCameraClient); @@ -420,11 +437,21 @@ status_t CameraService::Client::setPreviewDisplay(const sp& surface) result = NO_ERROR; // asBinder() is safe on NULL (returns NULL) if (surface->asBinder() != mSurface->asBinder()) { - if (mSurface != 0 && !mUseOverlay) { + if (mSurface != 0) { LOGD("clearing old preview surface %p", mSurface.get()); - mSurface->unregisterBuffers(); + if ( !mUseOverlay) + { + mSurface->unregisterBuffers(); + } + else + { + // Force the destruction of any previous overlay + sp dummy; + mHardware->setOverlay( dummy ); + } } mSurface = surface; + mOverlayRef = 0; // If preview has been already started, set overlay or register preview // buffers now. if (mHardware->previewEnabled()) { @@ -446,6 +473,13 @@ void CameraService::Client::setPreviewCallbackFlag(int callback_flag) Mutex::Autolock lock(mLock); if (checkPid() != NO_ERROR) return; mPreviewCallbackFlag = callback_flag; + + if(mUseOverlay) { + if(mPreviewCallbackFlag & FRAME_CALLBACK_FLAG_ENABLE_MASK) + mHardware->enableMsgType(CAMERA_MSG_PREVIEW_FRAME); + else + mHardware->disableMsgType(CAMERA_MSG_PREVIEW_FRAME); + } } // start preview mode @@ -504,7 +538,7 @@ status_t CameraService::Client::startRecordingMode() } // start recording mode - ret = mHardware->startRecording(recordingCallback, mCameraService.get()); + ret = mHardware->startRecording(); if (ret != NO_ERROR) { LOGE("mHardware->startRecording() failed with status %d", ret); } @@ -518,27 +552,47 @@ status_t CameraService::Client::setOverlay() CameraParameters params(mHardware->getParameters()); params.getPreviewSize(&w, &h); - const char *format = params.getPreviewFormat(); - int fmt; - if (!strcmp(format, "yuv422i")) - fmt = OVERLAY_FORMAT_YCbCr_422_I; - else if (!strcmp(format, "rgb565")) - fmt = OVERLAY_FORMAT_RGB_565; - else { - LOGE("Invalid preview format for overlays"); - return -EINVAL; + if ( w != mOverlayW || h != mOverlayH ) + { + // Force the destruction of any previous overlay + sp dummy; + mHardware->setOverlay( dummy ); + mOverlayRef = 0; } status_t ret = NO_ERROR; if (mSurface != 0) { - sp ref = mSurface->createOverlay(w, h, fmt); - ret = mHardware->setOverlay(new Overlay(ref)); + if (mOverlayRef.get() == NULL) { + + // FIXME: + // Surfaceflinger may hold onto the previous overlay reference for some + // time after we try to destroy it. retry a few times. In the future, we + // should make the destroy call block, or possibly specify that we can + // wait in the createOverlay call if the previous overlay is in the + // process of being destroyed. + for (int retry = 0; retry < 50; ++retry) { + mOverlayRef = mSurface->createOverlay(w, h, OVERLAY_FORMAT_DEFAULT); + if (mOverlayRef != NULL) break; + LOGD("Overlay create failed - retrying"); + usleep(20000); + } + if ( mOverlayRef.get() == NULL ) + { + LOGE("Overlay Creation Failed!"); + return -EINVAL; + } + ret = mHardware->setOverlay(new Overlay(mOverlayRef)); + } } else { ret = mHardware->setOverlay(NULL); } if (ret != NO_ERROR) { LOGE("mHardware->setOverlay() failed with status %d\n", ret); } + + mOverlayW = w; + mOverlayH = h; + return ret; } @@ -588,10 +642,10 @@ status_t CameraService::Client::startPreviewMode() ret = setOverlay(); } if (ret != NO_ERROR) return ret; - ret = mHardware->startPreview(NULL, mCameraService.get()); + ret = mHardware->startPreview(); } else { - ret = mHardware->startPreview(previewCallback, - mCameraService.get()); + mHardware->enableMsgType(CAMERA_MSG_PREVIEW_FRAME); + ret = mHardware->startPreview(); if (ret != NO_ERROR) return ret; // If preview display has been set, register preview buffers now. if (mSurface != 0) { @@ -606,7 +660,7 @@ status_t CameraService::Client::startPreviewMode() status_t CameraService::Client::startPreview() { LOGD("startPreview (pid %d)", getCallingPid()); - + return startCameraMode(CAMERA_PREVIEW_MODE); } @@ -618,6 +672,9 @@ status_t CameraService::Client::startRecording() mMediaPlayerBeep->seekTo(0); mMediaPlayerBeep->start(); } + + mHardware->enableMsgType(CAMERA_MSG_VIDEO_FRAME); + return startCameraMode(CAMERA_RECORDING_MODE); } @@ -626,21 +683,30 @@ void CameraService::Client::stopPreview() { LOGD("stopPreview (pid %d)", getCallingPid()); - Mutex::Autolock lock(mLock); - if (checkPid() != NO_ERROR) return; + // hold main lock during state transition + { + Mutex::Autolock lock(mLock); + if (checkPid() != NO_ERROR) return; - if (mHardware == 0) { - LOGE("mHardware is NULL, returning."); - return; - } + if (mHardware == 0) { + LOGE("mHardware is NULL, returning."); + return; + } - mHardware->stopPreview(); - LOGD("stopPreview(), hardware stopped OK"); + mHardware->stopPreview(); + mHardware->disableMsgType(CAMERA_MSG_PREVIEW_FRAME); + LOGD("stopPreview(), hardware stopped OK"); - if (mSurface != 0 && !mUseOverlay) { - mSurface->unregisterBuffers(); + if (mSurface != 0 && !mUseOverlay) { + mSurface->unregisterBuffers(); + } + } + + // hold preview buffer lock + { + Mutex::Autolock lock(mPreviewLock); + mPreviewBuffer.clear(); } - mPreviewBuffer.clear(); } // stop recording mode @@ -648,21 +714,31 @@ void CameraService::Client::stopRecording() { LOGD("stopRecording (pid %d)", getCallingPid()); - Mutex::Autolock lock(mLock); - if (checkPid() != NO_ERROR) return; + // hold main lock during state transition + { + Mutex::Autolock lock(mLock); + if (checkPid() != NO_ERROR) return; - if (mHardware == 0) { - LOGE("mHardware is NULL, returning."); - return; + if (mHardware == 0) { + LOGE("mHardware is NULL, returning."); + return; + } + + if (mMediaPlayerBeep.get() != NULL) { + mMediaPlayerBeep->seekTo(0); + mMediaPlayerBeep->start(); + } + + mHardware->stopRecording(); + mHardware->disableMsgType(CAMERA_MSG_VIDEO_FRAME); + LOGD("stopRecording(), hardware stopped OK"); } - if (mMediaPlayerBeep.get() != NULL) { - mMediaPlayerBeep->seekTo(0); - mMediaPlayerBeep->start(); + // hold preview buffer lock + { + Mutex::Autolock lock(mPreviewLock); + mPreviewBuffer.clear(); } - mHardware->stopRecording(); - LOGD("stopRecording(), hardware stopped OK"); - mPreviewBuffer.clear(); } // release a recording frame @@ -749,69 +825,25 @@ static void dump_to_file(const char *fname, } #endif -// preview callback - frame buffer update -void CameraService::Client::previewCallback(const sp& mem, void* user) +status_t CameraService::Client::autoFocus() { - LOGV("previewCallback()"); - sp client = getClientFromCookie(user); - if (client == 0) { - return; - } - -#if DEBUG_HEAP_LEAKS && 0 // debugging - if (gWeakHeap == NULL) { - ssize_t offset; - size_t size; - sp heap = mem->getMemory(&offset, &size); - if (gWeakHeap != heap) { - LOGD("SETTING PREVIEW HEAP"); - heap->trackMe(true, true); - gWeakHeap = heap; - } - } -#endif - -#if DEBUG_DUMP_PREVIEW_FRAME_TO_FILE - { - if (debug_frame_cnt++ == DEBUG_DUMP_PREVIEW_FRAME_TO_FILE) { - ssize_t offset; - size_t size; - sp heap = mem->getMemory(&offset, &size); - dump_to_file("/data/preview.yuv", - (uint8_t *)heap->base() + offset, size); - } - } -#endif + LOGD("autoFocus (pid %d)", getCallingPid()); - // The strong pointer guarantees the client will exist, but no lock is held. - client->postPreviewFrame(mem); + Mutex::Autolock lock(mLock); + status_t result = checkPid(); + if (result != NO_ERROR) return result; -#if DEBUG_CLIENT_REFERENCES - //**** if the client's refcount is 1, then we are about to destroy it here, - // which is bad--print all refcounts. - if (client->getStrongCount() == 1) { - LOGE("++++++++++++++++ (PREVIEW) THIS WILL CAUSE A LOCKUP!"); - client->printRefs(); + if (mHardware == 0) { + LOGE("mHardware is NULL, returning."); + return INVALID_OPERATION; } -#endif -} -// recording callback -void CameraService::Client::recordingCallback(nsecs_t timestamp, const sp& mem, void* user) -{ - LOGV("recordingCallback"); - sp client = getClientFromCookie(user); - if (client == 0) { - return; - } - // The strong pointer guarantees the client will exist, but no lock is held. - client->postRecordingFrame(timestamp, mem); + return mHardware->autoFocus(); } -// take a picture - image is returned in callback -status_t CameraService::Client::autoFocus() +status_t CameraService::Client::cancelAutoFocus() { - LOGD("autoFocus (pid %d)", getCallingPid()); + LOGD("cancelAutoFocus (pid %d)", getCallingPid()); Mutex::Autolock lock(mLock); status_t result = checkPid(); @@ -822,8 +854,7 @@ status_t CameraService::Client::autoFocus() return INVALID_OPERATION; } - return mHardware->autoFocus(autoFocusCallback, - mCameraService.get()); + return mHardware->cancelAutoFocus(); } // take a picture - image is returned in callback @@ -840,65 +871,155 @@ status_t CameraService::Client::takePicture() return INVALID_OPERATION; } - return mHardware->takePicture(shutterCallback, - yuvPictureCallback, - jpegPictureCallback, - mCameraService.get()); + mHardware->enableMsgType(CAMERA_MSG_SHUTTER | + CAMERA_MSG_POSTVIEW_FRAME | + CAMERA_MSG_RAW_IMAGE | + CAMERA_MSG_COMPRESSED_IMAGE); + + return mHardware->takePicture(); } -// picture callback - snapshot taken -void CameraService::Client::shutterCallback(void *user) +// snapshot taken +void CameraService::Client::handleShutter( + image_rect_type *size // The width and height of yuv picture for + // registerBuffer. If this is NULL, use the picture + // size from parameters. +) { - sp client = getClientFromCookie(user); - if (client == 0) { - return; - } - // Play shutter sound. - if (client->mMediaPlayerClick.get() != NULL) { - client->mMediaPlayerClick->seekTo(0); - client->mMediaPlayerClick->start(); + if (mMediaPlayerClick.get() != NULL) { + mMediaPlayerClick->seekTo(0); + mMediaPlayerClick->start(); } // Screen goes black after the buffer is unregistered. - if (client->mSurface != 0 && !client->mUseOverlay) { - client->mSurface->unregisterBuffers(); + if (mSurface != 0 && !mUseOverlay) { + mSurface->unregisterBuffers(); } - client->postShutter(); + sp c = mCameraClient; + if (c != NULL) { + c->notifyCallback(CAMERA_MSG_SHUTTER, 0, 0); + } + mHardware->disableMsgType(CAMERA_MSG_SHUTTER); // It takes some time before yuvPicture callback to be called. // Register the buffer for raw image here to reduce latency. - if (client->mSurface != 0 && !client->mUseOverlay) { + if (mSurface != 0 && !mUseOverlay) { int w, h; - CameraParameters params(client->mHardware->getParameters()); - params.getPictureSize(&w, &h); + CameraParameters params(mHardware->getParameters()); uint32_t transform = 0; if (params.getOrientation() == CameraParameters::CAMERA_ORIENTATION_PORTRAIT) { LOGV("portrait mode"); transform = ISurface::BufferHeap::ROT_90; } + + if (size == NULL) { + params.getPictureSize(&w, &h); + } else { + w = size->width; + h = size->height; + w &= ~1; + h &= ~1; + LOGD("Snapshot image width=%d, height=%d", w, h); + } ISurface::BufferHeap buffers(w, h, w, h, - PIXEL_FORMAT_YCbCr_420_SP, transform, 0, client->mHardware->getRawHeap()); + PIXEL_FORMAT_YCbCr_420_SP, transform, 0, mHardware->getRawHeap()); - client->mSurface->registerBuffers(buffers); + mSurface->registerBuffers(buffers); } } -// picture callback - raw image ready -void CameraService::Client::yuvPictureCallback(const sp& mem, - void *user) +// preview callback - frame buffer update +void CameraService::Client::handlePreviewData(const sp& mem) { - sp client = getClientFromCookie(user); - if (client == 0) { - return; + ssize_t offset; + size_t size; + sp heap = mem->getMemory(&offset, &size); + +#if DEBUG_HEAP_LEAKS && 0 // debugging + if (gWeakHeap == NULL) { + if (gWeakHeap != heap) { + LOGD("SETTING PREVIEW HEAP"); + heap->trackMe(true, true); + gWeakHeap = heap; + } + } +#endif +#if DEBUG_DUMP_PREVIEW_FRAME_TO_FILE + { + if (debug_frame_cnt++ == DEBUG_DUMP_PREVIEW_FRAME_TO_FILE) { + dump_to_file("/data/preview.yuv", + (uint8_t *)heap->base() + offset, size); + } + } +#endif + + if (!mUseOverlay) + { + Mutex::Autolock surfaceLock(mSurfaceLock); + if (mSurface != NULL) { + mSurface->postBuffer(offset); + } } - if (mem == NULL) { - client->postRaw(NULL); - client->postError(UNKNOWN_ERROR); + + // local copy of the callback flags + int flags = mPreviewCallbackFlag; + + // is callback enabled? + if (!(flags & FRAME_CALLBACK_FLAG_ENABLE_MASK)) { + // If the enable bit is off, the copy-out and one-shot bits are ignored + LOGV("frame callback is diabled"); return; } + // hold a strong pointer to the client + sp c = mCameraClient; + + // clear callback flags if no client or one-shot mode + if ((c == NULL) || (mPreviewCallbackFlag & FRAME_CALLBACK_FLAG_ONE_SHOT_MASK)) { + LOGV("Disable preview callback"); + mPreviewCallbackFlag &= ~(FRAME_CALLBACK_FLAG_ONE_SHOT_MASK | + FRAME_CALLBACK_FLAG_COPY_OUT_MASK | + FRAME_CALLBACK_FLAG_ENABLE_MASK); + // TODO: Shouldn't we use this API for non-overlay hardware as well? + if (mUseOverlay) + mHardware->disableMsgType(CAMERA_MSG_PREVIEW_FRAME); + } + + // Is the received frame copied out or not? + if (flags & FRAME_CALLBACK_FLAG_COPY_OUT_MASK) { + LOGV("frame is copied"); + copyFrameAndPostCopiedFrame(c, heap, offset, size); + } else { + LOGV("frame is forwarded"); + c->dataCallback(CAMERA_MSG_PREVIEW_FRAME, mem); + } +} + +// picture callback - postview image ready +void CameraService::Client::handlePostview(const sp& mem) +{ +#if DEBUG_DUMP_POSTVIEW_SNAPSHOT_TO_FILE // for testing pursposes only + { + ssize_t offset; + size_t size; + sp heap = mem->getMemory(&offset, &size); + dump_to_file("/data/postview.yuv", + (uint8_t *)heap->base() + offset, size); + } +#endif + + sp c = mCameraClient; + if (c != NULL) { + c->dataCallback(CAMERA_MSG_POSTVIEW_FRAME, mem); + } + mHardware->disableMsgType(CAMERA_MSG_POSTVIEW_FRAME); +} + +// picture callback - raw image ready +void CameraService::Client::handleRawPicture(const sp& mem) +{ ssize_t offset; size_t size; sp heap = mem->getMemory(&offset, &size); @@ -906,80 +1027,148 @@ void CameraService::Client::yuvPictureCallback(const sp& mem, gWeakHeap = heap; // debugging #endif - //LOGV("yuvPictureCallback(%d, %d, %p)", offset, size, user); + //LOGV("handleRawPicture(%d, %d)", offset, size); #if DEBUG_DUMP_YUV_SNAPSHOT_TO_FILE // for testing pursposes only dump_to_file("/data/photo.yuv", (uint8_t *)heap->base() + offset, size); #endif // Put the YUV version of the snapshot in the preview display. - if (client->mSurface != 0 && !client->mUseOverlay) { - client->mSurface->postBuffer(offset); + if (mSurface != 0 && !mUseOverlay) { + mSurface->postBuffer(offset); + } + + sp c = mCameraClient; + if (c != NULL) { + c->dataCallback(CAMERA_MSG_RAW_IMAGE, mem); + } + mHardware->disableMsgType(CAMERA_MSG_RAW_IMAGE); +} + +// picture callback - compressed picture ready +void CameraService::Client::handleCompressedPicture(const sp& mem) +{ +#if DEBUG_DUMP_JPEG_SNAPSHOT_TO_FILE // for testing pursposes only + { + ssize_t offset; + size_t size; + sp heap = mem->getMemory(&offset, &size); + dump_to_file("/data/photo.jpg", + (uint8_t *)heap->base() + offset, size); } +#endif - client->postRaw(mem); + sp c = mCameraClient; + if (c != NULL) { + c->dataCallback(CAMERA_MSG_COMPRESSED_IMAGE, mem); + } + mHardware->disableMsgType(CAMERA_MSG_COMPRESSED_IMAGE); +} + +void CameraService::Client::notifyCallback(int32_t msgType, int32_t ext1, int32_t ext2, void* user) +{ + LOGV("notifyCallback(%d)", msgType); + + sp client = getClientFromCookie(user); + if (client == 0) { + return; + } + + switch (msgType) { + case CAMERA_MSG_SHUTTER: + // ext1 is the dimension of the yuv picture. + client->handleShutter((image_rect_type *)ext1); + break; + default: + sp c = client->mCameraClient; + if (c != NULL) { + c->notifyCallback(msgType, ext1, ext2); + } + break; + } #if DEBUG_CLIENT_REFERENCES - //**** if the client's refcount is 1, then we are about to destroy it here, - // which is bad--print all refcounts. if (client->getStrongCount() == 1) { - LOGE("++++++++++++++++ (RAW) THIS WILL CAUSE A LOCKUP!"); + LOGE("++++++++++++++++ (NOTIFY CALLBACK) THIS WILL CAUSE A LOCKUP!"); client->printRefs(); } #endif } -// picture callback - jpeg ready -void CameraService::Client::jpegPictureCallback(const sp& mem, void *user) +void CameraService::Client::dataCallback(int32_t msgType, const sp& dataPtr, void* user) { + LOGV("dataCallback(%d)", msgType); + sp client = getClientFromCookie(user); if (client == 0) { return; } - if (mem == NULL) { - client->postJpeg(NULL); - client->postError(UNKNOWN_ERROR); + + sp c = client->mCameraClient; + if (dataPtr == NULL) { + LOGE("Null data returned in data callback"); + if (c != NULL) { + c->notifyCallback(CAMERA_MSG_ERROR, UNKNOWN_ERROR, 0); + c->dataCallback(msgType, NULL); + } return; } - /** We absolutely CANNOT call into user code with a lock held **/ - -#if DEBUG_DUMP_JPEG_SNAPSHOT_TO_FILE // for testing pursposes only - { - ssize_t offset; - size_t size; - sp heap = mem->getMemory(&offset, &size); - dump_to_file("/data/photo.jpg", - (uint8_t *)heap->base() + offset, size); + switch (msgType) { + case CAMERA_MSG_PREVIEW_FRAME: + client->handlePreviewData(dataPtr); + break; + case CAMERA_MSG_POSTVIEW_FRAME: + client->handlePostview(dataPtr); + break; + case CAMERA_MSG_RAW_IMAGE: + client->handleRawPicture(dataPtr); + break; + case CAMERA_MSG_COMPRESSED_IMAGE: + client->handleCompressedPicture(dataPtr); + break; + default: + if (c != NULL) { + c->dataCallback(msgType, dataPtr); + } + break; } -#endif - - client->postJpeg(mem); #if DEBUG_CLIENT_REFERENCES - //**** if the client's refcount is 1, then we are about to destroy it here, - // which is bad--print all refcounts. if (client->getStrongCount() == 1) { - LOGE("++++++++++++++++ (JPEG) THIS WILL CAUSE A LOCKUP!"); + LOGE("++++++++++++++++ (DATA CALLBACK) THIS WILL CAUSE A LOCKUP!"); client->printRefs(); } #endif } -void CameraService::Client::autoFocusCallback(bool focused, void *user) +void CameraService::Client::dataCallbackTimestamp(nsecs_t timestamp, int32_t msgType, + const sp& dataPtr, void* user) { - LOGV("autoFocusCallback"); + LOGV("dataCallbackTimestamp(%d)", msgType); sp client = getClientFromCookie(user); if (client == 0) { return; } + sp c = client->mCameraClient; - client->postAutoFocus(focused); + if (dataPtr == NULL) { + LOGE("Null data returned in data with timestamp callback"); + if (c != NULL) { + c->notifyCallback(CAMERA_MSG_ERROR, UNKNOWN_ERROR, 0); + c->dataCallbackTimestamp(0, msgType, NULL); + } + return; + } + + if (c != NULL) { + c->dataCallbackTimestamp(timestamp, msgType, dataPtr); + } #if DEBUG_CLIENT_REFERENCES if (client->getStrongCount() == 1) { - LOGE("++++++++++++++++ (AUTOFOCUS) THIS WILL CAUSE A LOCKUP!"); + LOGE("++++++++++++++++ (DATA CALLBACK TIMESTAMP) THIS WILL CAUSE A LOCKUP!"); client->printRefs(); } #endif @@ -1000,8 +1189,7 @@ status_t CameraService::Client::setParameters(const String8& params) } CameraParameters p(params); - mHardware->setParameters(p); - return NO_ERROR; + return mHardware->setParameters(p); } // get preview/capture parameters - key/value pairs @@ -1019,114 +1207,55 @@ String8 CameraService::Client::getParameters() const return params; } -void CameraService::Client::postAutoFocus(bool focused) +status_t CameraService::Client::sendCommand(int32_t cmd, int32_t arg1, int32_t arg2) { - LOGV("postAutoFocus"); - mCameraClient->notifyCallback(CAMERA_MSG_FOCUS, (int32_t)focused, 0); -} - -void CameraService::Client::postShutter() -{ - LOGD("postShutter"); - mCameraClient->notifyCallback(CAMERA_MSG_SHUTTER, 0, 0); -} + LOGD("sendCommand (pid %d)", getCallingPid()); + Mutex::Autolock lock(mLock); + status_t result = checkPid(); + if (result != NO_ERROR) return result; -void CameraService::Client::postRaw(const sp& mem) -{ - LOGD("postRaw"); - mCameraClient->dataCallback(CAMERA_MSG_RAW_IMAGE, mem); -} + if (mHardware == 0) { + LOGE("mHardware is NULL, returning."); + return INVALID_OPERATION; + } -void CameraService::Client::postJpeg(const sp& mem) -{ - LOGD("postJpeg"); - mCameraClient->dataCallback(CAMERA_MSG_COMPRESSED_IMAGE, mem); + return mHardware->sendCommand(cmd, arg1, arg2); } -void CameraService::Client::copyFrameAndPostCopiedFrame(sp heap, size_t offset, size_t size) +void CameraService::Client::copyFrameAndPostCopiedFrame(const sp& client, + const sp& heap, size_t offset, size_t size) { LOGV("copyFrameAndPostCopiedFrame"); // It is necessary to copy out of pmem before sending this to // the callback. For efficiency, reuse the same MemoryHeapBase // provided it's big enough. Don't allocate the memory or // perform the copy if there's no callback. - if (mPreviewBuffer == 0) { - mPreviewBuffer = new MemoryHeapBase(size, 0, NULL); - } else if (size > mPreviewBuffer->virtualSize()) { - mPreviewBuffer.clear(); - mPreviewBuffer = new MemoryHeapBase(size, 0, NULL); + + // hold the preview lock while we grab a reference to the preview buffer + sp previewBuffer; + { + Mutex::Autolock lock(mPreviewLock); + if (mPreviewBuffer == 0) { + mPreviewBuffer = new MemoryHeapBase(size, 0, NULL); + } else if (size > mPreviewBuffer->virtualSize()) { + mPreviewBuffer.clear(); + mPreviewBuffer = new MemoryHeapBase(size, 0, NULL); + } if (mPreviewBuffer == 0) { LOGE("failed to allocate space for preview buffer"); return; } + previewBuffer = mPreviewBuffer; } - memcpy(mPreviewBuffer->base(), + memcpy(previewBuffer->base(), (uint8_t *)heap->base() + offset, size); - sp frame = new MemoryBase(mPreviewBuffer, 0, size); + sp frame = new MemoryBase(previewBuffer, 0, size); if (frame == 0) { LOGE("failed to allocate space for frame callback"); return; } - mCameraClient->dataCallback(CAMERA_MSG_PREVIEW_FRAME, frame); -} - -void CameraService::Client::postRecordingFrame(nsecs_t timestamp, const sp& frame) -{ - LOGV("postRecordingFrame"); - if (frame == 0) { - LOGW("frame is a null pointer"); - return; - } - mCameraClient->dataCallbackTimestamp(timestamp, CAMERA_MSG_VIDEO_FRAME, frame); -} - -void CameraService::Client::postPreviewFrame(const sp& mem) -{ - LOGV("postPreviewFrame"); - if (mem == 0) { - LOGW("mem is a null pointer"); - return; - } - - ssize_t offset; - size_t size; - sp heap = mem->getMemory(&offset, &size); - { - Mutex::Autolock surfaceLock(mSurfaceLock); - if (mSurface != NULL) { - mSurface->postBuffer(offset); - } - } - - // Is the callback enabled or not? - if (!(mPreviewCallbackFlag & FRAME_CALLBACK_FLAG_ENABLE_MASK)) { - // If the enable bit is off, the copy-out and one-shot bits are ignored - LOGV("frame callback is diabled"); - return; - } - - // Is the received frame copied out or not? - if (mPreviewCallbackFlag & FRAME_CALLBACK_FLAG_COPY_OUT_MASK) { - LOGV("frame is copied out"); - copyFrameAndPostCopiedFrame(heap, offset, size); - } else { - LOGV("frame is directly sent out without copying"); - mCameraClient->dataCallback(CAMERA_MSG_PREVIEW_FRAME, mem); - } - - // Is this is one-shot only? - if (mPreviewCallbackFlag & FRAME_CALLBACK_FLAG_ONE_SHOT_MASK) { - LOGV("One-shot only, thus clear the bits and disable frame callback"); - mPreviewCallbackFlag &= ~(FRAME_CALLBACK_FLAG_ONE_SHOT_MASK | - FRAME_CALLBACK_FLAG_COPY_OUT_MASK | - FRAME_CALLBACK_FLAG_ENABLE_MASK); - } -} - -void CameraService::Client::postError(status_t error) -{ - mCameraClient->notifyCallback(CAMERA_MSG_ERROR, error, 0); + client->dataCallback(CAMERA_MSG_PREVIEW_FRAME, frame); } status_t CameraService::dump(int fd, const Vector& args) @@ -1160,12 +1289,6 @@ status_t CameraService::dump(int fd, const Vector& args) } -#define CHECK_INTERFACE(interface, data, reply) \ - do { if (!data.enforceInterface(interface::getInterfaceDescriptor())) { \ - LOGW("Call incorrectly routed to " #interface); \ - return PERMISSION_DENIED; \ - } } while (0) - status_t CameraService::onTransact( uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { diff --git a/camera/libcameraservice/CameraService.h b/camera/libcameraservice/CameraService.h index ea93789d14a010733a89bcb165d9bbeb403c8929..3e3e54f0a950de3fe90b31cc307e15c4fd9fc6a5 100644 --- a/camera/libcameraservice/CameraService.h +++ b/camera/libcameraservice/CameraService.h @@ -23,10 +23,9 @@ #include #include -class android::MemoryHeapBase; - namespace android { +class MemoryHeapBase; class MediaPlayer; // ---------------------------------------------------------------------------- @@ -110,6 +109,9 @@ private: // auto focus virtual status_t autoFocus(); + // cancel auto focus + virtual status_t cancelAutoFocus(); + // take a picture - returns an IMemory (ref-counted mmap) virtual status_t takePicture(); @@ -119,6 +121,9 @@ private: // get preview/capture parameters - key/value pairs virtual String8 getParameters() const; + // send command to camera driver + virtual status_t sendCommand(int32_t cmd, int32_t arg1, int32_t arg2); + // our client... const sp& getCameraClient() const { return mCameraClient; } @@ -132,22 +137,21 @@ private: status_t checkPid(); - static void recordingCallback(nsecs_t timestamp, const sp& mem, void* user); - static void previewCallback(const sp& mem, void* user); - static void shutterCallback(void *user); - static void yuvPictureCallback(const sp& mem, void* user); - static void jpegPictureCallback(const sp& mem, void* user); - static void autoFocusCallback(bool focused, void* user); + static void notifyCallback(int32_t msgType, int32_t ext1, int32_t ext2, void* user); + static void dataCallback(int32_t msgType, const sp& dataPtr, void* user); + static void dataCallbackTimestamp(nsecs_t timestamp, int32_t msgType, + const sp& dataPtr, void* user); + static sp getClientFromCookie(void* user); - void postShutter(); - void postRaw(const sp& mem); - void postJpeg(const sp& mem); - void postPreviewFrame(const sp& mem); - void postRecordingFrame(nsecs_t timestamp, const sp& frame); - void copyFrameAndPostCopiedFrame(sp heap, size_t offset, size_t size); - void postError(status_t error); - void postAutoFocus(bool focused); + void handlePreviewData(const sp&); + void handleShutter(image_rect_type *image); + void handlePostview(const sp&); + void handleRawPicture(const sp&); + void handleCompressedPicture(const sp&); + + void copyFrameAndPostCopiedFrame(const sp& client, + const sp& heap, size_t offset, size_t size); // camera operation mode enum camera_mode { @@ -177,7 +181,6 @@ private: mutable Condition mReady; sp mCameraService; sp mSurface; - sp mPreviewBuffer; int mPreviewCallbackFlag; sp mMediaPlayerClick; @@ -189,6 +192,13 @@ private: sp mHardware; pid_t mClientPid; bool mUseOverlay; + + sp mOverlayRef; + int mOverlayW; + int mOverlayH; + + mutable Mutex mPreviewLock; + sp mPreviewBuffer; }; // ---------------------------------------------------------------------------- diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java index 3782136f5c73d8a3beea8834c18e19ba6ed6f039..eca5af976d42c1ed5e0e19322bcd39dd6d8fc338 100644 --- a/cmds/am/src/com/android/commands/am/Am.java +++ b/cmds/am/src/com/android/commands/am/Am.java @@ -23,16 +23,19 @@ import android.app.IActivityManager; import android.app.IInstrumentationWatcher; import android.app.Instrumentation; import android.content.ComponentName; +import android.content.IIntentReceiver; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.AndroidException; import android.view.IWindowManager; import java.io.File; import java.io.FileNotFoundException; +import java.net.URISyntaxException; import java.util.Iterator; import java.util.Set; @@ -45,16 +48,29 @@ public class Am { private boolean mDebugOption = false; + // These are magic strings understood by the Eclipse plugin. + private static final String FATAL_ERROR_CODE = "Error type 1"; + private static final String NO_SYSTEM_ERROR_CODE = "Error type 2"; + private static final String NO_CLASS_ERROR_CODE = "Error type 3"; + /** * Command-line entry point. * * @param args The command-line arguments */ public static void main(String[] args) { - (new Am()).run(args); + try { + (new Am()).run(args); + } catch (IllegalArgumentException e) { + showUsage(); + System.err.println("Error: " + e.getMessage()); + } catch (Exception e) { + System.err.println(e.toString()); + System.exit(1); + } } - private void run(String[] args) { + private void run(String[] args) throws Exception { if (args.length < 1) { showUsage(); return; @@ -62,16 +78,14 @@ public class Am { mAm = ActivityManagerNative.getDefault(); if (mAm == null) { - System.err.println("Error type 2"); - System.err.println("Error: Unable to connect to activity manager; is the system running?"); - showUsage(); - return; + System.err.println(NO_SYSTEM_ERROR_CODE); + throw new AndroidException("Can't connect to activity manager; is the system running?"); } mArgs = args; - String op = args[0]; mNextArg = 1; + if (op.equals("start")) { runStart(); } else if (op.equals("instrument")) { @@ -81,13 +95,11 @@ public class Am { } else if (op.equals("profile")) { runProfile(); } else { - System.err.println("Error: Unknown command: " + op); - showUsage(); - return; + throw new IllegalArgumentException("Unknown command: " + op); } } - private Intent makeIntent() { + private Intent makeIntent() throws URISyntaxException { Intent intent = new Intent(); boolean hasIntentInfo = false; @@ -95,186 +107,146 @@ public class Am { Uri data = null; String type = null; - try { - String opt; - while ((opt=nextOption()) != null) { - if (opt.equals("-a")) { - intent.setAction(nextOptionData()); - hasIntentInfo = true; - } else if (opt.equals("-d")) { - data = Uri.parse(nextOptionData()); - hasIntentInfo = true; - } else if (opt.equals("-t")) { - type = nextOptionData(); - hasIntentInfo = true; - } else if (opt.equals("-c")) { - intent.addCategory(nextOptionData()); - hasIntentInfo = true; - } else if (opt.equals("-e") || opt.equals("--es")) { - String key = nextOptionData(); - String value = nextOptionData(); - intent.putExtra(key, value); - hasIntentInfo = true; - } else if (opt.equals("--ei")) { - String key = nextOptionData(); - String value = nextOptionData(); - intent.putExtra(key, Integer.valueOf(value)); - hasIntentInfo = true; - } else if (opt.equals("--ez")) { - String key = nextOptionData(); - String value = nextOptionData(); - intent.putExtra(key, Boolean.valueOf(value)); - hasIntentInfo = true; - } else if (opt.equals("-n")) { - String str = nextOptionData(); - ComponentName cn = ComponentName.unflattenFromString(str); - if (cn == null) { - System.err.println("Error: Bad component name: " + str); - showUsage(); - return null; - } - intent.setComponent(cn); - hasIntentInfo = true; - } else if (opt.equals("-f")) { - String str = nextOptionData(); - intent.setFlags(Integer.decode(str).intValue()); - } else if (opt.equals("-D")) { - mDebugOption = true; - } else { - System.err.println("Error: Unknown option: " + opt); - showUsage(); - return null; - } + String opt; + while ((opt=nextOption()) != null) { + if (opt.equals("-a")) { + intent.setAction(nextArgRequired()); + hasIntentInfo = true; + } else if (opt.equals("-d")) { + data = Uri.parse(nextArgRequired()); + hasIntentInfo = true; + } else if (opt.equals("-t")) { + type = nextArgRequired(); + hasIntentInfo = true; + } else if (opt.equals("-c")) { + intent.addCategory(nextArgRequired()); + hasIntentInfo = true; + } else if (opt.equals("-e") || opt.equals("--es")) { + String key = nextArgRequired(); + String value = nextArgRequired(); + intent.putExtra(key, value); + hasIntentInfo = true; + } else if (opt.equals("--ei")) { + String key = nextArgRequired(); + String value = nextArgRequired(); + intent.putExtra(key, Integer.valueOf(value)); + hasIntentInfo = true; + } else if (opt.equals("--ez")) { + String key = nextArgRequired(); + String value = nextArgRequired(); + intent.putExtra(key, Boolean.valueOf(value)); + hasIntentInfo = true; + } else if (opt.equals("-n")) { + String str = nextArgRequired(); + ComponentName cn = ComponentName.unflattenFromString(str); + if (cn == null) throw new IllegalArgumentException("Bad component name: " + str); + intent.setComponent(cn); + hasIntentInfo = true; + } else if (opt.equals("-f")) { + String str = nextArgRequired(); + intent.setFlags(Integer.decode(str).intValue()); + } else if (opt.equals("-D")) { + mDebugOption = true; + } else { + System.err.println("Error: Unknown option: " + opt); + showUsage(); + return null; } - } catch (RuntimeException ex) { - System.err.println("Error: " + ex.toString()); - showUsage(); - return null; } intent.setDataAndType(data, type); String uri = nextArg(); if (uri != null) { - try { - Intent oldIntent = intent; - try { - intent = Intent.getIntent(uri); - } catch (java.net.URISyntaxException ex) { - System.err.println("Bad URI: " + uri); - showUsage(); - return null; - } - if (oldIntent.getAction() != null) { - intent.setAction(oldIntent.getAction()); - } - if (oldIntent.getData() != null || oldIntent.getType() != null) { - intent.setDataAndType(oldIntent.getData(), oldIntent.getType()); - } - Set cats = oldIntent.getCategories(); - if (cats != null) { - Iterator it = cats.iterator(); - while (it.hasNext()) { - intent.addCategory((String)it.next()); - } + Intent oldIntent = intent; + intent = Intent.getIntent(uri); + if (oldIntent.getAction() != null) { + intent.setAction(oldIntent.getAction()); + } + if (oldIntent.getData() != null || oldIntent.getType() != null) { + intent.setDataAndType(oldIntent.getData(), oldIntent.getType()); + } + Set cats = oldIntent.getCategories(); + if (cats != null) { + Iterator it = cats.iterator(); + while (it.hasNext()) { + intent.addCategory((String)it.next()); } - } catch (RuntimeException ex) { - System.err.println("Error creating from URI: " + ex.toString()); - showUsage(); - return null; } - } else if (!hasIntentInfo) { - System.err.println("Error: No intent supplied"); - showUsage(); - return null; + hasIntentInfo = true; } + if (!hasIntentInfo) throw new IllegalArgumentException("No intent supplied"); return intent; } - private void runStart() { + private void runStart() throws Exception { Intent intent = makeIntent(); - - if (intent != null) { - System.out.println("Starting: " + intent); - try { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // XXX should do something to determine the MIME type. - int res = mAm.startActivity(null, intent, intent.getType(), - null, 0, null, null, 0, false, mDebugOption); - switch (res) { - case IActivityManager.START_SUCCESS: - break; - case IActivityManager.START_SWITCHES_CANCELED: - System.err.println( - "Warning: Activity not started because the " - + " current activity is being kept for the user."); - break; - case IActivityManager.START_DELIVERED_TO_TOP: - System.err.println( - "Warning: Activity not started, intent has " - + "been delivered to currently running " - + "top-most instance."); - break; - case IActivityManager.START_RETURN_INTENT_TO_CALLER: - System.err.println( - "Warning: Activity not started because intent " - + "should be handled by the caller"); - break; - case IActivityManager.START_TASK_TO_FRONT: - System.err.println( - "Warning: Activity not started, its current " - + "task has been brought to the front"); - break; - case IActivityManager.START_INTENT_NOT_RESOLVED: - System.err.println( - "Error: Activity not started, unable to " - + "resolve " + intent.toString()); - break; - case IActivityManager.START_CLASS_NOT_FOUND: - System.err.println("Error type 3"); - System.err.println("Error: Activity class " + - intent.getComponent().toShortString() - + " does not exist."); - break; - case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: - System.err.println( - "Error: Activity not started, you requested to " - + "both forward and receive its result"); - break; - case IActivityManager.START_PERMISSION_DENIED: - System.err.println( - "Error: Activity not started, you do not " - + "have permission to access it."); - break; - default: - System.err.println( - "Error: Activity not started, unknown error " - + "code " + res); - break; - } - } catch (RemoteException e) { - System.err.println("Error type 1"); + System.out.println("Starting: " + intent); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // XXX should do something to determine the MIME type. + int res = mAm.startActivity(null, intent, intent.getType(), + null, 0, null, null, 0, false, mDebugOption); + switch (res) { + case IActivityManager.START_SUCCESS: + break; + case IActivityManager.START_SWITCHES_CANCELED: + System.err.println( + "Warning: Activity not started because the " + + " current activity is being kept for the user."); + break; + case IActivityManager.START_DELIVERED_TO_TOP: + System.err.println( + "Warning: Activity not started, intent has " + + "been delivered to currently running " + + "top-most instance."); + break; + case IActivityManager.START_RETURN_INTENT_TO_CALLER: + System.err.println( + "Warning: Activity not started because intent " + + "should be handled by the caller"); + break; + case IActivityManager.START_TASK_TO_FRONT: + System.err.println( + "Warning: Activity not started, its current " + + "task has been brought to the front"); + break; + case IActivityManager.START_INTENT_NOT_RESOLVED: System.err.println( "Error: Activity not started, unable to " - + "call on to activity manager service"); - } + + "resolve " + intent.toString()); + break; + case IActivityManager.START_CLASS_NOT_FOUND: + System.err.println(NO_CLASS_ERROR_CODE); + System.err.println("Error: Activity class " + + intent.getComponent().toShortString() + + " does not exist."); + break; + case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: + System.err.println( + "Error: Activity not started, you requested to " + + "both forward and receive its result"); + break; + case IActivityManager.START_PERMISSION_DENIED: + System.err.println( + "Error: Activity not started, you do not " + + "have permission to access it."); + break; + default: + System.err.println( + "Error: Activity not started, unknown error code " + res); + break; } } - private void sendBroadcast() { + private void sendBroadcast() throws Exception { Intent intent = makeIntent(); - - if (intent != null) { - System.out.println("Broadcasting: " + intent); - try { - mAm.broadcastIntent(null, intent, null, null, 0, null, null, - null, true, false); - } catch (RemoteException e) { - } - } + IntentReceiver receiver = new IntentReceiver(); + System.out.println("Broadcasting: " + intent); + mAm.broadcastIntent(null, intent, null, receiver, 0, null, null, null, true, false); + receiver.waitForFinish(); } - private void runInstrument() { + private void runInstrument() throws Exception { String profileFile = null; boolean wait = false; boolean rawMode = false; @@ -283,46 +255,30 @@ public class Am { String argKey = null, argValue = null; IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); - try { - String opt; - while ((opt=nextOption()) != null) { - if (opt.equals("-p")) { - profileFile = nextOptionData(); - } else if (opt.equals("-w")) { - wait = true; - } else if (opt.equals("-r")) { - rawMode = true; - } else if (opt.equals("-e")) { - argKey = nextOptionData(); - argValue = nextOptionData(); - args.putString(argKey, argValue); - } else if (opt.equals("--no_window_animation")) { - no_window_animation = true; - } else { - System.err.println("Error: Unknown option: " + opt); - showUsage(); - return; - } + String opt; + while ((opt=nextOption()) != null) { + if (opt.equals("-p")) { + profileFile = nextArgRequired(); + } else if (opt.equals("-w")) { + wait = true; + } else if (opt.equals("-r")) { + rawMode = true; + } else if (opt.equals("-e")) { + argKey = nextArgRequired(); + argValue = nextArgRequired(); + args.putString(argKey, argValue); + } else if (opt.equals("--no_window_animation")) { + no_window_animation = true; + } else { + System.err.println("Error: Unknown option: " + opt); + showUsage(); + return; } - } catch (RuntimeException ex) { - System.err.println("Error: " + ex.toString()); - showUsage(); - return; } - String cnArg = nextArg(); - if (cnArg == null) { - System.err.println("Error: No instrumentation component supplied"); - showUsage(); - return; - } - + String cnArg = nextArgRequired(); ComponentName cn = ComponentName.unflattenFromString(cnArg); - if (cn == null) { - System.err.println("Error: Bad component name: " + cnArg); - showUsage(); - return; - } + if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg); InstrumentationWatcher watcher = null; if (wait) { @@ -331,22 +287,13 @@ public class Am { } float[] oldAnims = null; if (no_window_animation) { - try { - oldAnims = wm.getAnimationScales(); - wm.setAnimationScale(0, 0.0f); - wm.setAnimationScale(1, 0.0f); - } catch (RemoteException e) { - } + oldAnims = wm.getAnimationScales(); + wm.setAnimationScale(0, 0.0f); + wm.setAnimationScale(1, 0.0f); } - try { - if (!mAm.startInstrumentation(cn, profileFile, 0, args, watcher)) { - System.out.println("INSTRUMENTATION_FAILED: " + - cn.flattenToString()); - showUsage(); - return; - } - } catch (RemoteException e) { + if (!mAm.startInstrumentation(cn, profileFile, 0, args, watcher)) { + throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString()); } if (watcher != null) { @@ -356,9 +303,58 @@ public class Am { } if (oldAnims != null) { + wm.setAnimationScales(oldAnims); + } + } + + private void runProfile() throws Exception { + String profileFile = null; + boolean start = false; + String process = nextArgRequired(); + ParcelFileDescriptor fd = null; + + String cmd = nextArgRequired(); + if ("start".equals(cmd)) { + start = true; + profileFile = nextArgRequired(); + try { + fd = ParcelFileDescriptor.open( + new File(profileFile), + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE | + ParcelFileDescriptor.MODE_READ_WRITE); + } catch (FileNotFoundException e) { + System.err.println("Error: Unable to open file: " + profileFile); + return; + } + } else if (!"stop".equals(cmd)) { + throw new IllegalArgumentException("Profile command " + cmd + " not valid"); + } + + if (!mAm.profileControl(process, start, profileFile, fd)) { + throw new AndroidException("PROFILE FAILED on process " + process); + } + } + + private class IntentReceiver extends IIntentReceiver.Stub { + private boolean mFinished = false; + + public synchronized void performReceive( + Intent intent, int rc, String data, Bundle ext, boolean ord, + boolean sticky) { + String line = "Broadcast completed: result=" + rc; + if (data != null) line = line + ", data=\"" + data + "\""; + if (ext != null) line = line + ", extras: " + ext; + System.out.println(line); + mFinished = true; + notifyAll(); + } + + public synchronized void waitForFinish() { try { - wm.setAnimationScales(oldAnims); - } catch (RemoteException e) { + while (!mFinished) wait(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); } } } @@ -366,7 +362,7 @@ public class Am { private class InstrumentationWatcher extends IInstrumentationWatcher.Stub { private boolean mFinished = false; private boolean mRawMode = false; - + /** * Set or reset "raw mode". In "raw mode", all bundles are dumped. In "pretty mode", * if a bundle includes Instrumentation.REPORT_KEY_STREAMRESULT, just print that. @@ -375,7 +371,7 @@ public class Am { public void setRawOutput(boolean rawMode) { mRawMode = rawMode; } - + public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) { synchronized (this) { // pretty printer mode? @@ -431,6 +427,7 @@ public class Am { } wait(1000); } catch (InterruptedException e) { + throw new IllegalStateException(e); } } } @@ -438,62 +435,11 @@ public class Am { } } - private void runProfile() { - String profileFile = null; - boolean start = false; - - String process = nextArg(); - if (process == null) { - System.err.println("Error: No profile process supplied"); - showUsage(); - return; - } - - ParcelFileDescriptor fd = null; - - String cmd = nextArg(); - if ("start".equals(cmd)) { - start = true; - profileFile = nextArg(); - if (profileFile == null) { - System.err.println("Error: No profile file path supplied"); - showUsage(); - return; - } - try { - fd = ParcelFileDescriptor.open( - new File(profileFile), - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE | - ParcelFileDescriptor.MODE_READ_WRITE); - } catch (FileNotFoundException e) { - System.err.println("Error: Unable to open file: " + profileFile); - return; - } - } else if (!"stop".equals(cmd)) { - System.err.println("Error: Profile command " + cmd + " not valid"); - showUsage(); - return; - } - - try { - if (!mAm.profileControl(process, start, profileFile, fd)) { - System.err.println("PROFILE FAILED on process " + process); - return; - } - } catch (IllegalArgumentException e) { - System.out.println("PROFILE FAILED: " + e.getMessage()); - return; - } catch (IllegalStateException e) { - System.out.println("PROFILE FAILED: " + e.getMessage()); - return; - } catch (RemoteException e) { - System.out.println("PROFILE FAILED: activity manager gone"); - return; - } - } - private String nextOption() { + if (mCurArgData != null) { + String prev = mArgs[mNextArg - 1]; + throw new IllegalArgumentException("No argument expected after \"" + prev + "\""); + } if (mNextArg >= mArgs.length) { return null; } @@ -518,41 +464,52 @@ public class Am { return arg; } - private String nextOptionData() { + private String nextArg() { if (mCurArgData != null) { - return mCurArgData; - } - if (mNextArg >= mArgs.length) { + String arg = mCurArgData; + mCurArgData = null; + return arg; + } else if (mNextArg < mArgs.length) { + return mArgs[mNextArg++]; + } else { return null; } - String data = mArgs[mNextArg]; - mNextArg++; - return data; } - private String nextArg() { - if (mNextArg >= mArgs.length) { - return null; + private String nextArgRequired() { + String arg = nextArg(); + if (arg == null) { + String prev = mArgs[mNextArg - 1]; + throw new IllegalArgumentException("Argument expected after \"" + prev + "\""); } - String arg = mArgs[mNextArg]; - mNextArg++; return arg; } - private void showUsage() { - System.err.println("usage: am [start|broadcast|instrument|profile]"); - System.err.println(" am start [-D] INTENT"); - System.err.println(" am broadcast INTENT"); - System.err.println(" am instrument [-r] [-e ] [-p ]"); - System.err.println(" [-w] "); - System.err.println(" am profile [start |stop]"); - System.err.println(""); - System.err.println(" INTENT is described with:"); - System.err.println(" [-a ] [-d ] [-t ]"); - System.err.println(" [-c [-c ] ...]"); - System.err.println(" [-e|--es ...]"); - System.err.println(" [--ez ...]"); - System.err.println(" [-e|--ei ...]"); - System.err.println(" [-n ] [-f ] []"); + private static void showUsage() { + System.err.println( + "usage: am [subcommand] [options]\n" + + "\n" + + " start an Activity: am start [-D] \n" + + " -D: enable debugging\n" + + "\n" + + " send a broadcast Intent: am broadcast \n" + + "\n" + + " start an Instrumentation: am instrument [flags] \n" + + " -r: print raw results (otherwise decode REPORT_KEY_STREAMRESULT)\n" + + " -e : set argument to \n" + + " -p : write profiling data to \n" + + " -w: wait for instrumentation to finish before returning\n" + + "\n" + + " start profiling: am profile start \n" + + " stop profiling: am profile stop\n" + + "\n" + + " specifications include these flags:\n" + + " [-a ] [-d ] [-t ]\n" + + " [-c [-c ] ...]\n" + + " [-e|--es ...]\n" + + " [--ez ...]\n" + + " [-e|--ei ...]\n" + + " [-n ] [-f ] []\n" + ); } } diff --git a/cmds/app_process/app_main.cpp b/cmds/app_process/app_main.cpp index d825d5a3a4dcbf345089f000bf7085f668057050..7decf9ae737d6da8a3808a58b3fc7d75071b25d8 100644 --- a/cmds/app_process/app_main.cpp +++ b/cmds/app_process/app_main.cpp @@ -7,8 +7,8 @@ #define LOG_TAG "appproc" -#include -#include +#include +#include #include #include #include diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index ee3ec1aa18fbb16be26da4100b4a4bddfacfb577..8c15d0b4ea136080f383274e5c1f5ea70a09d2aa 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -268,7 +268,7 @@ public final class Bmgr { private void printRestoreSets(RestoreSet[] sets) { for (RestoreSet s : sets) { - System.out.println(" " + s.token + " : " + s.name); + System.out.println(" " + Long.toHexString(s.token) + " : " + s.name); } } @@ -294,7 +294,7 @@ public final class Bmgr { private void doRestore() { long token; try { - token = Long.parseLong(nextArg()); + token = Long.parseLong(nextArg(), 16); } catch (NumberFormatException e) { showUsage(); return; @@ -311,12 +311,13 @@ public final class Bmgr { return; } RestoreSet[] sets = mRestore.getAvailableRestoreSets(); - for (RestoreSet s : sets) { - if (s.token == token) { - System.out.println("Scheduling restore: " + s.name); - mRestore.performRestore(token, observer); - didRestore = true; - break; + if (sets != null) { + for (RestoreSet s : sets) { + if (s.token == token) { + System.out.println("Scheduling restore: " + s.name); + didRestore = (mRestore.performRestore(token, observer) == 0); + break; + } } } if (!didRestore) { @@ -327,21 +328,27 @@ public final class Bmgr { printRestoreSets(sets); } } + + // if we kicked off a restore successfully, we have to wait for it + // to complete before we can shut down the restore session safely + if (didRestore) { + synchronized (observer) { + while (!observer.done) { + try { + observer.wait(); + } catch (InterruptedException ex) { + } + } + } + } + + // once the restore has finished, close down the session and we're done mRestore.endRestoreSession(); } catch (RemoteException e) { System.err.println(e.toString()); System.err.println(BMGR_NOT_RUNNING_ERR); } - // now wait for it to be done - synchronized (observer) { - while (!observer.done) { - try { - observer.wait(); - } catch (InterruptedException ex) { - } - } - } System.out.println("done"); } diff --git a/cmds/bootanimation/Android.mk b/cmds/bootanimation/Android.mk index 9c94c2ef3cf95ad29f4de12ad89a97dfdd178f1c..3449de1fe737910da0576b6f2b1ecf8fe0c5af66 100644 --- a/cmds/bootanimation/Android.mk +++ b/cmds/bootanimation/Android.mk @@ -12,12 +12,13 @@ ifeq ($(TARGET_SIMULATOR),true) endif endif +LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES -DEGL_EGLEXT_PROTOTYPES + LOCAL_SHARED_LIBRARIES := \ libcutils \ libutils \ libui \ - libcorecg \ - libsgl \ + libskia \ libEGL \ libGLESv1_CM diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index 2fb3f7994961f27fc2dca900e35698db324a70b7..ce36c4b3320ae77a222a18214c2282874ae1601c 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -21,8 +21,9 @@ #include #include #include +#include -#include +#include #include #include #include @@ -35,7 +36,8 @@ #include #include #include -#include +#include +#include #include #include @@ -51,7 +53,7 @@ namespace android { // --------------------------------------------------------------------------- BootAnimation::BootAnimation() : Thread(false) -{ +{ mSession = new SurfaceComposerClient(); } @@ -59,13 +61,29 @@ BootAnimation::~BootAnimation() { } void BootAnimation::onFirstRef() { - run("BootAnimation", PRIORITY_DISPLAY); + status_t err = mSession->linkToComposerDeath(this); + LOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err)); + if (err == NO_ERROR) { + run("BootAnimation", PRIORITY_DISPLAY); + } } -const sp& BootAnimation::session() const { +sp BootAnimation::session() const { return mSession; } + +void BootAnimation::binderDied(const wp& who) +{ + // woah, surfaceflinger died! + LOGD("SurfaceFlinger died, exiting..."); + + // calling requestExit() is not enough here because the Surface code + // might be blocked on a condition variable that will never be updated. + kill( getpid(), SIGKILL ); + requestExit(); +} + status_t BootAnimation::initTexture(Texture* texture, AssetManager& assets, const char* name) { Asset* asset = assets.open(name, Asset::ACCESS_BUFFER); @@ -121,6 +139,62 @@ status_t BootAnimation::initTexture(Texture* texture, AssetManager& assets, return NO_ERROR; } +status_t BootAnimation::initTexture(void* buffer, size_t len) +{ + //StopWatch watch("blah"); + + SkBitmap bitmap; + SkImageDecoder::DecodeMemory(buffer, len, + &bitmap, SkBitmap::kRGB_565_Config, + SkImageDecoder::kDecodePixels_Mode); + + // ensure we can call getPixels(). No need to call unlock, since the + // bitmap will go out of scope when we return from this method. + bitmap.lockPixels(); + + const int w = bitmap.width(); + const int h = bitmap.height(); + const void* p = bitmap.getPixels(); + + GLint crop[4] = { 0, h, w, -h }; + int tw = 1 << (31 - __builtin_clz(w)); + int th = 1 << (31 - __builtin_clz(h)); + if (tw < w) tw <<= 1; + if (th < h) th <<= 1; + + switch (bitmap.getConfig()) { + case SkBitmap::kARGB_8888_Config: + if (tw != w || th != h) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, + GL_UNSIGNED_BYTE, 0); + glTexSubImage2D(GL_TEXTURE_2D, 0, + 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, p); + } else { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, + GL_UNSIGNED_BYTE, p); + } + break; + + case SkBitmap::kRGB_565_Config: + if (tw != w || th != h) { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tw, th, 0, GL_RGB, + GL_UNSIGNED_SHORT_5_6_5, 0); + glTexSubImage2D(GL_TEXTURE_2D, 0, + 0, 0, w, h, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, p); + } else { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tw, th, 0, GL_RGB, + GL_UNSIGNED_SHORT_5_6_5, p); + } + break; + default: + break; + } + + glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, crop); + + return NO_ERROR; +} + status_t BootAnimation::readyToRun() { mAssets.addDefaultAssets(); @@ -130,15 +204,19 @@ status_t BootAnimation::readyToRun() { return -1; // create the native surface - sp s = session()->createSurface(getpid(), 0, dinfo.w, dinfo.h, - PIXEL_FORMAT_RGB_565, ISurfaceComposer::eGPU); + sp control = session()->createSurface( + getpid(), 0, dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565); session()->openTransaction(); - s->setLayer(0x40000000); + control->setLayer(0x40000000); session()->closeTransaction(); + sp s = control->getSurface(); + // initialize opengl and egl - const EGLint attribs[] = { EGL_RED_SIZE, 5, EGL_GREEN_SIZE, 6, - EGL_BLUE_SIZE, 5, EGL_DEPTH_SIZE, 0, EGL_NONE }; + const EGLint attribs[] = { + EGL_DEPTH_SIZE, 0, + EGL_NONE + }; EGLint w, h, dummy; EGLint numConfigs; EGLConfig config; @@ -148,60 +226,75 @@ status_t BootAnimation::readyToRun() { EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(display, 0, 0); - eglChooseConfig(display, attribs, &config, 1, &numConfigs); - - mNativeWindowSurface = new EGLNativeWindowSurface(s); - surface = eglCreateWindowSurface(display, config, - mNativeWindowSurface.get(), NULL); - + EGLUtils::selectConfigForNativeWindow(display, attribs, s.get(), &config); + surface = eglCreateWindowSurface(display, config, s.get(), NULL); context = eglCreateContext(display, config, NULL, NULL); eglQuerySurface(display, surface, EGL_WIDTH, &w); eglQuerySurface(display, surface, EGL_HEIGHT, &h); - eglMakeCurrent(display, surface, surface, context); + + if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) + return NO_INIT; + mDisplay = display; mContext = context; mSurface = surface; mWidth = w; mHeight = h; + mFlingerSurfaceControl = control; mFlingerSurface = s; - // initialize GL - glShadeModel(GL_FLAT); - glEnable(GL_TEXTURE_2D); - glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + mAndroidAnimation = false; + status_t err = mZip.open("/data/local/bootanimation.zip"); + if (err != NO_ERROR) { + err = mZip.open("/system/media/bootanimation.zip"); + if (err != NO_ERROR) { + mAndroidAnimation = true; + } + } return NO_ERROR; } -bool BootAnimation::threadLoop() { - bool r = android(); +bool BootAnimation::threadLoop() +{ + bool r; + if (mAndroidAnimation) { + r = android(); + } else { + r = movie(); + } + eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(mDisplay, mContext); eglDestroySurface(mDisplay, mSurface); - mNativeWindowSurface.clear(); mFlingerSurface.clear(); + mFlingerSurfaceControl.clear(); eglTerminate(mDisplay); IPCThreadState::self()->stopProcess(); return r; } -bool BootAnimation::android() { +bool BootAnimation::android() +{ initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png"); initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png"); // clear screen + glShadeModel(GL_FLAT); glDisable(GL_DITHER); glDisable(GL_SCISSOR_TEST); glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(mDisplay, mSurface); + glEnable(GL_TEXTURE_2D); + glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + const GLint xc = (mWidth - mAndroid[0].w) / 2; const GLint yc = (mHeight - mAndroid[0].h) / 2; const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h); // draw and update only what we need - mNativeWindowSurface->setSwapRectangle(updateRect.left, - updateRect.top, updateRect.width(), updateRect.height()); + mFlingerSurface->setSwapRectangle(updateRect); glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(), updateRect.height()); @@ -238,7 +331,7 @@ bool BootAnimation::android() { // 12fps: don't animate too fast to preserve CPU const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now); if (sleepTime > 0) - usleep(sleepTime); + usleep(sleepTime); } while (!exitPending()); glDeleteTextures(1, &mAndroid[0].name); @@ -246,6 +339,167 @@ bool BootAnimation::android() { return false; } + +bool BootAnimation::movie() +{ + ZipFileRO& zip(mZip); + + size_t numEntries = zip.getNumEntries(); + ZipEntryRO desc = zip.findEntryByName("desc.txt"); + FileMap* descMap = zip.createEntryFileMap(desc); + LOGE_IF(!descMap, "descMap is null"); + if (!descMap) { + return false; + } + + String8 desString((char const*)descMap->getDataPtr(), + descMap->getDataLength()); + char const* s = desString.string(); + + Animation animation; + + // Parse the description file + for (;;) { + const char* endl = strstr(s, "\n"); + if (!endl) break; + String8 line(s, endl - s); + const char* l = line.string(); + int fps, width, height, count, pause; + char path[256]; + if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) { + //LOGD("> w=%d, h=%d, fps=%d", fps, width, height); + animation.width = width; + animation.height = height; + animation.fps = fps; + } + if (sscanf(l, "p %d %d %s", &count, &pause, path) == 3) { + //LOGD("> count=%d, pause=%d, path=%s", count, pause, path); + Animation::Part part; + part.count = count; + part.pause = pause; + part.path = path; + animation.parts.add(part); + } + s = ++endl; + } + + // read all the data structures + const size_t pcount = animation.parts.size(); + for (size_t i=0 ; i 0) { + for (int j=0 ; j 0) { + glBindTexture(GL_TEXTURE_2D, frame.tid); + } else { + if (part.count != 1) { + glGenTextures(1, &frame.tid); + glBindTexture(GL_TEXTURE_2D, frame.tid); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } + initTexture( + frame.map->getDataPtr(), + frame.map->getDataLength()); + } + + if (!clearReg.isEmpty()) { + Region::const_iterator head(clearReg.begin()); + Region::const_iterator tail(clearReg.end()); + glEnable(GL_SCISSOR_TEST); + while (head != tail) { + const Rect& r(*head++); + glScissor(r.left, mHeight - r.bottom, + r.width(), r.height()); + glClear(GL_COLOR_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + } + glDrawTexiOES(xc, yc, 0, animation.width, animation.height); + eglSwapBuffers(mDisplay, mSurface); + + nsecs_t now = systemTime(); + nsecs_t delay = frameDuration - (now - lastFrame); + lastFrame = now; + long wait = ns2us(frameDuration); + if (wait > 0) + usleep(wait); + } + usleep(part.pause * ns2us(frameDuration)); + } + + // free the textures for this part + if (part.count != 1) { + for (int j=0 ; j& session() const; + sp session() const; private: virtual bool threadLoop(); virtual status_t readyToRun(); virtual void onFirstRef(); + virtual void binderDied(const wp& who); struct Texture { GLint w; @@ -57,8 +57,31 @@ private: GLuint name; }; + struct Animation { + struct Frame { + String8 name; + FileMap* map; + mutable GLuint tid; + bool operator < (const Frame& rhs) const { + return name < rhs.name; + } + }; + struct Part { + int count; + int pause; + String8 path; + SortedVector frames; + }; + int fps; + int width; + int height; + Vector parts; + }; + status_t initTexture(Texture* texture, AssetManager& asset, const char* name); + status_t initTexture(void* buffer, size_t len); bool android(); + bool movie(); sp mSession; AssetManager mAssets; @@ -68,8 +91,10 @@ private: EGLDisplay mDisplay; EGLDisplay mContext; EGLDisplay mSurface; + sp mFlingerSurfaceControl; sp mFlingerSurface; - sp mNativeWindowSurface; + bool mAndroidAnimation; + ZipFileRO mZip; }; // --------------------------------------------------------------------------- diff --git a/cmds/bootanimation/bootanimation_main.cpp b/cmds/bootanimation/bootanimation_main.cpp index a8359c40d0d275eaa3b9a2fd39b94f14f8d6399d..3c82fe52dba941e974e9b4b446e80dd1481951e9 100644 --- a/cmds/bootanimation/bootanimation_main.cpp +++ b/cmds/bootanimation/bootanimation_main.cpp @@ -18,9 +18,10 @@ #include -#include -#include -#include +#include +#include +#include + #include #include diff --git a/cmds/bugreport/Android.mk b/cmds/bugreport/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..631c2193a9878d251eca008b5320fdc993cf78b6 --- /dev/null +++ b/cmds/bugreport/Android.mk @@ -0,0 +1,14 @@ +ifneq ($(TARGET_SIMULATOR),true) + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= bugreport.c + +LOCAL_MODULE:= bugreport + +LOCAL_SHARED_LIBRARIES := libcutils + +include $(BUILD_EXECUTABLE) + +endif diff --git a/cmds/bugreport/bugreport.c b/cmds/bugreport/bugreport.c new file mode 100644 index 0000000000000000000000000000000000000000..4a0b51147011fbdba9c6dd4bb486d6227ff97035 --- /dev/null +++ b/cmds/bugreport/bugreport.c @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#include +#include +#include + +#include +#include + +int main(int argc, char *argv[]) { + char buffer[65536]; + int i, s; + + /* start the dumpstate service */ + property_set("ctl.start", "dumpstate"); + + /* socket will not be available until service starts */ + for (i = 0; i < 10; i++) { + s = socket_local_client("dumpstate", + ANDROID_SOCKET_NAMESPACE_RESERVED, + SOCK_STREAM); + if (s >= 0) + break; + /* try again in 1 second */ + sleep(1); + } + + if (s < 0) { + fprintf(stderr, "Failed to connect to dumpstate service\n"); + exit(1); + } + + while (1) { + int length = read(s, buffer, sizeof(buffer)); + if (length <= 0) + break; + fwrite(buffer, 1, length, stdout); + } + + close(s); + return 0; +} diff --git a/cmds/dumpstate/Android.mk b/cmds/dumpstate/Android.mk index f61d7ecb3f5a3620277b2a0cddd6ce2d7ec9d640..f8b37a812d08a4b0075a3624afdd4e3c1b4c9327 100644 --- a/cmds/dumpstate/Android.mk +++ b/cmds/dumpstate/Android.mk @@ -11,7 +11,7 @@ LOCAL_SHARED_LIBRARIES := libcutils include $(BUILD_EXECUTABLE) -COMMANDS = dumpcrash bugreport +COMMANDS = dumpcrash SYMLINKS := $(addprefix $(TARGET_OUT_EXECUTABLES)/,$(COMMANDS)) $(SYMLINKS): DUMPSTATE_BINARY := dumpstate $(SYMLINKS): $(LOCAL_INSTALLED_MODULE) $(LOCAL_PATH)/Android.mk diff --git a/cmds/dumpstate/dumpstate.c b/cmds/dumpstate/dumpstate.c index cc951c1bcbd14bb11c07db173187f339a654ae1a..642c943ffd3685a8b5884adaf214a0884278491f 100644 --- a/cmds/dumpstate/dumpstate.c +++ b/cmds/dumpstate/dumpstate.c @@ -20,12 +20,18 @@ #include #include #include +#include #include #include #include +#include #include "private/android_filesystem_config.h" +#define LOG_NDEBUG 0 +#define LOG_TAG "dumpstate" +#include + #include "dumpstate.h" static char* const gzip_args[] = { "gzip", "-6", 0 }; @@ -34,6 +40,8 @@ static int end_pattern[] = { 75, 50, 75, 50, 75, 0 }; static struct tm now; +static void dump_kernel_log(const char *path, const char *title) ; + /* dumps the current system state to stdout */ static void dumpstate(int full) { if (full) { @@ -48,6 +56,8 @@ static void dumpstate(int full) { EXEC_XBIN("procrank"); PRINT("------ VIRTUAL MEMORY STATS ------"); DUMP("/proc/vmstat"); + PRINT("------ VMALLOC INFO ------"); + DUMP("/proc/vmallocinfo"); PRINT("------ SLAB INFO ------"); DUMP("/proc/slabinfo"); PRINT("------ ZONEINFO ------"); @@ -76,9 +86,9 @@ static void dumpstate(int full) { DUMP("/proc/wakelocks"); PRINT(""); PRINT("------ PROCESSES ------"); - EXEC("ps"); + EXEC1("ps", "-P"); PRINT("------ PROCESSES AND THREADS ------"); - EXEC2("ps", "-t", "-p"); + EXEC3("ps", "-t", "-p", "-P"); PRINT("------ LIBRANK ------"); EXEC_XBIN("librank"); PRINT("------ BINDER FAILED TRANSACTION LOG ------"); @@ -101,8 +111,19 @@ static void dumpstate(int full) { DUMP("/data/system/packages.xml"); PRINT("------ PACKAGE UID ERRORS ------"); DUMP("/data/system/uiderrors.txt"); - PRINT("------ LAST KERNEL LOG ------"); - DUMP("/proc/last_kmsg"); + + dump_kernel_log("/data/dontpanic/last_kmsg", "LAST KMSG"); + dump_kernel_log("/data/dontpanic/apanic_console", + "PANIC CONSOLE"); + dump_kernel_log("/data/dontpanic/apanic_threads", + "PANIC THREADS"); + + PRINT("------ BACKLIGHTS ------"); + DUMP_PROMPT("LCD brightness=", "/sys/class/leds/lcd-backlight/brightness"); + DUMP_PROMPT("Button brightness=", "/sys/class/leds/button-backlight/brightness"); + DUMP_PROMPT("Keyboard brightness=", "/sys/class/leds/keyboard-backlight/brightness"); + DUMP_PROMPT("ALS mode=", "/sys/class/leds/lcd-backlight/als"); + DUMP_PROMPT("LCD driver registers:\n", "/sys/class/leds/lcd-backlight/registers"); } PRINT("========================================================"); PRINT("== build.prop"); @@ -157,47 +178,50 @@ static int check_command_name(const char* name, const char* test) { int main(int argc, char *argv[]) { int dumpcrash = check_command_name(argv[0], "dumpcrash"); - int bugreport = check_command_name(argv[0], "bugreport"); int add_date = 0; char* outfile = 0; int vibrate = 0; int compress = 0; + int socket = 0; int c, fd, vibrate_fd, fds[2]; char path[PATH_MAX]; pid_t pid; gid_t groups[] = { AID_LOG, AID_SDCARD_RW }; + LOGI("begin\n"); + /* set as high priority, and protect from OOM killer */ setpriority(PRIO_PROCESS, 0, -20); protect_from_oom_killer(); get_time(&now); - if (bugreport) { - do { - c = getopt(argc, argv, "do:vz"); - if (c == EOF) + do { + c = getopt(argc, argv, "do:svz"); + if (c == EOF) + break; + switch (c) { + case 'd': + add_date = 1; break; - switch (c) { - case 'd': - add_date = 1; - break; - case 'o': - outfile = optarg; - break; - case 'v': - vibrate = 1; - break; - case 'z': - compress = 1; - break; - case '?': - fprintf(stderr, "%s: invalid option -%c\n", - argv[0], optopt); - exit(1); - } - } while (1); - } + case 'o': + outfile = optarg; + break; + case 'v': + vibrate = 1; + break; + case 'z': + compress = 1; + break; + case 's': + socket = 1; + break; + case '?': + fprintf(stderr, "%s: invalid option -%c\n", + argv[0], optopt); + exit(1); + } + } while (1); /* open vibrator before switching user */ if (vibrate) { @@ -214,7 +238,31 @@ int main(int argc, char *argv[]) { /* make it safe to use both printf and STDOUT_FILENO */ setvbuf(stdout, 0, _IONBF, 0); - if (outfile) { + if (socket) { + struct sockaddr addr; + socklen_t alen; + + int s = android_get_control_socket("dumpstate"); + if (s < 0) { + fprintf(stderr, "could not open dumpstate socket\n"); + exit(1); + } + if (listen(s, 4) < 0) { + fprintf(stderr, "could not listen on dumpstate socket\n"); + exit(1); + } + + alen = sizeof(addr); + fd = accept(s, &addr, &alen); + if (fd < 0) { + fprintf(stderr, "could not accept dumpstate socket\n"); + exit(1); + } + + /* redirect stdout to the socket */ + dup2(fd, STDOUT_FILENO); + close(fd); + } else if (outfile) { if (strlen(outfile) > sizeof(path) - 100) exit(1); @@ -292,6 +340,25 @@ int main(int argc, char *argv[]) { /* so gzip will terminate */ close(STDOUT_FILENO); + LOGI("done\n"); + return 0; } +static void dump_kernel_log(const char *path, const char *title) + +{ + printf("------ KERNEL %s LOG ------\n", title); + if (access(path, R_OK) < 0) + printf("%s: %s\n", path, strerror(errno)); + else { + struct stat sbuf; + + if (stat(path, &sbuf) < 0) + printf("%s: stat failed (%s)\n", path, strerror(errno)); + else + printf("Harvested %s", ctime(&sbuf.st_mtime)); + + DUMP(path); + } +} diff --git a/cmds/dumpstate/dumpstate.h b/cmds/dumpstate/dumpstate.h index 6862e5a3edb415862259c62a1053531a9f17ea5a..b99b6d792249a16257789dc06851e148ce614b42 100644 --- a/cmds/dumpstate/dumpstate.h +++ b/cmds/dumpstate/dumpstate.h @@ -61,6 +61,15 @@ run_command(&c, TIMEOUT); \ } +#define EXEC1(cmd, a1) \ +{ \ + static struct Command c = { \ + "/system/bin/" cmd, \ + { cmd, a1, 0 } \ + }; \ + run_command(&c, TIMEOUT); \ +} + #define EXEC2(cmd, a1, a2) \ { \ static struct Command c = { \ @@ -70,6 +79,15 @@ run_command(&c, TIMEOUT); \ } +#define EXEC3(cmd, a1, a2, a3) \ +{ \ + static struct Command c = { \ + "/system/bin/" cmd, \ + { cmd, a1, a2, a3, 0 } \ + }; \ + run_command(&c, TIMEOUT); \ +} + #define EXEC4(cmd, a1, a2, a3, a4) \ { \ static struct Command c = { \ diff --git a/cmds/dumpsys/Android.mk b/cmds/dumpsys/Android.mk index 0c623cc319c54e8958846e001768dd2a2a6861fa..42b1b7365b1babaa1be6333e35910bc3e9015cdf 100644 --- a/cmds/dumpsys/Android.mk +++ b/cmds/dumpsys/Android.mk @@ -5,7 +5,9 @@ LOCAL_SRC_FILES:= \ dumpsys.cpp LOCAL_SHARED_LIBRARIES := \ - libutils + libutils \ + libbinder + ifeq ($(TARGET_OS),linux) LOCAL_CFLAGS += -DXP_UNIX diff --git a/cmds/dumpsys/dumpsys.cpp b/cmds/dumpsys/dumpsys.cpp index a62fe55600e9380e0862c196d2e8b3d5dd436fb1..945a690ae0ab2f570559e159a7877125fcbdaf9d 100644 --- a/cmds/dumpsys/dumpsys.cpp +++ b/cmds/dumpsys/dumpsys.cpp @@ -6,9 +6,9 @@ #define LOG_TAG "dumpsys" #include -#include -#include -#include +#include +#include +#include #include #include diff --git a/cmds/keystore/Android.mk b/cmds/keystore/Android.mk index 3daf44e40a00218d9e124ed39da4e0fbe0b5df11..15a199f30c46fa914837bf35397a89378722ffe7 100644 --- a/cmds/keystore/Android.mk +++ b/cmds/keystore/Android.mk @@ -1,22 +1,36 @@ +# +# 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. +# + ifneq ($(TARGET_SIMULATOR),true) LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_SRC_FILES:= \ - netkeystore.c keymgmt.c - -LOCAL_C_INCLUDES := \ - $(call include-path-for, system-core)/cutils \ - external/openssl/include - -LOCAL_SHARED_LIBRARIES := \ - libcutils libssl - -LOCAL_STATIC_LIBRARIES := +include $(CLEAR_VARS) +LOCAL_SRC_FILES := keystore.c +LOCAL_C_INCLUDES := external/openssl/include +LOCAL_SHARED_LIBRARIES := libcutils libcrypto LOCAL_MODULE:= keystore +include $(BUILD_EXECUTABLE) +include $(CLEAR_VARS) +LOCAL_SRC_FILES := keystore_cli.c +LOCAL_C_INCLUDES := external/openssl/include +LOCAL_SHARED_LIBRARIES := libcutils libcrypto +LOCAL_MODULE:= keystore_cli +LOCAL_MODULE_TAGS := debug include $(BUILD_EXECUTABLE) -endif # !simulator)) +endif diff --git a/cmds/keystore/certtool.h b/cmds/keystore/certtool.h deleted file mode 100644 index aefad668d160144a52b5f3cd5a76bd8395bb830c..0000000000000000000000000000000000000000 --- a/cmds/keystore/certtool.h +++ /dev/null @@ -1,91 +0,0 @@ -/* -** -** 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. -*/ - -#ifndef __CERTTOOL_H__ -#define __CERTTOOL_H__ - -#include -#include -#include -#include - -#include "common.h" -#include "netkeystore.h" - -#define CERT_NAME_LEN (2 * MAX_KEY_NAME_LENGTH + 2) - -/* - * The specific function 'get_cert' is used in daemons to get the key value - * from keystore. Caller should allocate the buffer and the length of the buffer - * should be MAX_KEY_VALUE_LENGTH. - */ -static inline int get_cert(const char *certname, unsigned char *value, int *size) -{ - int count, fd, ret = -1; - LPC_MARSHAL cmd; - char delimiter[] = "_"; - char *namespace, *keyname; - char *context = NULL; - char cname[CERT_NAME_LEN]; - - if ((certname == NULL) || (value == NULL)) { - LOGE("get_cert: certname or value is null\n"); - return -1; - } - - if (strlcpy(cname, certname, CERT_NAME_LEN) >= CERT_NAME_LEN) { - LOGE("get_cert: keyname is too long\n"); - return -1; - } - - fd = socket_local_client(SOCKET_PATH, - ANDROID_SOCKET_NAMESPACE_RESERVED, - SOCK_STREAM); - if (fd == -1) { - LOGE("Keystore service is not up and running.\n"); - return -1; - } - - cmd.opcode = GET; - if (((namespace = strtok_r(cname, delimiter, &context)) == NULL) || - ((keyname = strtok_r(NULL, delimiter, &context)) == NULL)) { - goto err; - } - if ((cmd.len = snprintf((char*)cmd.data, BUFFER_MAX, "%s %s", namespace, keyname)) - > (2 * MAX_KEY_NAME_LENGTH + 1)) goto err; - - if (write_marshal(fd, &cmd)) { - LOGE("Incorrect command or command line is too long.\n"); - goto err; - } - if (read_marshal(fd, &cmd)) { - LOGE("Failed to read the result.\n"); - goto err; - } - - // copy the result if succeeded. - if (!cmd.retcode && cmd.len <= BUFFER_MAX) { - memcpy(value, cmd.data, cmd.len); - ret = 0; - *size = cmd.len; - } -err: - close(fd); - return ret; -} - -#endif diff --git a/cmds/keystore/common.h b/cmds/keystore/common.h deleted file mode 100644 index a18114e91abc7df68931d47e17462fd8824b092f..0000000000000000000000000000000000000000 --- a/cmds/keystore/common.h +++ /dev/null @@ -1,60 +0,0 @@ -/* -** -** 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. -*/ - -#ifndef __COMMON_H__ -#define __COMMON_H__ - -#define SOCKET_PATH "keystore" -#define KEYSTORE_DIR "/data/misc/keystore/" - -#define READ_TIMEOUT 3 -#define MAX_KEY_NAME_LENGTH 64 -#define MAX_NAMESPACE_LENGTH MAX_KEY_NAME_LENGTH -#define MAX_KEY_VALUE_LENGTH 4096 - -#define BUFFER_MAX MAX_KEY_VALUE_LENGTH - -typedef enum { - BOOTUP, - UNINITIALIZED, - LOCKED, - UNLOCKED, -} KEYSTORE_STATE; - -typedef enum { - LOCK, - UNLOCK, - PASSWD, - GETSTATE, - LISTKEYS, - GET, - PUT, - REMOVE, - RESET, - MAX_OPCODE -} KEYSTORE_OPCODE; - -typedef struct { - uint32_t len; - union { - uint32_t opcode; - uint32_t retcode; - }; - unsigned char data[BUFFER_MAX + 1]; -} LPC_MARSHAL; - -#endif diff --git a/cmds/keystore/keymgmt.c b/cmds/keystore/keymgmt.c deleted file mode 100644 index 9a1f8457d224da88765d40cc4992a7a6d3cf27eb..0000000000000000000000000000000000000000 --- a/cmds/keystore/keymgmt.c +++ /dev/null @@ -1,426 +0,0 @@ -/* -** 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. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common.h" -#include "keymgmt.h" - -static int retry_count = 0; -static unsigned char iv[IV_LEN]; -static KEYSTORE_STATE state = BOOTUP; -static AES_KEY encryptKey, decryptKey; - -inline void unlock_keystore(unsigned char *master_key) -{ - AES_set_encrypt_key(master_key, AES_KEY_LEN, &encryptKey); - AES_set_decrypt_key(master_key, AES_KEY_LEN, &decryptKey); - memset(master_key, 0, sizeof(master_key)); - state = UNLOCKED; -} - -inline void lock_keystore() -{ - memset(&encryptKey, 0 , sizeof(AES_KEY)); - memset(&decryptKey, 0 , sizeof(AES_KEY)); - state = LOCKED; -} - -inline void get_encrypt_key(char *passwd, AES_KEY *key) -{ - unsigned char user_key[USER_KEY_LEN]; - gen_key(passwd, user_key, USER_KEY_LEN); - AES_set_encrypt_key(user_key, AES_KEY_LEN, key); -} - -inline void get_decrypt_key(char *passwd, AES_KEY *key) -{ - unsigned char user_key[USER_KEY_LEN]; - gen_key(passwd, user_key, USER_KEY_LEN); - AES_set_decrypt_key(user_key, AES_KEY_LEN, key); -} - -static int gen_random_blob(unsigned char *key, int size) -{ - int ret = 0; - int fd = open("/dev/urandom", O_RDONLY); - if (fd == -1) return -1; - if (read(fd, key, size) != size) ret = -1; - close(fd); - return ret; -} - -static int encrypt_n_save(AES_KEY *enc_key, DATA_BLOB *blob, - const char *keyfile) -{ - int size, fd, ret = -1; - unsigned char enc_blob[MAX_BLOB_LEN]; - char tmpfile[KEYFILE_LEN]; - - if ((keyfile == NULL) || (strlen(keyfile) >= (KEYFILE_LEN - 4))) { - LOGE("keyfile name is too long or null"); - return -1; - } - strcpy(tmpfile, keyfile); - strcat(tmpfile, ".tmp"); - - // prepare the blob - if (IV_LEN > USER_KEY_LEN) { - LOGE("iv length is too long."); - return -1; - } - memcpy(blob->iv, iv, IV_LEN); - blob->blob_size = get_blob_size(blob); - if (blob->blob_size > MAX_BLOB_LEN) { - LOGE("blob data size is too large."); - return -1; - } - memcpy(enc_blob, blob->blob, blob->blob_size); - AES_cbc_encrypt((unsigned char *)enc_blob, (unsigned char *)blob->blob, - blob->blob_size, enc_key, iv, AES_ENCRYPT); - // write to keyfile - size = data_blob_size(blob); - if ((fd = open(tmpfile, O_CREAT|O_RDWR)) == -1) return -1; - if (write(fd, blob, size) == size) ret = 0; - close(fd); - if (!ret) { - unlink(keyfile); - rename(tmpfile, keyfile); - chmod(keyfile, 0440); - } - return ret; -} - -static int load_n_decrypt(const char *keyname, const char *keyfile, - AES_KEY *key, DATA_BLOB *blob) -{ - int fd, ret = -1; - if ((fd = open(keyfile, O_RDONLY)) == -1) return -1; - // get the encrypted blob and iv - if ((read(fd, blob->iv, sizeof(blob->iv)) != sizeof(blob->iv)) || - (read(fd, &blob->blob_size, sizeof(uint32_t)) != sizeof(uint32_t)) || - (blob->blob_size > MAX_BLOB_LEN)) { - goto err; - } else { - unsigned char enc_blob[MAX_BLOB_LEN]; - if (read(fd, enc_blob, blob->blob_size) != - (int) blob->blob_size) goto err; - // decrypt the blob - AES_cbc_encrypt((unsigned char *)enc_blob, (unsigned char*)blob->blob, - blob->blob_size, key, blob->iv, AES_DECRYPT); - if (strcmp(keyname, (char*)blob->keyname) == 0) ret = 0; - } -err: - close(fd); - return ret; -} - -static int store_master_key(char *upasswd, unsigned char *master_key) -{ - AES_KEY key; - DATA_BLOB blob; - - // prepare the blob - if (strlen(MASTER_KEY_TAG) >= USER_KEY_LEN) return -1; - strlcpy(blob.keyname, MASTER_KEY_TAG, USER_KEY_LEN); - blob.value_size = USER_KEY_LEN; - if (USER_KEY_LEN > MAX_KEY_VALUE_LENGTH) { - LOGE("master_key length is too long."); - return -1; - } - memcpy((void*)blob.value, (const void*)master_key, USER_KEY_LEN); - - // generate the encryption key - get_encrypt_key(upasswd, &key); - return encrypt_n_save(&key, &blob, MASTER_KEY); -} - -static int get_master_key(char *upasswd, unsigned char *master_key) -{ - AES_KEY key; - int size, ret = 0; - DATA_BLOB blob; - - get_decrypt_key(upasswd, &key); - ret = load_n_decrypt(MASTER_KEY_TAG, MASTER_KEY, &key, &blob); - if (blob.value_size > USER_KEY_LEN) { - LOGE("the blob's value size is too large"); - return -1; - } - if (!ret) memcpy(master_key, blob.value, blob.value_size); - return ret; -} - -static int create_master_key(char *upasswd) -{ - int ret; - unsigned char mpasswd[AES_KEY_LEN]; - unsigned char master_key[USER_KEY_LEN]; - - gen_random_blob(mpasswd, AES_KEY_LEN); - gen_key((char*)mpasswd, master_key, USER_KEY_LEN); - if ((ret = store_master_key(upasswd, master_key)) == 0) { - unlock_keystore(master_key); - } - memset(master_key, 0, USER_KEY_LEN); - memset(mpasswd, 0, AES_KEY_LEN); - - return ret; -} - -static int change_passwd(char *data) -{ - unsigned char master_key[USER_KEY_LEN]; - char *old_pass, *new_pass = NULL, *p, *delimiter=" "; - int ret, count = 0; - char *context = NULL; - - old_pass = p = strtok_r(data, delimiter, &context); - while (p != NULL) { - count++; - new_pass = p; - p = strtok_r(NULL, delimiter, &context); - } - if (count != 2) return -1; - if (strlen(new_pass) < MIN_PASSWD_LENGTH) return -1; - if ((ret = get_master_key(old_pass, master_key)) == 0) { - ret = store_master_key(new_pass, master_key); - retry_count = 0; - } else { - ret = MAX_RETRY_COUNT - ++retry_count; - if (ret == 0) { - retry_count = 0; - LOGE("passwd:reach max retry count, reset the keystore now."); - reset_keystore(); - return -1; - } - - } - return ret; -} - -int remove_key(const char *namespace, const char *keyname) -{ - char keyfile[KEYFILE_LEN]; - - if (state != UNLOCKED) return -state; - if ((strlen(namespace) >= MAX_KEY_NAME_LENGTH) || - (strlen(keyname) >= MAX_KEY_NAME_LENGTH)) { - LOGE("keyname is too long."); - return -1; - } - sprintf(keyfile, KEYFILE_NAME, namespace, keyname); - return unlink(keyfile); -} - -int put_key(const char *namespace, const char *keyname, - unsigned char *data, int size) -{ - DATA_BLOB blob; - uint32_t real_size; - char keyfile[KEYFILE_LEN]; - - if (state != UNLOCKED) { - LOGE("Can not store key with current state %d\n", state); - return -state; - } - if ((strlen(namespace) >= MAX_KEY_NAME_LENGTH) || - (strlen(keyname) >= MAX_KEY_NAME_LENGTH)) { - LOGE("keyname is too long."); - return -1; - } - sprintf(keyfile, KEYFILE_NAME, namespace, keyname); - strcpy(blob.keyname, keyname); - blob.value_size = size; - if (size > MAX_KEY_VALUE_LENGTH) { - LOGE("the data size is too large."); - return -1; - } - memcpy(blob.value, data, size); - return encrypt_n_save(&encryptKey, &blob, keyfile); -} - -int get_key(const char *namespace, const char *keyname, - unsigned char *data, int *size) -{ - int ret; - DATA_BLOB blob; - uint32_t blob_size; - char keyfile[KEYFILE_LEN]; - - if (state != UNLOCKED) { - LOGE("Can not retrieve key value with current state %d\n", state); - return -state; - } - if ((strlen(namespace) >= MAX_KEY_NAME_LENGTH) || - (strlen(keyname) >= MAX_KEY_NAME_LENGTH)) { - LOGE("keyname is too long."); - return -1; - } - sprintf(keyfile, KEYFILE_NAME, namespace, keyname); - ret = load_n_decrypt(keyname, keyfile, &decryptKey, &blob); - if (!ret) { - if ((blob.value_size > MAX_KEY_VALUE_LENGTH)) { - LOGE("blob value size is too large."); - ret = -1; - } else { - *size = blob.value_size; - memcpy(data, blob.value, *size); - } - } - return ret; -} - -int list_keys(const char *namespace, char reply[BUFFER_MAX]) -{ - DIR *d; - struct dirent *de; - - if (state != UNLOCKED) { - LOGE("Can not list key with current state %d\n", state); - return -1; - } - - if (!namespace || ((d = opendir("."))) == NULL) { - LOGE("cannot open keystore dir or namespace is null\n"); - return -1; - } - - if (strlen(namespace) >= MAX_KEY_NAME_LENGTH) { - LOGE("namespace is too long."); - return -1; - } - - reply[0] = 0; - while ((de = readdir(d))) { - char *prefix, *name, *keyfile = de->d_name; - char *context = NULL; - - if (de->d_type != DT_REG) continue; - if ((prefix = strtok_r(keyfile, NAME_DELIMITER, &context)) - == NULL) continue; - if (strcmp(prefix, namespace)) continue; - if ((name = strtok_r(NULL, NAME_DELIMITER, &context)) == NULL) continue; - // append the key name into reply - if (reply[0] != 0) strlcat(reply, " ", BUFFER_MAX); - if (strlcat(reply, name, BUFFER_MAX) >= BUFFER_MAX) { - LOGE("too many files under keystore directory\n"); - return -1; - } - } - closedir(d); - return 0; -} - -int passwd(char *data) -{ - if (state == UNINITIALIZED) { - if (strchr(data, ' ')) return -1; - if (strlen(data) < MIN_PASSWD_LENGTH) return -1; - return create_master_key(data); - } - return change_passwd(data); -} - -int lock() -{ - switch(state) { - case UNLOCKED: - lock_keystore(); - case LOCKED: - return 0; - default: - return -1; - } -} - -int unlock(char *passwd) -{ - unsigned char master_key[USER_KEY_LEN]; - int ret = get_master_key(passwd, master_key); - if (!ret) { - unlock_keystore(master_key); - retry_count = 0; - } else { - ret = MAX_RETRY_COUNT - ++retry_count; - if (ret == 0) { - retry_count = 0; - LOGE("unlock:reach max retry count, reset the keystore now."); - reset_keystore(); - return -1; - } - } - return ret; -} - -KEYSTORE_STATE get_state() -{ - return state; -} - -int reset_keystore() -{ - int ret = 0; - DIR *d; - struct dirent *de; - - if ((d = opendir(".")) == NULL) { - LOGE("cannot open keystore dir\n"); - return -1; - } - while ((de = readdir(d))) { - if (unlink(de->d_name) != 0) ret = -1; - } - closedir(d); - state = UNINITIALIZED; - if (ret == 0) { - LOGI("keystore is reset."); - } else { - LOGI("keystore can not be cleaned up entirely."); - } - return ret; -} - -int init_keystore(const char *dir) -{ - int fd; - - if (dir) mkdir(dir, 0770); - if (!dir || chdir(dir)) { - LOGE("Can not open/create the keystore directory %s\n", - dir ? dir : "(null)"); - return -1; - } - gen_random_blob(iv, IV_LEN); - if ((fd = open(MASTER_KEY, O_RDONLY)) == -1) { - state = UNINITIALIZED; - return 0; - } - close(fd); - state = LOCKED; - return 0; -} diff --git a/cmds/keystore/keymgmt.h b/cmds/keystore/keymgmt.h deleted file mode 100644 index 0e928db494fdb99e0bd03aff0271613bc186a508..0000000000000000000000000000000000000000 --- a/cmds/keystore/keymgmt.h +++ /dev/null @@ -1,82 +0,0 @@ -/* -** 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. -*/ - -#ifndef __KEYMGMT_H__ -#define __KEYMGMT_H__ - -#define MASTER_KEY_TAG "master_key" -#define MASTER_KEY ".keymaster" -#define MAX_PATH_LEN 128 -#define SALT "Android Keystore 0.1" -#define NAME_DELIMITER "_" -#define KEYFILE_NAME "%s"NAME_DELIMITER"%s" -#define KEYGEN_ITER 1024 -#define AES_KEY_LEN 128 -#define USER_KEY_LEN (AES_KEY_LEN/8) -#define IV_LEN USER_KEY_LEN -#define MAX_RETRY_COUNT 6 -#define MIN_PASSWD_LENGTH 8 - -#define gen_key(passwd, key, len) \ - PKCS5_PBKDF2_HMAC_SHA1(passwd, strlen(passwd), \ - (unsigned char*)SALT, \ - strlen(SALT), KEYGEN_ITER, \ - len, key) - -#define KEYFILE_LEN MAX_NAMESPACE_LENGTH + MAX_KEY_NAME_LENGTH + 6 - -#define get_blob_size(blob) \ - (((blob->value_size + sizeof(uint32_t) + MAX_KEY_NAME_LENGTH \ - + USER_KEY_LEN - 1) / USER_KEY_LEN) * USER_KEY_LEN) - -#define MAX_BLOB_LEN ((MAX_KEY_VALUE_LENGTH + MAX_KEY_NAME_LENGTH + \ - sizeof(uint32_t) + USER_KEY_LEN - 1) / USER_KEY_LEN)\ - * USER_KEY_LEN - -#define data_blob_size(blob) USER_KEY_LEN + sizeof(uint32_t) + blob->blob_size - -typedef struct { - unsigned char iv[USER_KEY_LEN]; - uint32_t blob_size; - union { - unsigned char blob[1]; - struct { - uint32_t value_size; - char keyname[MAX_KEY_NAME_LENGTH]; - unsigned char value[MAX_KEY_VALUE_LENGTH]; - } __attribute__((packed)); - }; -} DATA_BLOB; - -typedef struct { - char tag[USER_KEY_LEN]; - unsigned char master_key[USER_KEY_LEN]; -} MASTER_BLOB; - -int put_key(const char *namespace, const char *keyname, - unsigned char *data, int size); -int get_key(const char *namespace, const char *keyname, - unsigned char *data, int *size); -int remove_key(const char *namespace, const char *keyname); -int list_keys(const char *namespace, char reply[BUFFER_MAX]); -int passwd(char *data); -int lock(); -int unlock(char *passwd); -KEYSTORE_STATE get_state(); -int reset_keystore(); -int init_keystore(const char *dir); - -#endif diff --git a/cmds/keystore/keystore.c b/cmds/keystore/keystore.c new file mode 100644 index 0000000000000000000000000000000000000000..ba74c7814e81cac535aa04460a385e1934fc6b77 --- /dev/null +++ b/cmds/keystore/keystore.c @@ -0,0 +1,542 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_TAG "keystore" +#include +#include +#include + +#include "keystore.h" + +/* KeyStore is a secured storage for key-value pairs. In this implementation, + * each file stores one key-value pair. Keys are encoded in file names, and + * values are encrypted with checksums. The encryption key is protected by a + * user-defined password. To keep things simple, buffers are always larger than + * the maximum space we needed, so boundary checks on buffers are omitted. */ + +#define KEY_SIZE 120 +#define VALUE_SIZE 32768 +#define PASSWORD_SIZE VALUE_SIZE + +/* Here is the encoding of keys. This is necessary in order to allow arbitrary + * characters in keys. Characters in [0-~] are not encoded. Others are encoded + * into two bytes. The first byte is one of [+-.] which represents the first + * two bits of the character. The second byte encodes the rest of the bits into + * [0-o]. Therefore in the worst case the length of a key gets doubled. Note + * that Base64 cannot be used here due to the need of prefix match on keys. */ + +static int encode_key(char *out, uint8_t *in, int length) +{ + int i; + for (i = length; i > 0; --i, ++in, ++out) { + if (*in >= '0' && *in <= '~') { + *out = *in; + } else { + *out = '+' + (*in >> 6); + *++out = '0' + (*in & 0x3F); + ++length; + } + } + *out = 0; + return length; +} + +static int decode_key(uint8_t *out, char *in, int length) +{ + int i; + for (i = 0; i < length; ++i, ++in, ++out) { + if (*in >= '0' && *in <= '~') { + *out = *in; + } else { + *out = (*in - '+') << 6; + *out |= (*++in - '0') & 0x3F; + --length; + } + } + *out = 0; + return length; +} + +/* Here is the protocol used in both requests and responses: + * code [length_1 message_1 ... length_n message_n] end-of-file + * where code is one byte long and lengths are unsigned 16-bit integers in + * network order. Thus the maximum length of a message is 65535 bytes. */ + +static int the_socket = -1; + +static int recv_code(int8_t *code) +{ + return recv(the_socket, code, 1, 0) == 1; +} + +static int recv_message(uint8_t *message, int length) +{ + uint8_t bytes[2]; + if (recv(the_socket, &bytes[0], 1, 0) != 1 || + recv(the_socket, &bytes[1], 1, 0) != 1) { + return -1; + } else { + int offset = bytes[0] << 8 | bytes[1]; + if (length < offset) { + return -1; + } + length = offset; + offset = 0; + while (offset < length) { + int n = recv(the_socket, &message[offset], length - offset, 0); + if (n <= 0) { + return -1; + } + offset += n; + } + } + return length; +} + +static int recv_end_of_file() +{ + uint8_t byte; + return recv(the_socket, &byte, 1, 0) == 0; +} + +static void send_code(int8_t code) +{ + send(the_socket, &code, 1, 0); +} + +static void send_message(uint8_t *message, int length) +{ + uint16_t bytes = htons(length); + send(the_socket, &bytes, 2, 0); + send(the_socket, message, length, 0); +} + +/* Here is the file format. Values are encrypted by AES CBC, and MD5 is used to + * compute their checksums. To make the files portable, the length is stored in + * network order. Note that the first four bytes are reserved for future use and + * are always set to zero in this implementation. */ + +static int the_entropy = -1; + +static struct __attribute__((packed)) { + uint32_t reserved; + uint8_t vector[AES_BLOCK_SIZE]; + uint8_t encrypted[0]; + uint8_t digest[MD5_DIGEST_LENGTH]; + uint8_t digested[0]; + int32_t length; + uint8_t value[VALUE_SIZE + AES_BLOCK_SIZE]; +} blob; + +static int8_t encrypt_blob(char *name, AES_KEY *aes_key) +{ + uint8_t vector[AES_BLOCK_SIZE]; + int length = blob.length; + int fd; + + if (read(the_entropy, vector, AES_BLOCK_SIZE) != AES_BLOCK_SIZE) { + return SYSTEM_ERROR; + } + + length += blob.value - blob.digested; + blob.length = htonl(blob.length); + MD5(blob.digested, length, blob.digest); + + length += blob.digested - blob.encrypted; + length = (length + AES_BLOCK_SIZE - 1) / AES_BLOCK_SIZE * AES_BLOCK_SIZE; + memcpy(vector, blob.vector, AES_BLOCK_SIZE); + AES_cbc_encrypt(blob.encrypted, blob.encrypted, length, aes_key, vector, + AES_ENCRYPT); + + blob.reserved = 0; + length += blob.encrypted - (uint8_t *)&blob; + + fd = open(".tmp", O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR); + if (fd == -1 || write(fd, &blob, length) != length) { + return SYSTEM_ERROR; + } + close(fd); + return rename(".tmp", name) ? SYSTEM_ERROR : NO_ERROR; +} + +static int8_t decrypt_blob(char *name, AES_KEY *aes_key) +{ + int fd = open(name, O_RDONLY); + int length; + + if (fd == -1) { + return (errno == ENOENT) ? KEY_NOT_FOUND : SYSTEM_ERROR; + } + length = read(fd, &blob, sizeof(blob)); + close(fd); + + length -= blob.encrypted - (uint8_t *)&blob; + if (length < blob.value - blob.encrypted || length % AES_BLOCK_SIZE != 0) { + return VALUE_CORRUPTED; + } + + AES_cbc_encrypt(blob.encrypted, blob.encrypted, length, aes_key, + blob.vector, AES_DECRYPT); + length -= blob.digested - blob.encrypted; + if (!memcmp(blob.digest, MD5(blob.digested, length, NULL), + MD5_DIGEST_LENGTH)) { + return VALUE_CORRUPTED; + } + + length -= blob.value - blob.digested; + blob.length = ntohl(blob.length); + return (length < blob.length) ? VALUE_CORRUPTED : NO_ERROR; +} + +/* Here are the actions. Each of them is a function without arguments. All + * information is defined in global variables, which are set properly before + * performing an action. The number of parameters required by each action is + * fixed and defined in a table. If the return value of an action is positive, + * it will be treated as a response code and transmitted to the client. Note + * that the lengths of parameters are checked when they are received, so + * boundary checks on parameters are omitted. */ + +#define MAX_PARAM 2 +#define MAX_RETRY 4 + +static uid_t uid = -1; +static int8_t state = UNINITIALIZED; +static int8_t retry = MAX_RETRY; + +static struct { + int length; + uint8_t value[VALUE_SIZE]; +} params[MAX_PARAM]; + +static AES_KEY encryption_key; +static AES_KEY decryption_key; + +static int8_t test() +{ + return state; +} + +static int8_t get() +{ + char name[NAME_MAX]; + int n = sprintf(name, "%u_", uid); + encode_key(&name[n], params[0].value, params[0].length); + n = decrypt_blob(name, &decryption_key); + if (n != NO_ERROR) { + return n; + } + send_code(NO_ERROR); + send_message(blob.value, blob.length); + return -NO_ERROR; +} + +static int8_t insert() +{ + char name[NAME_MAX]; + int n = sprintf(name, "%u_", uid); + encode_key(&name[n], params[0].value, params[0].length); + blob.length = params[1].length; + memcpy(blob.value, params[1].value, params[1].length); + return encrypt_blob(name, &encryption_key); +} + +static int8_t delete() +{ + char name[NAME_MAX]; + int n = sprintf(name, "%u_", uid); + encode_key(&name[n], params[0].value, params[0].length); + return (unlink(name) && errno != ENOENT) ? SYSTEM_ERROR : NO_ERROR; +} + +static int8_t exist() +{ + char name[NAME_MAX]; + int n = sprintf(name, "%u_", uid); + encode_key(&name[n], params[0].value, params[0].length); + if (access(name, R_OK) == -1) { + return (errno != ENOENT) ? SYSTEM_ERROR : KEY_NOT_FOUND; + } + return NO_ERROR; +} + +static int8_t saw() +{ + DIR *dir = opendir("."); + struct dirent *file; + char name[NAME_MAX]; + int n; + + if (!dir) { + return SYSTEM_ERROR; + } + n = sprintf(name, "%u_", uid); + n += encode_key(&name[n], params[0].value, params[0].length); + send_code(NO_ERROR); + while ((file = readdir(dir)) != NULL) { + if (!strncmp(name, file->d_name, n)) { + char *p = &file->d_name[n]; + params[0].length = decode_key(params[0].value, p, strlen(p)); + send_message(params[0].value, params[0].length); + } + } + closedir(dir); + return -NO_ERROR; +} + +static int8_t reset() +{ + DIR *dir = opendir("."); + struct dirent *file; + + memset(&encryption_key, 0, sizeof(encryption_key)); + memset(&decryption_key, 0, sizeof(decryption_key)); + state = UNINITIALIZED; + retry = MAX_RETRY; + + if (!dir) { + return SYSTEM_ERROR; + } + while ((file = readdir(dir)) != NULL) { + unlink(file->d_name); + } + closedir(dir); + return NO_ERROR; +} + +#define MASTER_KEY_FILE ".masterkey" +#define MASTER_KEY_SIZE 16 + +static void generate_key(uint8_t *key, uint8_t *password, int length) +{ + PKCS5_PBKDF2_HMAC_SHA1((char *)password, length, (uint8_t *)"keystore", + sizeof("keystore"), 1024, MASTER_KEY_SIZE, key); +} + +static int8_t password() +{ + uint8_t key[MASTER_KEY_SIZE]; + AES_KEY aes_key; + int n; + + if (state == UNINITIALIZED) { + blob.length = MASTER_KEY_SIZE; + if (read(the_entropy, blob.value, MASTER_KEY_SIZE) != MASTER_KEY_SIZE) { + return SYSTEM_ERROR; + } + } else { + generate_key(key, params[0].value, params[0].length); + AES_set_decrypt_key(key, MASTER_KEY_SIZE * 8, &aes_key); + n = decrypt_blob(MASTER_KEY_FILE, &aes_key); + if (n == SYSTEM_ERROR) { + return SYSTEM_ERROR; + } + if (n != NO_ERROR || blob.length != MASTER_KEY_SIZE) { + if (retry <= 0) { + reset(); + return UNINITIALIZED; + } + return WRONG_PASSWORD + --retry; + } + } + + if (params[1].length == -1) { + memcpy(key, blob.value, MASTER_KEY_SIZE); + } else { + generate_key(key, params[1].value, params[1].length); + AES_set_encrypt_key(key, MASTER_KEY_SIZE * 8, &aes_key); + memcpy(key, blob.value, MASTER_KEY_SIZE); + n = encrypt_blob(MASTER_KEY_FILE, &aes_key); + } + + if (n == NO_ERROR) { + AES_set_encrypt_key(key, MASTER_KEY_SIZE * 8, &encryption_key); + AES_set_decrypt_key(key, MASTER_KEY_SIZE * 8, &decryption_key); + state = NO_ERROR; + retry = MAX_RETRY; + } + return n; +} + +static int8_t lock() +{ + memset(&encryption_key, 0, sizeof(encryption_key)); + memset(&decryption_key, 0, sizeof(decryption_key)); + state = LOCKED; + return NO_ERROR; +} + +static int8_t unlock() +{ + params[1].length = -1; + return password(); +} + +/* Here are the permissions, actions, users, and the main function. */ + +enum perm { + TEST = 1, + GET = 2, + INSERT = 4, + DELETE = 8, + EXIST = 16, + SAW = 32, + RESET = 64, + PASSWORD = 128, + LOCK = 256, + UNLOCK = 512, +}; + +static struct action { + int8_t (*run)(); + int8_t code; + int8_t state; + uint32_t perm; + int lengths[MAX_PARAM]; +} actions[] = { + {test, 't', 0, TEST, {0}}, + {get, 'g', NO_ERROR, GET, {KEY_SIZE}}, + {insert, 'i', NO_ERROR, INSERT, {KEY_SIZE, VALUE_SIZE}}, + {delete, 'd', 0, DELETE, {KEY_SIZE}}, + {exist, 'e', 0, EXIST, {KEY_SIZE}}, + {saw, 's', 0, SAW, {KEY_SIZE}}, + {reset, 'r', 0, RESET, {0}}, + {password, 'p', 0, PASSWORD, {PASSWORD_SIZE, PASSWORD_SIZE}}, + {lock, 'l', NO_ERROR, LOCK, {0}}, + {unlock, 'u', LOCKED, UNLOCK, {PASSWORD_SIZE}}, + {NULL, 0 , 0, 0, {0}}, +}; + +static struct user { + uid_t uid; + uid_t euid; + uint32_t perms; +} users[] = { + {AID_SYSTEM, 0, ~GET}, + {AID_VPN, AID_SYSTEM, GET}, + {AID_WIFI, AID_SYSTEM, GET}, + {0, 0, TEST | GET | INSERT | DELETE | EXIST | SAW}, +}; + +static int8_t process(int8_t code) { + struct user *user = users; + struct action *action = actions; + int i; + + while (user->uid && user->uid != uid) { + ++user; + } + while (action->code && action->code != code) { + ++action; + } + if (!action->code) { + return UNDEFINED_ACTION; + } + if (!(action->perm & user->perms)) { + return PERMISSION_DENIED; + } + if (action->state && action->state != state) { + return state; + } + if (user->euid) { + uid = user->euid; + } + for (i = 0; i < MAX_PARAM && action->lengths[i]; ++i) { + params[i].length = recv_message(params[i].value, action->lengths[i]); + if (params[i].length == -1) { + return PROTOCOL_ERROR; + } + } + if (!recv_end_of_file()) { + return PROTOCOL_ERROR; + } + return action->run(); +} + +#define RANDOM_DEVICE "/dev/urandom" + +int main(int argc, char **argv) +{ + int control_socket = android_get_control_socket("keystore"); + if (argc < 2) { + LOGE("A directory must be specified!"); + return 1; + } + if (chdir(argv[1]) == -1) { + LOGE("chdir: %s: %s", argv[1], strerror(errno)); + return 1; + } + if ((the_entropy = open(RANDOM_DEVICE, O_RDONLY)) == -1) { + LOGE("open: %s: %s", RANDOM_DEVICE, strerror(errno)); + return 1; + } + if (listen(control_socket, 3) == -1) { + LOGE("listen: %s", strerror(errno)); + return 1; + } + + signal(SIGPIPE, SIG_IGN); + if (access(MASTER_KEY_FILE, R_OK) == 0) { + state = LOCKED; + } + + while ((the_socket = accept(control_socket, NULL, 0)) != -1) { + struct timeval tv = {.tv_sec = 3}; + struct ucred cred; + socklen_t size = sizeof(cred); + int8_t request; + + setsockopt(the_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(the_socket, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + if (getsockopt(the_socket, SOL_SOCKET, SO_PEERCRED, &cred, &size)) { + LOGW("getsockopt: %s", strerror(errno)); + } else if (recv_code(&request)) { + int8_t old_state = state; + int8_t response; + uid = cred.uid; + + if ((response = process(request)) > 0) { + send_code(response); + response = -response; + } + + LOGI("uid: %d action: %c -> %d state: %d -> %d retry: %d", + cred.uid, request, -response, old_state, state, retry); + } + close(the_socket); + } + LOGE("accept: %s", strerror(errno)); + return 1; +} diff --git a/cmds/keystore/keystore.h b/cmds/keystore/keystore.h new file mode 100644 index 0000000000000000000000000000000000000000..5ef51e9cd7d00f85a97c00fd0bb5ab47f7afa2bc --- /dev/null +++ b/cmds/keystore/keystore.h @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#ifndef __KEYSTORE_H__ +#define __KEYSTORE_H__ + +enum response_code { + NO_ERROR = 1, + LOCKED = 2, + UNINITIALIZED = 3, + SYSTEM_ERROR = 4, + PROTOCOL_ERROR = 5, + PERMISSION_DENIED = 6, + KEY_NOT_FOUND = 7, + VALUE_CORRUPTED = 8, + UNDEFINED_ACTION = 9, + WRONG_PASSWORD = 10, +}; + +#endif diff --git a/cmds/keystore/keystore_cli.c b/cmds/keystore/keystore_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..e8afb5a945b2657620cc4cb9c68ddd5141f22929 --- /dev/null +++ b/cmds/keystore/keystore_cli.c @@ -0,0 +1,97 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include + +#include + +#include "keystore.h" + +char *responses[256] = { + [NO_ERROR] = "No error", + [LOCKED] = "Locked", + [UNINITIALIZED] = "Uninitialized", + [SYSTEM_ERROR] = "System error", + [PROTOCOL_ERROR] = "Protocol error", + [PERMISSION_DENIED] = "Permission denied", + [KEY_NOT_FOUND] = "Key not found", + [VALUE_CORRUPTED] = "Value corrupted", + [UNDEFINED_ACTION] = "Undefined action", + [WRONG_PASSWORD] = "Wrong password (last chance)", + [WRONG_PASSWORD + 1] = "Wrong password (2 tries left)", + [WRONG_PASSWORD + 2] = "Wrong password (3 tries left)", + [WRONG_PASSWORD + 3] = "Wrong password (4 tries left)", +}; + +#define MAX_RESPONSE (WRONG_PASSWORD + 3) + +int main(int argc, char **argv) +{ + uint8_t bytes[65536]; + uint8_t code; + int sock, i; + + if (argc < 2) { + printf("Usage: %s action [parameter ...]\n", argv[0]); + return 0; + } + + sock = socket_local_client("keystore", ANDROID_SOCKET_NAMESPACE_RESERVED, + SOCK_STREAM); + if (sock == -1) { + puts("Failed to connect"); + return 1; + } + + send(sock, argv[1], 1, 0); + for (i = 2; i < argc; ++i) { + uint16_t length = strlen(argv[i]); + bytes[0] = length >> 8; + bytes[1] = length; + send(sock, &bytes, 2, 0); + send(sock, argv[i], length, 0); + } + shutdown(sock, SHUT_WR); + + if (recv(sock, &code, 1, 0) != 1) { + puts("Failed to receive"); + return 1; + } + printf("%d %s\n", code , responses[code] ? responses[code] : "Unknown"); + while ((i = recv(sock, &bytes[0], 1, 0)) == 1) { + int length; + int offset; + if ((i = recv(sock, &bytes[1], 1, 0)) != 1) { + puts("Failed to receive"); + return 1; + } + length = bytes[0] << 8 | bytes[1]; + for (offset = 0; offset < length; offset += i) { + i = recv(sock, &bytes[offset], length - offset, 0); + if (i <= 0) { + puts("Failed to receive"); + return 1; + } + } + fwrite(bytes, 1, length, stdout); + puts(""); + } + return 0; +} diff --git a/cmds/keystore/keystore_get.h b/cmds/keystore/keystore_get.h index 7665e81a314d6aada5b73234647d1d6cb5c62458..0e7e1aeabb0f56eab7747e5262f09f0558c1b856 100644 --- a/cmds/keystore/keystore_get.h +++ b/cmds/keystore/keystore_get.h @@ -1,53 +1,69 @@ /* -** -** 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. -*/ + * 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. + */ #ifndef __KEYSTORE_GET_H__ #define __KEYSTORE_GET_H__ #include -#include +#include #include +#include +#include +#include -#include "certtool.h" +#include -/* This function is provided to native components to get values from keystore. - * Users are required to link against libcutils. If something goes wrong, NULL - * is returned. Otherwise it returns the value in dynamically allocated memory - * and sets the size if the pointer is not NULL. One can release the memory by - * calling free(). */ -static char *keystore_get(const char *key, int *size) +#define KEYSTORE_MESSAGE_SIZE 65535 + +/* This function is provided for native components to get values from keystore. + * Users are required to link against libcutils. The lengths of keys and values + * are limited to KEYSTORE_MESSAGE_SIZE. This function returns the length of + * the requested value or -1 if something goes wrong. */ +static int keystore_get(const char *key, char *value) { - char buffer[MAX_KEY_VALUE_LENGTH]; - char *value; - int length; + int length = strlen(key); + uint8_t bytes[2] = {length >> 8, length}; + uint8_t code = 'g'; + int sock; - if (get_cert(key, (unsigned char *)buffer, &length) != 0) { - return NULL; + if (length > KEYSTORE_MESSAGE_SIZE) { + return -1; } - value = malloc(length + 1); - if (!value) { - return NULL; + sock = socket_local_client("keystore", ANDROID_SOCKET_NAMESPACE_RESERVED, + SOCK_STREAM); + if (sock == -1) { + return -1; } - memcpy(value, buffer, length); - value[length] = 0; - if (size) { - *size = length; + if (send(sock, &code, 1, 0) == 1 && send(sock, bytes, 2, 0) == 2 && + send(sock, key, length, 0) == length && shutdown(sock, SHUT_WR) == 0 && + recv(sock, &code, 1, 0) == 1 && code == /* NO_ERROR */ 1 && + recv(sock, &bytes[0], 1, 0) == 1 && recv(sock, &bytes[1], 1, 0) == 1) { + int offset = 0; + length = bytes[0] << 8 | bytes[1]; + while (offset < length) { + int n = recv(sock, &value[offset], length - offset, 0); + if (n <= 0) { + length = -1; + break; + } + offset += n; + } } - return value; + close(sock); + return length; } #endif diff --git a/cmds/keystore/netkeystore.c b/cmds/keystore/netkeystore.c deleted file mode 100644 index 637e0d87600dd7d2aefd9f9ce3de0bfacbd052ef..0000000000000000000000000000000000000000 --- a/cmds/keystore/netkeystore.c +++ /dev/null @@ -1,411 +0,0 @@ -/* -** 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. -*/ - -#define LOG_TAG "keystore" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "netkeystore.h" -#include "keymgmt.h" - -#define DBG 1 -#define CMD_PUT_WITH_FILE "putfile" - -typedef void CMD_FUNC(LPC_MARSHAL *cmd, LPC_MARSHAL *reply); - -struct cmdinfo { - const char *name; - CMD_FUNC *func; -}; - -static CMD_FUNC do_lock; -static CMD_FUNC do_unlock; -static CMD_FUNC do_passwd; -static CMD_FUNC do_get_state;; -static CMD_FUNC do_listkeys; -static CMD_FUNC do_get_key; -static CMD_FUNC do_put_key; -static CMD_FUNC do_remove_key; -static CMD_FUNC do_reset_keystore; - -#define str(x) #x - -struct cmdinfo cmds[] = { - { str(LOCK), do_lock }, - { str(UNLOCK), do_unlock }, - { str(PASSWD), do_passwd }, - { str(GETSTATE), do_get_state }, - { str(LISTKEYS), do_listkeys }, - { str(GET), do_get_key }, - { str(PUT), do_put_key }, - { str(REMOVE), do_remove_key }, - { str(RESET), do_reset_keystore }, -}; - -static struct ucred cr; - -static int check_get_perm(int uid) -{ - if (uid == AID_WIFI || uid == AID_VPN) return 0; - return -1; -} - -static int check_reset_perm(int uid) -{ - if (uid == AID_SYSTEM) return 0; - return -1; -} - -static int parse_keyname(char *name, uint32_t len, - char *namespace, char *keyname) -{ - int count = 0; - char *c = namespace, *p = namespace, *t = name; - - if (!name || !namespace || !keyname) return -1; - while (t < name + len && (*t != 0)) { - if (*t == ' ') { - if (c == keyname) return -1; - *p = count = 0; - c = p = keyname; - t++; - } else { - if (!isalnum(*t)) return -1; - *p++ = *t++; - // also check if the keyname/namespace is too long. - if (count++ == MAX_KEY_NAME_LENGTH) return -1; - } - } - *p = 0; - return 0; -} - -// args of passwd(): -// firstPassword - for the first time -// oldPassword newPassword - for changing the password -static void do_passwd(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - reply->retcode = passwd((char*)cmd->data); -} - -// args of lock(): -// no argument -static void do_lock(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - reply->retcode = lock(); -} - -// args of unlock(): -// password -static void do_unlock(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - reply->retcode = unlock((char*)cmd->data); -} - -// args of get_state(): -// no argument -static void do_get_state(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - reply->retcode = get_state(); -} - -// args of listkeys(): -// namespace -static void do_listkeys(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - reply->retcode = list_keys((const char*)cmd->data, (char*)reply->data); - if (!reply->retcode) reply->len = strlen((char*)reply->data); -} - -// args of get(): -// namespace keyname -static void do_get_key(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - char namespace[MAX_KEY_NAME_LENGTH]; - char keyname[MAX_KEY_NAME_LENGTH]; - - if (check_get_perm(cr.uid)) { - LOGE("uid %d doesn't have the permission to get key value\n", cr.uid); - reply->retcode = -1; - return; - } - - if (parse_keyname((char*)cmd->data, cmd->len, namespace, keyname)) { - reply->retcode = -1; - } else { - reply->retcode = get_key(namespace, keyname, reply->data, - (int*)&reply->len); - } -} - -static int get_value_index(LPC_MARSHAL *cmd) -{ - uint32_t count = 0, i; - for (i = 0 ; i < cmd->len ; ++i) { - if (cmd->data[i] == ' ') { - if (++count == 2) return ++i; - } - } - return -1; -} - -// args of put(): -// namespace keyname keyvalue -static void do_put_key(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - char namespace[MAX_KEY_NAME_LENGTH]; - char keyname[MAX_KEY_NAME_LENGTH]; - - int p = get_value_index(cmd); - if (p == -1) { - reply->retcode = -1; - } else { - unsigned char *value; - if (parse_keyname((char*)cmd->data, p - 1, namespace, keyname)) { - reply->retcode = -1; - return; - } - value = &cmd->data[p]; - int len = cmd->len - p; - reply->retcode = put_key(namespace, keyname, value, len); - } -} - -// args of remove_key(): -// namespace keyname -static void do_remove_key(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - char namespace[MAX_KEY_NAME_LENGTH]; - char keyname[MAX_KEY_NAME_LENGTH]; - if (parse_keyname((char*)cmd->data, cmd->len, namespace, keyname)) { - reply->retcode = -1; - return; - } - reply->retcode = remove_key(namespace, keyname); -} - -// args of reset_keystore(): -// no argument -static void do_reset_keystore(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - if (check_reset_perm(cr.uid)) { - LOGE("uid %d doesn't have the permission to reset the keystore\n", - cr.uid); - reply->retcode = -1; - return; - } - reply->retcode = reset_keystore(); -} - -static void execute(LPC_MARSHAL *cmd, LPC_MARSHAL *reply) -{ - uint32_t cmd_max = sizeof(cmds)/sizeof(struct cmdinfo); - - if (cmd->opcode >= cmd_max) { - LOGE("the opcode (%d) is not valid", cmd->opcode); - reply->retcode = -1; - return; - } - cmds[cmd->opcode].func(cmd, reply); -} - -static int set_read_timeout(int socket) -{ - struct timeval tv; - tv.tv_sec = READ_TIMEOUT; - if (setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof tv)) - { - LOGE("setsockopt failed"); - return -1; - } - return 0; -} - -static int append_input_from_file(const char *filename, LPC_MARSHAL *cmd) -{ - int fd, len, ret = 0; - - // get opcode of the function put() - if ((fd = open(filename, O_RDONLY)) == -1) { - fprintf(stderr, "Can not open file %s\n", filename); - return -1; - } - cmd->data[cmd->len] = ' '; - cmd->len++; - len = read(fd, cmd->data + cmd->len, BUFFER_MAX - cmd->len); - if (len < 0 || (len == (int)(BUFFER_MAX - cmd->len))) { - ret = -1; - } else { - cmd->len += len; - } - close(fd); - return ret; -} - -static int flatten_str_args(int argc, const char **argv, LPC_MARSHAL *cmd) -{ - int i, len = 0; - char *buf = (char*)cmd->data; - buf[0] = 0; - for (i = 0 ; i < argc ; ++i) { - if (i == 0) { - len = strlcpy(buf, argv[i], BUFFER_MAX); - } else { - len += snprintf(buf + len, BUFFER_MAX - len, " %s", argv[i]); - } - if (len >= BUFFER_MAX) return -1; - } - if (len) cmd->len = len; - return 0; -} - -static int parse_cmd(int argc, const char **argv, LPC_MARSHAL *cmd) -{ - uint32_t i, len = 0; - uint32_t cmd_max = sizeof(cmds)/sizeof(cmds[0]); - - for (i = 0 ; i < cmd_max ; ++i) { - if (!strcasecmp(argv[0], cmds[i].name)) break; - } - - if (i == cmd_max) { - // check if this is a command to put the key value with a file. - if (strcmp(argv[0], CMD_PUT_WITH_FILE) != 0) return -1; - cmd->opcode = PUT; - if (argc != 4) { - fprintf(stderr, "%s args\n\tnamespace keyname filename\n", - argv[0]); - return -1; - } - if (flatten_str_args(argc - 2, argv + 1, cmd)) return -1; - return append_input_from_file(argv[3], cmd); - } else { - cmd->opcode = i; - return flatten_str_args(argc - 1, argv + 1, cmd); - } -} - -static int shell_command(const int argc, const char **argv) -{ - int fd, i; - LPC_MARSHAL cmd; - - if (parse_cmd(argc, argv , &cmd)) { - fprintf(stderr, "Incorrect command or command line is too long.\n"); - exit(1); - } - fd = socket_local_client(SOCKET_PATH, - ANDROID_SOCKET_NAMESPACE_RESERVED, - SOCK_STREAM); - if (fd == -1) { - fprintf(stderr, "Keystore service is not up and running.\n"); - exit(1); - } - - if (write_marshal(fd, &cmd)) { - fprintf(stderr, "Incorrect command or command line is too long.\n"); - exit(1); - } - if (read_marshal(fd, &cmd)) { - fprintf(stderr, "Failed to read the result.\n"); - exit(1); - } - cmd.data[cmd.len] = 0; - fprintf(stdout, "%s\n", (cmd.retcode == 0) ? "Succeeded!" : "Failed!"); - if (cmd.len) fprintf(stdout, "\t%s\n", (char*)cmd.data); - close(fd); - return 0; -} - -int main(const int argc, const char *argv[]) -{ - struct sockaddr addr; - socklen_t alen; - int lsocket, s; - LPC_MARSHAL cmd, reply; - - if (argc > 1) { - return shell_command(argc - 1, argv + 1); - } - - if (init_keystore(KEYSTORE_DIR)) { - LOGE("Can not initialize the keystore, the directory exist?\n"); - exit(1); - } - - lsocket = android_get_control_socket(SOCKET_PATH); - if (lsocket < 0) { - LOGE("Failed to get socket from environment: %s\n", strerror(errno)); - exit(1); - } - if (listen(lsocket, 5)) { - LOGE("Listen on socket failed: %s\n", strerror(errno)); - exit(1); - } - fcntl(lsocket, F_SETFD, FD_CLOEXEC); - memset(&reply, 0, sizeof(LPC_MARSHAL)); - - for (;;) { - socklen_t cr_size = sizeof(cr); - alen = sizeof(addr); - s = accept(lsocket, &addr, &alen); - - /* retrieve the caller info here */ - if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) { - close(s); - LOGE("Unable to recieve socket options\n"); - continue; - } - - if (s < 0) { - LOGE("Accept failed: %s\n", strerror(errno)); - continue; - } - fcntl(s, F_SETFD, FD_CLOEXEC); - if (set_read_timeout(s)) { - close(s); - continue; - } - - // read the command, execute and send the result back. - if(read_marshal(s, &cmd)) goto err; - if (DBG) LOGD("new connection\n"); - execute(&cmd, &reply); - write_marshal(s, &reply); -err: - memset(&reply, 0, sizeof(LPC_MARSHAL)); - if (DBG) LOGD("closing connection\n"); - close(s); - } - - return 0; -} diff --git a/cmds/keystore/netkeystore.h b/cmds/keystore/netkeystore.h deleted file mode 100644 index a87a667e91236a88a7737ab5948729fe670962cc..0000000000000000000000000000000000000000 --- a/cmds/keystore/netkeystore.h +++ /dev/null @@ -1,96 +0,0 @@ -/* -** -** 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. -*/ - -#ifndef __NETKEYSTORE_H__ -#define __NETKEYSTORE_H__ - -#include -#include -#include - -#include "common.h" - -static inline int readx(int s, void *_buf, int count) -{ - char *buf = _buf; - int n = 0, r; - if (count < 0) return -1; - while (n < count) { - r = read(s, buf + n, count - n); - if (r < 0) { - if (errno == EINTR) continue; - LOGE("read error: %s\n", strerror(errno)); - return -1; - } - if (r == 0) { - LOGE("eof\n"); - return -1; /* EOF */ - } - n += r; - } - return 0; -} - -static inline int writex(int s, const void *_buf, int count) -{ - const char *buf = _buf; - int n = 0, r; - if (count < 0) return -1; - while (n < count) { - r = write(s, buf + n, count - n); - if (r < 0) { - if (errno == EINTR) continue; - LOGE("write error: %s\n", strerror(errno)); - return -1; - } - n += r; - } - return 0; -} - -static inline int read_marshal(int s, LPC_MARSHAL *cmd) -{ - if (readx(s, cmd, 2 * sizeof(uint32_t))) { - LOGE("failed to read header\n"); - return -1; - } - if (cmd->len > BUFFER_MAX) { - LOGE("invalid size %d\n", cmd->len); - return -1; - } - if (readx(s, cmd->data, cmd->len)) { - LOGE("failed to read data\n"); - return -1; - } - cmd->data[cmd->len] = 0; - return 0; -} - -static inline int write_marshal(int s, LPC_MARSHAL *cmd) -{ - if (writex(s, cmd, 2 * sizeof(uint32_t))) { - LOGE("failed to write marshal header\n"); - return -1; - } - if (writex(s, cmd->data, cmd->len)) { - LOGE("failed to write marshal data\n"); - return -1; - } - return 0; -} - -#endif diff --git a/cmds/keystore/tests/netkeystore_test.c b/cmds/keystore/tests/netkeystore_test.c deleted file mode 100644 index e7e686bd4368d3d770fd76e07fd680b7b2f0e87f..0000000000000000000000000000000000000000 --- a/cmds/keystore/tests/netkeystore_test.c +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS - * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED - * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -#include -#include -#include - -#include "common.h" -#include "keymgmt.h" - -typedef int FUNC_PTR(); -typedef struct { - const char *name; - FUNC_PTR *func; -} TESTFUNC; - -#define FUNC_NAME(x) { #x, test_##x } -#define FUNC_BODY(x) int test_##x() - -#define TEST_PASSWD "12345678" -#define TEST_NPASSWD "11111111" -#define TEST_DIR "/data/local/tmp/keystore" -#define READONLY_DIR "/proc/keystore" -#define TEST_NAMESPACE "test" -#define TEST_KEYNAME "key" -#define TEST_KEYNAME2 "key2" -#define TEST_KEYVALUE "ANDROID" - -void setup() -{ - if (init_keystore(TEST_DIR) != 0) { - fprintf(stderr, "Can not create the test directory %s\n", TEST_DIR); - exit(-1); - } -} - -void teardown() -{ - reset_keystore(); - rmdir(TEST_DIR); -} - -FUNC_BODY(init_keystore) -{ - if (init_keystore(READONLY_DIR) == 0) return -1; - - return EXIT_SUCCESS; -} - -FUNC_BODY(reset_keystore) -{ - chdir("/procx"); - if (reset_keystore() == 0) return -1; - chdir(TEST_DIR); - return EXIT_SUCCESS; -} - -FUNC_BODY(get_state) -{ - if (get_state() != UNINITIALIZED) return -1; - passwd(TEST_PASSWD); - if (get_state() != UNLOCKED) return -1; - lock(); - if (get_state() != LOCKED) return -1; - reset_keystore(); - if (get_state() != UNINITIALIZED) return -1; - return EXIT_SUCCESS; -} - -FUNC_BODY(passwd) -{ - char buf[512]; - - if (passwd(" 23432dsfsdf") == 0) return -1; - if (passwd("dsfsdf") == 0) return -1; - passwd(TEST_PASSWD); - lock(); - if (unlock("55555555") == 0) return -1; - if (unlock(TEST_PASSWD) != 0) return -1; - - // change the password - sprintf(buf, "%s %s", "klfdjdsklfjg", "abcdefghi"); - if (passwd(buf) == 0) return -1; - - sprintf(buf, "%s %s", TEST_PASSWD, TEST_NPASSWD); - if (passwd(buf) != 0) return -1; - lock(); - - if (unlock(TEST_PASSWD) == 0) return -1; - if (unlock(TEST_NPASSWD) != 0) return -1; - - return EXIT_SUCCESS; -} - -FUNC_BODY(lock) -{ - if (lock() == 0) return -1; - passwd(TEST_PASSWD); - if (lock() != 0) return -1; - if (lock() != 0) return -1; - return EXIT_SUCCESS; -} - -FUNC_BODY(unlock) -{ - int i = MAX_RETRY_COUNT; - passwd(TEST_PASSWD); - lock(); - while (i > 1) { - if (unlock(TEST_NPASSWD) != --i) return -1; - } - if (unlock(TEST_NPASSWD) != -1) return -1; - return EXIT_SUCCESS; -} - -FUNC_BODY(put_key) -{ - int i = 0; - char keyname[512]; - - if (put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)) == 0) return -1; - passwd(TEST_PASSWD); - if (put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)) != 0) return -1; - - for(i = 0; i < 500; i++) keyname[i] = 'K'; - keyname[i] = 0; - if (put_key(TEST_NAMESPACE, keyname, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)) == 0) return -1; - if (put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - MAX_KEY_VALUE_LENGTH + 1) == 0) return -1; - return EXIT_SUCCESS; -} - -FUNC_BODY(get_key) -{ - int size; - unsigned char data[MAX_KEY_VALUE_LENGTH]; - - if (get_key(TEST_NAMESPACE, TEST_KEYNAME, data, &size) == 0) return -1; - - passwd(TEST_PASSWD); - put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)); - if (get_key(TEST_NAMESPACE, TEST_KEYNAME, data, &size) != 0) return -1; - if (memcmp(data, TEST_KEYVALUE, size) != 0) return -1; - - return EXIT_SUCCESS; -} - -FUNC_BODY(remove_key) -{ - if (remove_key(TEST_NAMESPACE, TEST_KEYNAME) == 0) return -1; - - passwd(TEST_PASSWD); - if (remove_key(TEST_NAMESPACE, TEST_KEYNAME) == 0) return -1; - - put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)); - if (remove_key(TEST_NAMESPACE, TEST_KEYNAME) != 0) return -1; - - return EXIT_SUCCESS; -} - -FUNC_BODY(list_keys) -{ - int i; - char buf[128]; - char reply[BUFFER_MAX]; - - for(i = 0; i < 100; i++) buf[i] = 'K'; - buf[i] = 0; - - if (list_keys(TEST_NAMESPACE, reply) == 0) return -1; - - passwd(TEST_PASSWD); - if (list_keys(buf, reply) == 0) return -1; - - if (list_keys(TEST_NAMESPACE, reply) != 0) return -1; - if (strcmp(reply, "") != 0) return -1; - - put_key(TEST_NAMESPACE, TEST_KEYNAME, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)); - if (list_keys(TEST_NAMESPACE, reply) != 0) return -1; - if (strcmp(reply, TEST_KEYNAME) != 0) return -1; - - put_key(TEST_NAMESPACE, TEST_KEYNAME2, (unsigned char *)TEST_KEYVALUE, - strlen(TEST_KEYVALUE)); - - if (list_keys(TEST_NAMESPACE, reply) != 0) return -1; - sprintf(buf, "%s %s", TEST_KEYNAME2, TEST_KEYNAME); - if (strcmp(reply, buf) != 0) return -1; - - return EXIT_SUCCESS; -} - -TESTFUNC all_tests[] = { - FUNC_NAME(init_keystore), - FUNC_NAME(reset_keystore), - FUNC_NAME(get_state), - FUNC_NAME(passwd), - FUNC_NAME(lock), - FUNC_NAME(unlock), - FUNC_NAME(put_key), - FUNC_NAME(get_key), - FUNC_NAME(remove_key), - FUNC_NAME(list_keys), -}; - -int main(int argc, char **argv) { - int i, ret; - for (i = 0 ; i < (int)(sizeof(all_tests)/sizeof(TESTFUNC)) ; ++i) { - setup(); - if ((ret = all_tests[i].func()) != EXIT_SUCCESS) { - fprintf(stderr, "ERROR in function %s\n", all_tests[i].name); - return ret; - } else { - fprintf(stderr, "function %s PASSED!\n", all_tests[i].name); - } - teardown(); - } - return EXIT_SUCCESS; -} diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index fd9e70884e68641bb117dcb8e9321207bcab693a..79eb310bb0e5410428d535145679c46be0aa599b 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -18,6 +18,7 @@ package com.android.commands.pm; import android.content.ComponentName; import android.content.pm.ApplicationInfo; +import android.content.pm.FeatureInfo; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageManager; @@ -34,6 +35,8 @@ import android.os.RemoteException; import android.os.ServiceManager; import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -42,21 +45,21 @@ import java.util.WeakHashMap; public final class Pm { IPackageManager mPm; - + private WeakHashMap mResourceCache = new WeakHashMap(); - + private String[] mArgs; private int mNextArg; private String mCurArgData; - - private static final String PM_NOT_RUNNING_ERR = + + private static final String PM_NOT_RUNNING_ERR = "Error: Could not access the Package Manager. Is the system running?"; - + public static void main(String[] args) { new Pm().run(args); } - + public void run(String[] args) { boolean validCommand = false; if (args.length < 1) { @@ -73,37 +76,37 @@ public final class Pm { mArgs = args; String op = args[0]; mNextArg = 1; - + if ("list".equals(op)) { runList(); return; } - + if ("path".equals(op)) { runPath(); return; } - + if ("install".equals(op)) { runInstall(); return; } - + if ("uninstall".equals(op)) { runUninstall(); return; } - + if ("enable".equals(op)) { runSetEnabledSetting(PackageManager.COMPONENT_ENABLED_STATE_ENABLED); return; } - + if ("disable".equals(op)) { runSetEnabledSetting(PackageManager.COMPONENT_ENABLED_STATE_DISABLED); return; } - + try { if (args.length == 1) { if (args[0].equalsIgnoreCase("-l")) { @@ -128,13 +131,14 @@ public final class Pm { } } } - + /** * Execute the list sub-command. - * + * * pm list [package | packages] * pm list permission-groups * pm list permissions + * pm list features * pm list instrumentation */ private void runList() { @@ -150,6 +154,8 @@ public final class Pm { runListPermissionGroups(); } else if ("permissions".equals(type)) { runListPermissions(); + } else if ("features".equals(type)) { + runListFeatures(); } else if ("instrumentation".equals(type)) { runListInstrumentation(); } else { @@ -157,7 +163,7 @@ public final class Pm { showUsage(); } } - + /** * Lists all the installed packages. */ @@ -182,10 +188,10 @@ public final class Pm { showUsage(); return; } - + try { List packages = mPm.getInstalledPackages(0 /* all */); - + int count = packages.size(); for (int p = 0 ; p < count ; p++) { PackageInfo info = packages.get(p); @@ -201,10 +207,48 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + + /** + * Lists all of the features supported by the current device. + * + * pm list features + */ + private void runListFeatures() { + try { + List list = new ArrayList(); + FeatureInfo[] rawList = mPm.getSystemAvailableFeatures(); + for (int i=0; i() { + public int compare(FeatureInfo o1, FeatureInfo o2) { + if (o1.name == o2.name) return 0; + if (o1.name == null) return -1; + if (o2.name == null) return 1; + return o1.name.compareTo(o2.name); + } + }); + + int count = (list != null) ? list.size() : 0; + for (int p = 0; p < count; p++) { + FeatureInfo fi = list.get(p); + System.out.print("feature:"); + if (fi.name != null) System.out.println(fi.name); + else System.out.println("reqGlEsVersion=0x" + + Integer.toHexString(fi.reqGlEsVersion)); + } + } catch (RemoteException e) { + System.err.println(e.toString()); + System.err.println(PM_NOT_RUNNING_ERR); + } + } + /** * Lists all of the installed instrumentation, or all for a given package - * + * * pm list instrumentation [package] [-f] */ private void runListInstrumentation() { @@ -260,14 +304,14 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + /** * Lists all the known permission groups. */ private void runListPermissionGroups() { try { List pgs = mPm.getAllPermissionGroups(0); - + int count = pgs.size(); for (int p = 0 ; p < count ; p++) { PermissionGroupInfo pgi = pgs.get(p); @@ -279,7 +323,7 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + private String loadText(PackageItemInfo pii, int res, CharSequence nonLocalized) { if (nonLocalized != null) { return nonLocalized.toString(); @@ -290,7 +334,7 @@ public final class Pm { } return null; } - + /** * Lists all the permissions in a group. */ @@ -321,7 +365,7 @@ public final class Pm { return; } } - + String grp = nextOption(); ArrayList groupList = new ArrayList(); if (groups) { @@ -334,7 +378,7 @@ public final class Pm { } else { groupList.add(grp); } - + if (dangerousOnly) { System.out.println("Dangerous Permissions:"); System.out.println(""); @@ -365,7 +409,7 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + private void doListPermissions(ArrayList groupList, boolean groups, boolean labels, boolean summary, int startProtectionLevel, int endProtectionLevel) @@ -385,7 +429,7 @@ public final class Pm { pgi.nonLocalizedLabel) + ": "); } else { System.out.print(pgi.name + ": "); - + } } else { System.out.println((labels ? "+ " : "") @@ -468,13 +512,13 @@ public final class Pm { } } } - + if (summary) { System.out.println(""); } } } - + private void runPath() { String pkg = nextArg(); if (pkg == null) { @@ -484,7 +528,7 @@ public final class Pm { } displayPackageFilePath(pkg); } - + class PackageInstallObserver extends IPackageInstallObserver.Stub { boolean finished; int result; @@ -497,92 +541,40 @@ public final class Pm { } } } - + + /** + * Converts a failure code into a string by using reflection to find a matching constant + * in PackageManager. + */ private String installFailureToString(int result) { - String s; - switch (result) { - case PackageManager.INSTALL_FAILED_ALREADY_EXISTS: - s = "INSTALL_FAILED_ALREADY_EXISTS"; - break; - case PackageManager.INSTALL_FAILED_INVALID_APK: - s = "INSTALL_FAILED_INVALID_APK"; - break; - case PackageManager.INSTALL_FAILED_INVALID_URI: - s = "INSTALL_FAILED_INVALID_URI"; - break; - case PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE: - s = "INSTALL_FAILED_INSUFFICIENT_STORAGE"; - break; - case PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE: - s = "INSTALL_FAILED_DUPLICATE_PACKAGE"; - break; - case PackageManager.INSTALL_FAILED_NO_SHARED_USER: - s = "INSTALL_FAILED_NO_SHARED_USER"; - break; - case PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE: - s = "INSTALL_FAILED_UPDATE_INCOMPATIBLE"; - break; - case PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE: - s = "INSTALL_FAILED_SHARED_USER_INCOMPATIBLE"; - break; - case PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY: - s = "INSTALL_FAILED_MISSING_SHARED_LIBRARY"; - break; - case PackageManager.INSTALL_FAILED_DEXOPT: - s = "INSTALL_FAILED_DEXOPT"; - break; - case PackageManager.INSTALL_FAILED_OLDER_SDK: - s = "INSTALL_FAILED_OLDER_SDK"; - break; - case PackageManager.INSTALL_FAILED_CONFLICTING_PROVIDER: - s = "INSTALL_FAILED_CONFLICTING_PROVIDER"; - break; - case PackageManager.INSTALL_FAILED_NEWER_SDK: - s = "INSTALL_FAILED_NEWER_SDK"; - break; - case PackageManager.INSTALL_FAILED_TEST_ONLY: - s = "INSTALL_FAILED_TEST_ONLY"; - break; - case PackageManager.INSTALL_FAILED_CPU_ABI_INCOMPATIBLE: - s = "INSTALL_FAILED_CPU_ABI_INCOMPATIBLE"; - break; - case PackageManager.INSTALL_PARSE_FAILED_NOT_APK: - s = "INSTALL_PARSE_FAILED_NOT_APK"; - break; - case PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST: - s = "INSTALL_PARSE_FAILED_BAD_MANIFEST"; - break; - case PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION: - s = "INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION"; - break; - case PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES: - s = "INSTALL_PARSE_FAILED_NO_CERTIFICATES"; - break; - case PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES: - s = "INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES"; - break; - case PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING: - s = "INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING"; - break; - case PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME: - s = "INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME"; - break; - case PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID: - s = "INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID"; - break; - case PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: - s = "INSTALL_PARSE_FAILED_MANIFEST_MALFORMED"; - break; - case PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY: - s = "INSTALL_PARSE_FAILED_MANIFEST_EMPTY"; - break; - default: - s = Integer.toString(result); - break; - } - return s; + Field[] fields = PackageManager.class.getFields(); + for (Field f: fields) { + if (f.getType() == int.class) { + int modifiers = f.getModifiers(); + // only look at public final static fields. + if (((modifiers & Modifier.FINAL) != 0) && + ((modifiers & Modifier.PUBLIC) != 0) && + ((modifiers & Modifier.STATIC) != 0)) { + String fieldName = f.getName(); + if (fieldName.startsWith("INSTALL_FAILED_") || + fieldName.startsWith("INSTALL_PARSE_FAILED_")) { + // get the int value and compare it to result. + try { + if (result == f.getInt(null)) { + return fieldName; + } + } catch (IllegalAccessException e) { + // this shouldn't happen since we only look for public static fields. + } + } + } + } + } + + // couldn't find a matching constant? return the value + return Integer.toString(result); } - + private void runInstall() { int installFlags = 0; String installerPackageName = null; @@ -621,7 +613,7 @@ public final class Pm { try { mPm.installPackage(Uri.fromFile(new File(apkFilePath)), obs, installFlags, installerPackageName); - + synchronized (obs) { while (!obs.finished) { try { @@ -642,11 +634,11 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + class PackageDeleteObserver extends IPackageDeleteObserver.Stub { boolean finished; boolean result; - + public void packageDeleted(boolean succeeded) { synchronized (this) { finished = true; @@ -655,7 +647,7 @@ public final class Pm { } } } - + private void runUninstall() { int unInstallFlags = 0; @@ -709,7 +701,7 @@ public final class Pm { } return "unknown"; } - + private void runSetEnabledSetting(int state) { String pkg = nextArg(); if (pkg == null) { @@ -757,11 +749,11 @@ public final class Pm { System.err.println(PM_NOT_RUNNING_ERR); } } - + private Resources getResources(PackageItemInfo pii) { Resources res = mResourceCache.get(pii.packageName); if (res != null) return res; - + try { ApplicationInfo ai = mPm.getApplicationInfo(pii.packageName, 0); AssetManager am = new AssetManager(); @@ -775,7 +767,7 @@ public final class Pm { return null; } } - + private String nextOption() { if (mNextArg >= mArgs.length) { return null; @@ -827,7 +819,8 @@ public final class Pm { System.err.println(" pm list packages [-f]"); System.err.println(" pm list permission-groups"); System.err.println(" pm list permissions [-g] [-f] [-d] [-u] [GROUP]"); - System.err.println(" pm list instrumentation [-f] [TARGET-PACKAGE]"); + System.err.println(" pm list instrumentation [-f] [TARGET-PACKAGE]"); + System.err.println(" pm list features"); System.err.println(" pm path PACKAGE"); System.err.println(" pm install [-l] [-r] [-t] [-i INSTALLER_PACKAGE_NAME] PATH"); System.err.println(" pm uninstall [-k] PACKAGE"); @@ -852,6 +845,8 @@ public final class Pm { System.err.println("or only those that target a specified package. Options:"); System.err.println(" -f: see their associated file."); System.err.println(""); + System.err.println("The list features command prints all features of the system."); + System.err.println(""); System.err.println("The path command prints the path to the .apk of a package."); System.err.println(""); System.err.println("The install command installs a package to the system. Options:"); diff --git a/cmds/runtime/Android.mk b/cmds/runtime/Android.mk index 521eb2b2863eaa8eb24ec432723355eb65f2d942..6a72d1070850c69eb50614ec506d8faf4f161951 100644 --- a/cmds/runtime/Android.mk +++ b/cmds/runtime/Android.mk @@ -10,6 +10,7 @@ LOCAL_SRC_FILES:= \ LOCAL_SHARED_LIBRARIES := \ libutils \ + libbinder \ libandroid_runtime \ libcutils \ libui \ diff --git a/cmds/runtime/ServiceManager.cpp b/cmds/runtime/ServiceManager.cpp index 758a95c07d56a71bbe664204de50cdb29bfb62b3..b2bef07ff70e7c992e8d11ba1887e1739dc9f47d 100644 --- a/cmds/runtime/ServiceManager.cpp +++ b/cmds/runtime/ServiceManager.cpp @@ -9,9 +9,9 @@ #include #include -#include +#include #include -#include +#include #include diff --git a/cmds/runtime/ServiceManager.h b/cmds/runtime/ServiceManager.h index d09cec8ddad8c60bd1dc86b28cdfd74a068a24d4..090ca6de648b22772e5fce02e19e0d3228ef8f30 100644 --- a/cmds/runtime/ServiceManager.h +++ b/cmds/runtime/ServiceManager.h @@ -4,7 +4,7 @@ #ifndef ANDROID_SERVICE_MANAGER_H #define ANDROID_SERVICE_MANAGER_H -#include +#include #include #include diff --git a/cmds/runtime/main_runtime.cpp b/cmds/runtime/main_runtime.cpp index 476f38a4d073eb5f943da84486b64bbe95613ac6..21e0e4d02662c55a73122f9937f647dca266c18c 100644 --- a/cmds/runtime/main_runtime.cpp +++ b/cmds/runtime/main_runtime.cpp @@ -7,9 +7,11 @@ #include "ServiceManager.h" #include "SignalHandler.h" -#include -#include -#include +#include +#include + +#include +#include #include #include diff --git a/cmds/service/Android.mk b/cmds/service/Android.mk index 8c5005c1fbb13a4a2d5b7dcda0c44b4a7d844ecb..275bbb2e17be50dc8f275423fd86a7628e76c7fc 100644 --- a/cmds/service/Android.mk +++ b/cmds/service/Android.mk @@ -4,8 +4,7 @@ include $(CLEAR_VARS) LOCAL_SRC_FILES:= \ service.cpp -LOCAL_SHARED_LIBRARIES := \ - libutils +LOCAL_SHARED_LIBRARIES := libutils libbinder ifeq ($(TARGET_OS),linux) LOCAL_CFLAGS += -DXP_UNIX diff --git a/cmds/service/service.cpp b/cmds/service/service.cpp index 859a9bf9f93ccf82aeece9b038279b4ca6e55dcd..32db83ba1dcb19b160ab76e4da78392e5cbdb2a8 100644 --- a/cmds/service/service.cpp +++ b/cmds/service/service.cpp @@ -3,9 +3,9 @@ * */ -#include -#include -#include +#include +#include +#include #include #include diff --git a/cmds/servicemanager/service_manager.c b/cmds/servicemanager/service_manager.c index e4aa8b51cf2a7a1587acc1d3e6a637b9e2ef19fb..f3a471394b6a5d2d57a685ca1127650f65187ced 100644 --- a/cmds/servicemanager/service_manager.c +++ b/cmds/servicemanager/service_manager.c @@ -30,6 +30,7 @@ static struct { { AID_MEDIA, "media.audio_flinger" }, { AID_MEDIA, "media.player" }, { AID_MEDIA, "media.camera" }, + { AID_MEDIA, "media.audio_policy" }, { AID_RADIO, "radio.phone" }, { AID_RADIO, "radio.sms" }, { AID_RADIO, "radio.phonesubinfo" }, diff --git a/cmds/stagefright/Android.mk b/cmds/stagefright/Android.mk new file mode 100644 index 0000000000000000000000000000000000000000..5b5525240f02dd469596d648d6599caa8f2c9a8d --- /dev/null +++ b/cmds/stagefright/Android.mk @@ -0,0 +1,46 @@ +ifeq ($(BUILD_WITH_FULL_STAGEFRIGHT),true) + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + stagefright.cpp + +LOCAL_SHARED_LIBRARIES := \ + libstagefright + +LOCAL_C_INCLUDES:= \ + $(JNI_H_INCLUDE) \ + frameworks/base/media/libstagefright \ + $(TOP)/external/opencore/extern_libs_v2/khronos/openmax/include + +LOCAL_CFLAGS += -Wno-multichar + +LOCAL_MODULE:= stagefright + +include $(BUILD_EXECUTABLE) + +################################################################################ + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + SineSource.cpp \ + record.cpp + +LOCAL_SHARED_LIBRARIES := \ + libstagefright + +LOCAL_C_INCLUDES:= \ + $(JNI_H_INCLUDE) \ + frameworks/base/media/libstagefright \ + $(TOP)/external/opencore/extern_libs_v2/khronos/openmax/include + +LOCAL_CFLAGS += -Wno-multichar + +LOCAL_MODULE:= record + +include $(BUILD_EXECUTABLE) + +endif diff --git a/cmds/stagefright/SineSource.cpp b/cmds/stagefright/SineSource.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e5a6ccb6b32a2998fc874b93cac226ae2c786788 --- /dev/null +++ b/cmds/stagefright/SineSource.cpp @@ -0,0 +1,101 @@ +#include "SineSource.h" + +#include + +#include +#include +#include +#include + +namespace android { + +SineSource::SineSource(int32_t sampleRate, int32_t numChannels) + : mStarted(false), + mSampleRate(sampleRate), + mNumChannels(numChannels), + mPhase(0), + mGroup(NULL) { + CHECK(numChannels == 1 || numChannels == 2); +} + +SineSource::~SineSource() { + if (mStarted) { + stop(); + } +} + +status_t SineSource::start(MetaData *params) { + CHECK(!mStarted); + + mGroup = new MediaBufferGroup; + mGroup->add_buffer(new MediaBuffer(kBufferSize)); + + mPhase = 0; + mStarted = true; + + return OK; +} + +status_t SineSource::stop() { + CHECK(mStarted); + + delete mGroup; + mGroup = NULL; + + mStarted = false; + + return OK; +} + +sp SineSource::getFormat() { + sp meta = new MetaData; + meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_AUDIO_RAW); + meta->setInt32(kKeyChannelCount, mNumChannels); + meta->setInt32(kKeySampleRate, mSampleRate); + + return meta; +} + +status_t SineSource::read( + MediaBuffer **out, const ReadOptions *options) { + *out = NULL; + + MediaBuffer *buffer; + status_t err = mGroup->acquire_buffer(&buffer); + + if (err != OK) { + return err; + } + + size_t frameSize = mNumChannels * sizeof(int16_t); + size_t numFramesPerBuffer = buffer->size() / frameSize; + + int16_t *ptr = (int16_t *)buffer->data(); + + const double k = kFrequency / mSampleRate * (2.0 * M_PI); + + double x = mPhase * k; + for (size_t i = 0; i < numFramesPerBuffer; ++i) { + int16_t amplitude = (int16_t)(32767.0 * sin(x)); + + *ptr++ = amplitude; + if (mNumChannels == 2) { + *ptr++ = amplitude; + } + + x += k; + } + + buffer->meta_data()->setInt32(kKeyTimeUnits, mPhase); + buffer->meta_data()->setInt32(kKeyTimeScale, mSampleRate); + + mPhase += numFramesPerBuffer; + + buffer->set_range(0, numFramesPerBuffer * frameSize); + + *out = buffer; + + return OK; +} + +} // namespace android diff --git a/cmds/stagefright/SineSource.h b/cmds/stagefright/SineSource.h new file mode 100644 index 0000000000000000000000000000000000000000..76ab669fbfc66ff39a78482b1fe0b1b01b51548d --- /dev/null +++ b/cmds/stagefright/SineSource.h @@ -0,0 +1,39 @@ +#ifndef SINE_SOURCE_H_ + +#define SINE_SOURCE_H_ + +#include + +namespace android { + +struct MediaBufferGroup; + +struct SineSource : public MediaSource { + SineSource(int32_t sampleRate, int32_t numChannels); + + virtual status_t start(MetaData *params); + virtual status_t stop(); + + virtual sp getFormat(); + + virtual status_t read( + MediaBuffer **out, const ReadOptions *options = NULL); + +protected: + virtual ~SineSource(); + +private: + enum { kBufferSize = 8192 }; + static const double kFrequency = 500.0; + + bool mStarted; + int32_t mSampleRate; + int32_t mNumChannels; + size_t mPhase; + + MediaBufferGroup *mGroup; +}; + +} // namespace android + +#endif // SINE_SOURCE_H_ diff --git a/cmds/stagefright/WaveWriter.h b/cmds/stagefright/WaveWriter.h new file mode 100644 index 0000000000000000000000000000000000000000..a0eb66e2b9bcd504f0d93de5f131e7dcbbcc2378 --- /dev/null +++ b/cmds/stagefright/WaveWriter.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#ifndef ANDROID_WAVEWRITER_H_ + +#define ANDROID_WAVEWRITER_H_ + +namespace android { + +class WaveWriter { +public: + WaveWriter(const char *filename, + uint16_t num_channels, uint32_t sampling_rate) + : mFile(fopen(filename, "wb")), + mTotalBytes(0) { + fwrite("RIFFxxxxWAVEfmt \x10\x00\x00\x00\x01\x00", 1, 22, mFile); + write_u16(num_channels); + write_u32(sampling_rate); + write_u32(sampling_rate * num_channels * 2); + write_u16(num_channels * 2); + write_u16(16); + fwrite("dataxxxx", 1, 8, mFile); + } + + ~WaveWriter() { + fseek(mFile, 40, SEEK_SET); + write_u32(mTotalBytes); + + fseek(mFile, 4, SEEK_SET); + write_u32(36 + mTotalBytes); + + fclose(mFile); + mFile = NULL; + } + + void Append(const void *data, size_t size) { + fwrite(data, 1, size, mFile); + mTotalBytes += size; + } + +private: + void write_u16(uint16_t x) { + fputc(x & 0xff, mFile); + fputc(x >> 8, mFile); + } + + void write_u32(uint32_t x) { + write_u16(x & 0xffff); + write_u16(x >> 16); + } + + FILE *mFile; + size_t mTotalBytes; +}; + +} // namespace android + +#endif // ANDROID_WAVEWRITER_H_ diff --git a/cmds/stagefright/record.cpp b/cmds/stagefright/record.cpp new file mode 100644 index 0000000000000000000000000000000000000000..323d448663f9f85cd0fb3ae135195573936c84b6 --- /dev/null +++ b/cmds/stagefright/record.cpp @@ -0,0 +1,265 @@ +/* + * 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. + */ + +#include "SineSource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace android; + +#if 0 +class DummySource : public MediaSource { +public: + DummySource(int width, int height) + : mWidth(width), + mHeight(height), + mSize((width * height * 3) / 2) { + mGroup.add_buffer(new MediaBuffer(mSize)); + } + + virtual sp getFormat() { + sp meta = new MetaData; + meta->setInt32(kKeyWidth, mWidth); + meta->setInt32(kKeyHeight, mHeight); + meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_RAW); + + return meta; + } + + virtual status_t start(MetaData *params) { + return OK; + } + + virtual status_t stop() { + return OK; + } + + virtual status_t read( + MediaBuffer **buffer, const MediaSource::ReadOptions *options) { + status_t err = mGroup.acquire_buffer(buffer); + if (err != OK) { + return err; + } + + char x = (char)((double)rand() / RAND_MAX * 255); + memset((*buffer)->data(), x, mSize); + (*buffer)->set_range(0, mSize); + + return OK; + } + +protected: + virtual ~DummySource() {} + +private: + MediaBufferGroup mGroup; + int mWidth, mHeight; + size_t mSize; + + DummySource(const DummySource &); + DummySource &operator=(const DummySource &); +}; + +sp createSource(const char *filename) { + sp source; + + sp extractor = + new MPEG4Extractor(new MmapSource(filename)); + + size_t num_tracks = extractor->countTracks(); + + sp meta; + for (size_t i = 0; i < num_tracks; ++i) { + meta = extractor->getTrackMetaData(i); + CHECK(meta.get() != NULL); + + const char *mime; + if (!meta->findCString(kKeyMIMEType, &mime)) { + continue; + } + + if (strncasecmp(mime, "video/", 6)) { + continue; + } + + source = extractor->getTrack(i); + break; + } + + return source; +} + +int main(int argc, char **argv) { + android::ProcessState::self()->startThreadPool(); + +#if 1 + if (argc != 2) { + fprintf(stderr, "usage: %s filename\n", argv[0]); + return 1; + } + + OMXClient client; + CHECK_EQ(client.connect(), OK); + +#if 0 + sp source = createSource(argv[1]); + + if (source == NULL) { + fprintf(stderr, "Unable to find a suitable video track.\n"); + return 1; + } + + sp meta = source->getFormat(); + + sp decoder = OMXCodec::Create( + client.interface(), meta, false /* createEncoder */, source); + + int width, height; + bool success = meta->findInt32(kKeyWidth, &width); + success = success && meta->findInt32(kKeyHeight, &height); + CHECK(success); +#else + int width = 320; + int height = 240; + sp decoder = new DummySource(width, height); +#endif + + sp enc_meta = new MetaData; + // enc_meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_H263); + enc_meta->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_MPEG4); + enc_meta->setInt32(kKeyWidth, width); + enc_meta->setInt32(kKeyHeight, height); + + sp encoder = + OMXCodec::Create( + client.interface(), enc_meta, true /* createEncoder */, decoder); + +#if 0 + sp writer = new MPEG4Writer("/sdcard/output.mp4"); + writer->addSource(enc_meta, encoder); + writer->start(); + sleep(20); + printf("stopping now.\n"); + writer->stop(); +#else + encoder->start(); + + MediaBuffer *buffer; + while (encoder->read(&buffer) == OK) { + printf("got an output frame of size %d\n", buffer->range_length()); + + buffer->release(); + buffer = NULL; + } + + encoder->stop(); +#endif + + client.disconnect(); +#endif + +#if 0 + CameraSource *source = CameraSource::Create(); + printf("source = %p\n", source); + + for (int i = 0; i < 100; ++i) { + MediaBuffer *buffer; + status_t err = source->read(&buffer); + CHECK_EQ(err, OK); + + printf("got a frame, data=%p, size=%d\n", + buffer->data(), buffer->range_length()); + + buffer->release(); + buffer = NULL; + } + + delete source; + source = NULL; +#endif + + return 0; +} +#else + +int main(int argc, char **argv) { + android::ProcessState::self()->startThreadPool(); + + OMXClient client; + CHECK_EQ(client.connect(), OK); + + const int32_t kSampleRate = 22050; + const int32_t kNumChannels = 2; + sp audioSource = new SineSource(kSampleRate, kNumChannels); + +#if 0 + sp audioSink; + AudioPlayer *player = new AudioPlayer(audioSink); + player->setSource(audioSource); + player->start(); + + sleep(10); + + player->stop(); +#endif + + sp encMeta = new MetaData; + encMeta->setCString(kKeyMIMEType, + 1 ? MEDIA_MIMETYPE_AUDIO_AMR_WB : MEDIA_MIMETYPE_AUDIO_AAC); + encMeta->setInt32(kKeySampleRate, kSampleRate); + encMeta->setInt32(kKeyChannelCount, kNumChannels); + encMeta->setInt32(kKeyMaxInputSize, 8192); + + sp encoder = + OMXCodec::Create(client.interface(), encMeta, true, audioSource); + + encoder->start(); + + int32_t n = 0; + status_t err; + MediaBuffer *buffer; + while ((err = encoder->read(&buffer)) == OK) { + printf("."); + fflush(stdout); + + buffer->release(); + buffer = NULL; + + if (++n == 100) { + break; + } + } + printf("$\n"); + + encoder->stop(); + + client.disconnect(); + + return 0; +} +#endif diff --git a/cmds/stagefright/stagefright.cpp b/cmds/stagefright/stagefright.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4ffc8e437551b25eda5f003fb7267b75de3d004d --- /dev/null +++ b/cmds/stagefright/stagefright.cpp @@ -0,0 +1,402 @@ +/* + * 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. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace android; + +static long gNumRepetitions; +static long gMaxNumFrames; // 0 means decode all available. +static long gReproduceBug; // if not -1. + +static int64_t getNowUs() { + struct timeval tv; + gettimeofday(&tv, NULL); + + return (int64_t)tv.tv_usec + tv.tv_sec * 1000000; +} + +static void playSource(OMXClient *client, const sp &source) { + sp meta = source->getFormat(); + + int32_t durationUnits; + int32_t timeScale; + CHECK(meta->findInt32(kKeyDuration, &durationUnits)); + CHECK(meta->findInt32(kKeyTimeScale, &timeScale)); + + int64_t durationUs = ((int64_t)durationUnits * 1000000) / timeScale; + + sp decoder = OMXCodec::Create( + client->interface(), meta, false /* createEncoder */, source); + + if (decoder == NULL) { + return; + } + + decoder->start(); + + if (gReproduceBug >= 3 && gReproduceBug <= 5) { + status_t err; + MediaBuffer *buffer; + MediaSource::ReadOptions options; + int64_t seekTimeUs = -1; + for (;;) { + err = decoder->read(&buffer, &options); + options.clearSeekTo(); + + bool shouldSeek = false; + if (err != OK) { + printf("reached EOF.\n"); + + shouldSeek = true; + } else { + int32_t timestampUnits; + CHECK(buffer->meta_data()->findInt32(kKeyTimeUnits, ×tampUnits)); + + int64_t timestampUs = ((int64_t)timestampUnits * 1000000) / timeScale; + + bool failed = false; + + if (seekTimeUs >= 0) { + int64_t diff = timestampUs - seekTimeUs; + if (diff < 0) { + diff = -diff; + } + + if ((gReproduceBug == 4 && diff > 500000) + || (gReproduceBug == 5 && timestampUs < 0)) { + printf("wanted: %.2f secs, got: %.2f secs\n", + seekTimeUs / 1E6, timestampUs / 1E6); + + printf("ERROR: "); + failed = true; + } + } + + printf("buffer has timestamp %lld us (%.2f secs)\n", + timestampUs, timestampUs / 1E6); + + buffer->release(); + buffer = NULL; + + if (failed) { + break; + } + + shouldSeek = ((double)rand() / RAND_MAX) < 0.1; + + if (gReproduceBug == 3) { + shouldSeek = false; + } + } + + seekTimeUs = -1; + + if (shouldSeek) { + seekTimeUs = (rand() * (float)durationUs) / RAND_MAX; + options.setSeekTo(seekTimeUs); + + printf("seeking to %lld us (%.2f secs)\n", + seekTimeUs, seekTimeUs / 1E6); + } + } + + decoder->stop(); + + return; + } + + int n = 0; + int64_t startTime = getNowUs(); + + long numIterationsLeft = gNumRepetitions; + MediaSource::ReadOptions options; + + while (numIterationsLeft-- > 0) { + long numFrames = 0; + + MediaBuffer *buffer; + + for (;;) { + status_t err = decoder->read(&buffer, &options); + options.clearSeekTo(); + + if (err != OK) { + CHECK_EQ(buffer, NULL); + + break; + } + + if ((n++ % 16) == 0) { + printf("."); + fflush(stdout); + } + + buffer->release(); + buffer = NULL; + + ++numFrames; + if (gMaxNumFrames > 0 && numFrames == gMaxNumFrames) { + break; + } + + if (gReproduceBug == 1 && numFrames == 40) { + printf("seeking past the end now."); + options.setSeekTo(0x7fffffffL); + } else if (gReproduceBug == 2 && numFrames == 40) { + printf("seeking to 5 secs."); + options.setSeekTo(5000000); + } + } + + printf("$"); + fflush(stdout); + + options.setSeekTo(0); + } + + decoder->stop(); + printf("\n"); + + int64_t delay = getNowUs() - startTime; + printf("avg. %.2f fps\n", n * 1E6 / delay); + + printf("decoded a total of %d frame(s).\n", n); +} + +static void usage(const char *me) { + fprintf(stderr, "usage: %s\n", me); + fprintf(stderr, " -h(elp)\n"); + fprintf(stderr, " -a(udio)\n"); + fprintf(stderr, " -n repetitions\n"); + fprintf(stderr, " -l(ist) components\n"); + fprintf(stderr, " -m max-number-of-frames-to-decode in each pass\n"); + fprintf(stderr, " -b bug to reproduce\n"); + fprintf(stderr, " -p(rofiles) dump decoder profiles supported\n"); +} + +int main(int argc, char **argv) { + android::ProcessState::self()->startThreadPool(); + + bool audioOnly = false; + bool listComponents = false; + bool dumpProfiles = false; + gNumRepetitions = 1; + gMaxNumFrames = 0; + gReproduceBug = -1; + + int res; + while ((res = getopt(argc, argv, "han:lm:b:p")) >= 0) { + switch (res) { + case 'a': + { + audioOnly = true; + break; + } + + case 'l': + { + listComponents = true; + break; + } + + case 'm': + case 'n': + case 'b': + { + char *end; + long x = strtol(optarg, &end, 10); + + if (*end != '\0' || end == optarg || x <= 0) { + x = 1; + } + + if (res == 'n') { + gNumRepetitions = x; + } else if (res == 'm') { + gMaxNumFrames = x; + } else { + CHECK_EQ(res, 'b'); + gReproduceBug = x; + } + break; + } + + case 'p': + { + dumpProfiles = true; + break; + } + + case '?': + case 'h': + default: + { + usage(argv[0]); + exit(1); + break; + } + } + } + + argc -= optind; + argv += optind; + + if (dumpProfiles) { + sp sm = defaultServiceManager(); + sp binder = sm->getService(String16("media.player")); + sp service = + interface_cast(binder); + + CHECK(service.get() != NULL); + + sp omx = service->getOMX(); + CHECK(omx.get() != NULL); + + const char *kMimeTypes[] = { + MEDIA_MIMETYPE_VIDEO_AVC, MEDIA_MIMETYPE_VIDEO_MPEG4, + MEDIA_MIMETYPE_VIDEO_H263 + }; + + for (size_t k = 0; k < sizeof(kMimeTypes) / sizeof(kMimeTypes[0]); + ++k) { + printf("type '%s':\n", kMimeTypes[k]); + + Vector results; + CHECK_EQ(QueryCodecs(omx, kMimeTypes[k], + true, // queryDecoders + &results), OK); + + for (size_t i = 0; i < results.size(); ++i) { + printf(" decoder '%s' supports ", + results[i].mComponentName.string()); + + if (results[i].mProfileLevels.size() == 0) { + printf("NOTHING.\n"); + continue; + } + + for (size_t j = 0; j < results[i].mProfileLevels.size(); ++j) { + const CodecProfileLevel &profileLevel = + results[i].mProfileLevels[j]; + + printf("%s%ld/%ld", j > 0 ? ", " : "", + profileLevel.mProfile, profileLevel.mLevel); + } + + printf("\n"); + } + } + } + + if (listComponents) { + sp sm = defaultServiceManager(); + sp binder = sm->getService(String16("media.player")); + sp service = interface_cast(binder); + + CHECK(service.get() != NULL); + + sp omx = service->getOMX(); + CHECK(omx.get() != NULL); + + List list; + omx->listNodes(&list); + + for (List::iterator it = list.begin(); + it != list.end(); ++it) { + printf("%s\n", (*it).string()); + } + } + + DataSource::RegisterDefaultSniffers(); + + OMXClient client; + status_t err = client.connect(); + + for (int k = 0; k < argc; ++k) { + const char *filename = argv[k]; + + sp dataSource; + if (!strncasecmp("http://", filename, 7)) { + dataSource = new HTTPDataSource(filename); + dataSource = new CachingDataSource(dataSource, 64 * 1024, 10); + } else { + dataSource = new MmapSource(filename); + } + + bool isJPEG = false; + + size_t len = strlen(filename); + if (len >= 4 && !strcasecmp(filename + len - 4, ".jpg")) { + isJPEG = true; + } + + sp mediaSource; + + if (isJPEG) { + mediaSource = new JPEGSource(dataSource); + } else { + sp extractor = MediaExtractor::Create(dataSource); + + size_t numTracks = extractor->countTracks(); + + sp meta; + size_t i; + for (i = 0; i < numTracks; ++i) { + meta = extractor->getTrackMetaData(i); + + const char *mime; + meta->findCString(kKeyMIMEType, &mime); + + if (audioOnly && !strncasecmp(mime, "audio/", 6)) { + break; + } + + if (!audioOnly && !strncasecmp(mime, "video/", 6)) { + break; + } + } + + mediaSource = extractor->getTrack(i); + } + + playSource(&client, mediaSource); + } + + client.disconnect(); + + return 0; +} diff --git a/cmds/surfaceflinger/Android.mk b/cmds/surfaceflinger/Android.mk index 37c3d942414a7bdff49e58b73c33579ff968537d..bfa58a1cbf7f80df2bf04748ae854885ca99644c 100644 --- a/cmds/surfaceflinger/Android.mk +++ b/cmds/surfaceflinger/Android.mk @@ -6,6 +6,7 @@ LOCAL_SRC_FILES:= \ LOCAL_SHARED_LIBRARIES := \ libsurfaceflinger \ + libbinder \ libutils LOCAL_C_INCLUDES := \ diff --git a/cmds/surfaceflinger/main_surfaceflinger.cpp b/cmds/surfaceflinger/main_surfaceflinger.cpp index 7c895783d7625e92a8c63b07971920921507b514..d65072132783249f9cae86f2e5d008651b8a0ff8 100644 --- a/cmds/surfaceflinger/main_surfaceflinger.cpp +++ b/cmds/surfaceflinger/main_surfaceflinger.cpp @@ -1,6 +1,6 @@ -#include -#include -#include +#include +#include +#include #include #include diff --git a/cmds/svc/src/com/android/commands/svc/PowerCommand.java b/cmds/svc/src/com/android/commands/svc/PowerCommand.java index 2b54f549f3894b3c17a6186523557d2cb8772a7d..e021012268c9d9b5b0a0acd7a54d40748f08197a 100644 --- a/cmds/svc/src/com/android/commands/svc/PowerCommand.java +++ b/cmds/svc/src/com/android/commands/svc/PowerCommand.java @@ -16,7 +16,10 @@ package com.android.commands.svc; +import android.os.Binder; +import android.os.IBinder; import android.os.IPowerManager; +import android.os.PowerManager; import android.os.ServiceManager; import android.os.RemoteException; import android.os.BatteryManager; @@ -60,7 +63,10 @@ public class PowerCommand extends Svc.Command { IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE)); try { + IBinder lock = new Binder(); + pm.acquireWakeLock(PowerManager.FULL_WAKE_LOCK, lock, "svc power"); pm.setStayOnSetting(val); + pm.releaseWakeLock(lock); } catch (RemoteException e) { System.err.println("Faild to set setting: " + e); diff --git a/cmds/system_server/Android.mk b/cmds/system_server/Android.mk index 0a684e86b1c7cbf5d287c753dd4ecaf7c9416546..ad537977d9eb95eb70101f7d5de6fe170e07284c 100644 --- a/cmds/system_server/Android.mk +++ b/cmds/system_server/Android.mk @@ -6,6 +6,7 @@ LOCAL_SRC_FILES:= \ LOCAL_SHARED_LIBRARIES := \ libutils \ + libbinder \ libsystem_server LOCAL_C_INCLUDES := \ diff --git a/cmds/system_server/library/Android.mk b/cmds/system_server/library/Android.mk index 580331a695afb683c69f85d87441caf0445b7a0e..1813d3e5ab0c117e5f4572f1374e025109aec75c 100644 --- a/cmds/system_server/library/Android.mk +++ b/cmds/system_server/library/Android.mk @@ -20,6 +20,7 @@ LOCAL_SHARED_LIBRARIES := \ libcameraservice \ libmediaplayerservice \ libutils \ + libbinder \ libcutils LOCAL_MODULE:= libsystem_server diff --git a/cmds/system_server/library/system_init.cpp b/cmds/system_server/library/system_init.cpp index 73b23e27e3bd9371c212053fb9fd0a807e9dee59..1d57fdcc4c77e4ea7b0cb955050853bb303a8d32 100644 --- a/cmds/system_server/library/system_init.cpp +++ b/cmds/system_server/library/system_init.cpp @@ -8,15 +8,16 @@ #define LOG_TAG "sysproc" -#include -#include -#include +#include +#include +#include #include #include #include #include #include +#include #include #include @@ -80,6 +81,9 @@ extern "C" status_t system_init() // Start the camera service CameraService::instantiate(); + + // Start the audio policy service + AudioPolicyService::instantiate(); } // And now start the Android runtime. We have to do this bit diff --git a/cmds/system_server/system_main.cpp b/cmds/system_server/system_main.cpp index ca16e57006229435bea0ff5fdcc6fffb7f44bab2..543f650a4d228d3dc69e93604be83e188403fe6e 100644 --- a/cmds/system_server/system_main.cpp +++ b/cmds/system_server/system_main.cpp @@ -9,7 +9,7 @@ #define LOG_TAG "sysproc" -#include +#include #include #include diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 79bd6e7f55b1cba19316c7e8e5d7687f09e2cbe9..03346fec837f8387eed58982f641431869f61e2f 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -185,7 +185,7 @@ public abstract class AccessibilityService extends Service { private final HandlerCaller mCaller; - private AccessibilityService mTarget; + private final AccessibilityService mTarget; public IEventListenerWrapper(AccessibilityService context) { mTarget = context; @@ -211,8 +211,10 @@ public abstract class AccessibilityService extends Service { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT : AccessibilityEvent event = (AccessibilityEvent) message.obj; - mTarget.onAccessibilityEvent(event); - event.recycle(); + if (event != null) { + mTarget.onAccessibilityEvent(event); + event.recycle(); + } return; case DO_ON_INTERRUPT : mTarget.onInterrupt(); diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java new file mode 100644 index 0000000000000000000000000000000000000000..be2bdbefb59d0cc4f10fba4d9cb8b51822ba5650 --- /dev/null +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.RemoteException; +import android.os.Binder; +import android.os.IBinder; +import android.content.pm.PackageManager; +import android.content.Context; +import android.content.Intent; +import android.Manifest; + +/** + * Abstract base class for creating AccountAuthenticators. + * In order to be an authenticator one must extend this class, provider implementations for the + * abstract methods and write a service that returns the result of {@link #getIBinder()} + * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked + * with an intent with action {@link AccountManager#ACTION_AUTHENTICATOR_INTENT}. This service + * must specify the following intent filter and metadata tags in its AndroidManifest.xml file + *
+ *   <intent-filter>
+ *     <action android:name="android.accounts.AccountAuthenticator" />
+ *   </intent-filter>
+ *   <meta-data android:name="android.accounts.AccountAuthenticator"
+ *             android:resource="@xml/authenticator" />
+ * 
+ * The android:resource attribute must point to a resource that looks like: + *
+ * <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ *    android:accountType="typeOfAuthenticator"
+ *    android:icon="@drawable/icon"
+ *    android:smallIcon="@drawable/miniIcon"
+ *    android:label="@string/label"
+ *    android:accountPreferences="@xml/account_preferences"
+ * />
+ * 
+ * Replace the icons and labels with your own resources. The android:accountType + * attribute must be a string that uniquely identifies your authenticator and will be the same + * string that user will use when making calls on the {@link AccountManager} and it also + * corresponds to {@link Account#type} for your accounts. One user of the android:icon is the + * "Account & Sync" settings page and one user of the android:smallIcon is the Contact Application's + * tab panels. + *

+ * The preferences attribute points to an PreferenceScreen xml hierarchy that contains + * a list of PreferenceScreens that can be invoked to manage the authenticator. An example is: + *

+ * <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ *    <PreferenceCategory android:title="@string/title_fmt" />
+ *    <PreferenceScreen
+ *         android:key="key1"
+ *         android:title="@string/key1_action"
+ *         android:summary="@string/key1_summary">
+ *         <intent
+ *             android:action="key1.ACTION"
+ *             android:targetPackage="key1.package"
+ *             android:targetClass="key1.class" />
+ *     </PreferenceScreen>
+ * </PreferenceScreen>
+ * 
+ * + *

+ * The standard pattern for implementing any of the abstract methods is the following: + *

    + *
  • If the supplied arguments are enough for the authenticator to fully satisfy the request + * then it will do so and return a {@link Bundle} that contains the results. + *
  • If the authenticator needs information from the user to satisfy the request then it + * will create an {@link Intent} to an activity that will prompt the user for the information + * and then carry out the request. This intent must be returned in a Bundle as key + * {@link AccountManager#KEY_INTENT}. + *

    + * The activity needs to return the final result when it is complete so the Intent should contain + * the {@link AccountAuthenticatorResponse} as {@link AccountManager#KEY_ACCOUNT_MANAGER_RESPONSE}. + * The activity must then call {@link AccountAuthenticatorResponse#onResult} or + * {@link AccountAuthenticatorResponse#onError} when it is complete. + *

  • If the authenticator cannot synchronously process the request and return a result then it + * may choose to return null and then use the AccountManagerResponse to send the result + * when it has completed the request. + *
+ *

+ * The following descriptions of each of the abstract authenticator methods will not describe the + * possible asynchronous nature of the request handling and will instead just describe the input + * parameters and the expected result. + *

+ * When writing an activity to satisfy these requests one must pass in the AccountManagerResponse + * and return the result via that response when the activity finishes (or whenever else the + * activity author deems it is the correct time to respond). + * The {@link AccountAuthenticatorActivity} handles this, so one may wish to extend that when + * writing activities to handle these requests. + */ +public abstract class AbstractAccountAuthenticator { + private final Context mContext; + + public AbstractAccountAuthenticator(Context context) { + mContext = context; + } + + private class Transport extends IAccountAuthenticator.Stub { + public void addAccount(IAccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.addAccount( + new AccountAuthenticatorResponse(response), + accountType, authTokenType, requiredFeatures, options); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "addAccount not supported"); + } + } + + public void confirmCredentials(IAccountAuthenticatorResponse response, + Account account, Bundle options) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.confirmCredentials( + new AccountAuthenticatorResponse(response), account, options); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "confirmCredentials not supported"); + } + } + + public void getAuthTokenLabel(IAccountAuthenticatorResponse response, + String authTokenType) + throws RemoteException { + checkBinderPermission(); + try { + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, + AbstractAccountAuthenticator.this.getAuthTokenLabel(authTokenType)); + response.onResult(result); + } catch (IllegalArgumentException e) { + response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS, + "unknown authTokenType"); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAuthTokenTypeLabel not supported"); + } + } + + public void getAuthToken(IAccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle loginOptions) + throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.getAuthToken( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAuthToken not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + + public void updateCredentials(IAccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle loginOptions) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.updateCredentials( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + if (result != null) { + response.onResult(result); + } + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "updateCredentials not supported"); + } + } + + public void editProperties(IAccountAuthenticatorResponse response, + String accountType) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.editProperties( + new AccountAuthenticatorResponse(response), accountType); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "editProperties not supported"); + } + } + + public void hasFeatures(IAccountAuthenticatorResponse response, + Account account, String[] features) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.hasFeatures( + new AccountAuthenticatorResponse(response), account, features); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "hasFeatures not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + + public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response, + Account account) throws RemoteException { + checkBinderPermission(); + try { + final Bundle result = AbstractAccountAuthenticator.this.getAccountRemovalAllowed( + new AccountAuthenticatorResponse(response), account); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAccountRemovalAllowed not supported"); + } catch (NetworkErrorException e) { + response.onError(AccountManager.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + } + + private void checkBinderPermission() { + final int uid = Binder.getCallingUid(); + final String perm = Manifest.permission.ACCOUNT_MANAGER; + if (mContext.checkCallingOrSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("caller uid " + uid + " lacks " + perm); + } + } + + private Transport mTransport = new Transport(); + + /** + * @return the IBinder for the AccountAuthenticator + */ + public final IBinder getIBinder() { + return mTransport.asBinder(); + } + + /** + * Returns a Bundle that contains the Intent of the activity that can be used to edit the + * properties. In order to indicate success the activity should call response.setResult() + * with a non-null Bundle. + * @param response used to set the result for the request. If the Constants.INTENT_KEY + * is set in the bundle then this response field is to be used for sending future + * results if and when the Intent is started. + * @param accountType the AccountType whose properties are to be edited. + * @return a Bundle containing the result or the Intent to start to continue the request. + * If this is null then the request is considered to still be active and the result should + * sent later using response. + */ + public abstract Bundle editProperties(AccountAuthenticatorResponse response, + String accountType); + + /** + * Adds an account of the specified accountType. + * @param response to send the result back to the AccountManager, will never be null + * @param accountType the type of account to add, will never be null + * @param authTokenType the type of auth token to retrieve after adding the account, may be null + * @param requiredFeatures a String array of authenticator-specific features that the added + * account must support, may be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *

    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of + * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType + * was supplied, or + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws NetworkErrorException; + + /** + * Checks that the user knows the credentials of an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be checked, will never be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *
    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response, + Account account, Bundle options) + throws NetworkErrorException; + /** + * Gets the authtoken for an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be retrieved, will never be null + * @param authTokenType the type of auth token to retrieve, will never be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *
    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_ACCOUNT_NAME}, {@link AccountManager#KEY_ACCOUNT_TYPE}, + * and {@link AccountManager#KEY_AUTHTOKEN}, or + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle getAuthToken(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) + throws NetworkErrorException; + + /** + * Ask the authenticator for a localized label for the given authTokenType. + * @param authTokenType the authTokenType whose label is to be returned, will never be null + * @return the localized label of the auth token type, may be null if the type isn't known + */ + public abstract String getAuthTokenLabel(String authTokenType); + + /** + * Update the locally stored credentials for an account. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account whose credentials are to be updated, will never be null + * @param authTokenType the type of auth token to retrieve after updating the credentials, + * may be null + * @param options a Bundle of authenticator-specific options, may be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *
    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of + * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType + * was supplied, or + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle updateCredentials(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle options) throws NetworkErrorException; + + /** + * Checks if the account supports all the specified authenticator specific features. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account to check, will never be null + * @param features an array of features to check, will never be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *
    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features, + * false otherwise + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public abstract Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, String[] features) throws NetworkErrorException; + + /** + * Checks if the removal of this account is allowed. + * @param response to send the result back to the AccountManager, will never be null + * @param account the account to check, will never be null + * @return a Bundle result or null if the result is to be returned via the response. The result + * will contain either: + *
    + *
  • {@link AccountManager#KEY_INTENT}, or + *
  • {@link AccountManager#KEY_BOOLEAN_RESULT}, true if the removal of the account is + * allowed, false otherwise + *
  • {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to + * indicate an error + *
+ * @throws NetworkErrorException if the authenticator could not honor the request due to a + * network error + */ + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, + Account account) throws NetworkErrorException { + final Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); + return result; + } +} diff --git a/core/java/android/accounts/Account.aidl b/core/java/android/accounts/Account.aidl new file mode 100644 index 0000000000000000000000000000000000000000..8752d9924c79a83d8966f2707668c248c7ec6120 --- /dev/null +++ b/core/java/android/accounts/Account.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +parcelable Account; diff --git a/core/java/android/accounts/Account.java b/core/java/android/accounts/Account.java new file mode 100644 index 0000000000000000000000000000000000000000..7b83a3076db336d53bb8d3eef65eb799ed96ff39 --- /dev/null +++ b/core/java/android/accounts/Account.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Parcelable; +import android.os.Parcel; +import android.text.TextUtils; + +/** + * Value type that represents an Account in the {@link AccountManager}. This object is + * {@link Parcelable} and also overrides {@link #equals} and {@link #hashCode}, making it + * suitable for use as the key of a {@link java.util.Map} + */ +public class Account implements Parcelable { + public final String name; + public final String type; + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Account)) return false; + final Account other = (Account)o; + return name.equals(other.name) && type.equals(other.type); + } + + public int hashCode() { + int result = 17; + result = 31 * result + name.hashCode(); + result = 31 * result + type.hashCode(); + return result; + } + + public Account(String name, String type) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("the name must not be empty: " + name); + } + if (TextUtils.isEmpty(type)) { + throw new IllegalArgumentException("the type must not be empty: " + type); + } + this.name = name; + this.type = type; + } + + public Account(Parcel in) { + this.name = in.readString(); + this.type = in.readString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(type); + } + + public static final Creator CREATOR = new Creator() { + public Account createFromParcel(Parcel source) { + return new Account(source); + } + + public Account[] newArray(int size) { + return new Account[size]; + } + }; + + public String toString() { + return "Account {name=" + name + ", type=" + type + "}"; + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..5cce6da6d0ca60077a4cd349a9594543274beb01 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * Base class for implementing an Activity that is used to help implement an + * AbstractAccountAuthenticator. If the AbstractAccountAuthenticator needs to use an activity + * to handle the request then it can have the activity extend AccountAuthenticatorActivity. + * The AbstractAccountAuthenticator passes in the response to the intent using the following: + *
+ *      intent.putExtra(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY, response);
+ * 
+ * The activity then sets the result that is to be handed to the response via + * {@link #setAccountAuthenticatorResult(android.os.Bundle)}. + * This result will be sent as the result of the request when the activity finishes. If this + * is never set or if it is set to null then error {@link AccountManager#ERROR_CODE_CANCELED} + * will be called on the response. + */ +public class AccountAuthenticatorActivity extends Activity { + private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null; + private Bundle mResultBundle = null; + + /** + * Set the result that is to be sent as the result of the request that caused this + * Activity to be launched. If result is null or this method is never called then + * the request will be canceled. + * @param result this is returned as the result of the AbstractAccountAuthenticator request + */ + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Retreives the AccountAuthenticatorResponse from either the intent of the icicle, if the + * icicle is non-zero. + * @param icicle the save instance data of this Activity, may be null + */ + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mAccountAuthenticatorResponse = + getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + + if (mAccountAuthenticatorResponse != null) { + mAccountAuthenticatorResponse.onRequestContinued(); + } + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + if (mAccountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + mAccountAuthenticatorResponse.onResult(mResultBundle); + } else { + mAccountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, + "canceled"); + } + mAccountAuthenticatorResponse = null; + } + super.finish(); + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorCache.java b/core/java/android/accounts/AccountAuthenticatorCache.java new file mode 100644 index 0000000000000000000000000000000000000000..d6c76a2a5d75412e5ffd6ef4dadc84db87e26ab7 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorCache.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.pm.PackageManager; +import android.content.pm.RegisteredServicesCache; +import android.content.pm.XmlSerializerAndParser; +import android.content.res.TypedArray; +import android.content.Context; +import android.util.AttributeSet; +import android.text.TextUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A cache of services that export the {@link IAccountAuthenticator} interface. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The authenticators are referred to by their account type and + * are made available via the {@link RegisteredServicesCache#getServiceInfo} method. + * @hide + */ +/* package private */ class AccountAuthenticatorCache + extends RegisteredServicesCache { + private static final String TAG = "Account"; + private static final MySerializer sSerializer = new MySerializer(); + + public AccountAuthenticatorCache(Context context) { + super(context, AccountManager.ACTION_AUTHENTICATOR_INTENT, + AccountManager.AUTHENTICATOR_META_DATA_NAME, + AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME, sSerializer); + } + + public AuthenticatorDescription parseServiceAttributes(String packageName, AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.AccountAuthenticator); + try { + final String accountType = + sa.getString(com.android.internal.R.styleable.AccountAuthenticator_accountType); + final int labelId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_label, 0); + final int iconId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_icon, 0); + final int smallIconId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_smallIcon, 0); + final int prefId = sa.getResourceId( + com.android.internal.R.styleable.AccountAuthenticator_accountPreferences, 0); + if (TextUtils.isEmpty(accountType)) { + return null; + } + return new AuthenticatorDescription(accountType, packageName, labelId, iconId, + smallIconId, prefId); + } finally { + sa.recycle(); + } + } + + private static class MySerializer implements XmlSerializerAndParser { + public void writeAsXml(AuthenticatorDescription item, XmlSerializer out) + throws IOException { + out.attribute(null, "type", item.type); + } + + public AuthenticatorDescription createFromXml(XmlPullParser parser) + throws IOException, XmlPullParserException { + return AuthenticatorDescription.newKey(parser.getAttributeValue(null, "type")); + } + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorResponse.java b/core/java/android/accounts/AccountAuthenticatorResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..7c09fbffb86ada96ec3bc0db4ef78b67513d4254 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorResponse.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.Parcelable; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Object used to communicate responses back to the AccountManager + */ +public class AccountAuthenticatorResponse implements Parcelable { + private IAccountAuthenticatorResponse mAccountAuthenticatorResponse; + + /** + * @hide + */ + /* package private */ AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) { + mAccountAuthenticatorResponse = response; + } + + public AccountAuthenticatorResponse(Parcel parcel) { + mAccountAuthenticatorResponse = + IAccountAuthenticatorResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mAccountAuthenticatorResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onRequestContinued() { + try { + mAccountAuthenticatorResponse.onRequestContinued(); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mAccountAuthenticatorResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mAccountAuthenticatorResponse.asBinder()); + } + + public static final Creator CREATOR = + new Creator() { + public AccountAuthenticatorResponse createFromParcel(Parcel source) { + return new AccountAuthenticatorResponse(source); + } + + public AccountAuthenticatorResponse[] newArray(int size) { + return new AccountAuthenticatorResponse[size]; + } + }; +} diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9765496762c031a668ebd5ac35ffc1452a4f19c9 --- /dev/null +++ b/core/java/android/accounts/AccountManager.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.app.Activity; +import android.content.Intent; +import android.content.Context; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.database.SQLException; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Map; + +import com.google.android.collect.Maps; + +/** + * A class that helps with interactions with the AccountManager Service. It provides + * methods to allow for account, password, and authtoken management for all accounts on the + * device. One accesses the {@link AccountManager} by calling: + *
+ *    AccountManager accountManager = AccountManager.get(context);
+ * 
+ * + *

+ * The AccountManager Service provides storage for the accounts known to the system, + * provides methods to manage them, and allows the registration of authenticators to + * which operations such as addAccount and getAuthToken are delegated. + *

+ * Many of the calls take an {@link AccountManagerCallback} and {@link Handler} as parameters. + * These calls return immediately but run asynchronously. If a callback is provided then + * {@link AccountManagerCallback#run} will be invoked wen the request completes, successfully + * or not. An {@link AccountManagerFuture} is returned by these requests and also passed into the + * callback. The result if retrieved by calling {@link AccountManagerFuture#getResult()} which + * either returns the result or throws an exception as appropriate. + *

+ * The asynchronous request can be made blocking by not providing a callback and instead + * calling {@link AccountManagerFuture#getResult()} on the future that is returned. This will + * cause the running thread to block until the result is returned. Keep in mind that one + * should not block the main thread in this way. Instead one should either use a callback, + * thus making the call asynchronous, or make the blocking call on a separate thread. + *

+ * If one wants to ensure that the callback is invoked from a specific handler then they should + * pass the handler to the request. This makes it easier to ensure thread-safety by running + * all of one's logic from a single handler. + */ +public class AccountManager { + private static final String TAG = "AccountManager"; + + public static final int ERROR_CODE_REMOTE_EXCEPTION = 1; + public static final int ERROR_CODE_NETWORK_ERROR = 3; + public static final int ERROR_CODE_CANCELED = 4; + public static final int ERROR_CODE_INVALID_RESPONSE = 5; + public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6; + public static final int ERROR_CODE_BAD_ARGUMENTS = 7; + public static final int ERROR_CODE_BAD_REQUEST = 8; + + public static final String KEY_ACCOUNTS = "accounts"; + public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types"; + public static final String KEY_USERDATA = "userdata"; + public static final String KEY_AUTHTOKEN = "authtoken"; + public static final String KEY_PASSWORD = "password"; + public static final String KEY_ACCOUNT_NAME = "authAccount"; + public static final String KEY_ACCOUNT_TYPE = "accountType"; + public static final String KEY_ERROR_CODE = "errorCode"; + public static final String KEY_ERROR_MESSAGE = "errorMessage"; + public static final String KEY_INTENT = "intent"; + public static final String KEY_BOOLEAN_RESULT = "booleanResult"; + public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse"; + public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse"; + public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage"; + public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey"; + public static final String ACTION_AUTHENTICATOR_INTENT = + "android.accounts.AccountAuthenticator"; + public static final String AUTHENTICATOR_META_DATA_NAME = + "android.accounts.AccountAuthenticator"; + public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator"; + + private final Context mContext; + private final IAccountManager mService; + private final Handler mMainHandler; + /** + * Action sent as a broadcast Intent by the AccountsService + * when accounts are added to and/or removed from the device's + * database. + */ + public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = + "android.accounts.LOGIN_ACCOUNTS_CHANGED"; + + /** + * @hide + */ + public AccountManager(Context context, IAccountManager service) { + mContext = context; + mService = service; + mMainHandler = new Handler(mContext.getMainLooper()); + } + + /** + * @hide used for testing only + */ + public AccountManager(Context context, IAccountManager service, Handler handler) { + mContext = context; + mService = service; + mMainHandler = handler; + } + + /** + * Retrieve an AccountManager instance that is associated with the context that is passed in. + * Certain calls such as {@link #addOnAccountsUpdatedListener} use this context internally, + * so the caller must take care to use a {@link Context} whose lifetime is associated with + * the listener registration. + * @param context The {@link Context} to use when necessary + * @return an {@link AccountManager} instance that is associated with context + */ + public static AccountManager get(Context context) { + return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); + } + + /** + * Get the password that is associated with the account. Returns null if the account does + * not exist. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + */ + public String getPassword(final Account account) { + try { + return mService.getPassword(account); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Get the user data named by "key" that is associated with the account. + * Returns null if the account does not exist or if it does not have a value for key. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + */ + public String getUserData(final Account account, final String key) { + try { + return mService.getUserData(account, key); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager Service for an array that contains a + * {@link AuthenticatorDescription} for each registered authenticator. + * @return an array that contains all the authenticators known to the AccountManager service. + * This array will be empty if there are no authenticators and will never return null. + *

+ * No permission is required to make this call. + */ + public AuthenticatorDescription[] getAuthenticatorTypes() { + try { + return mService.getAuthenticatorTypes(); + } catch (RemoteException e) { + // will never happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager Service for all accounts. + * @return an array that contains all the accounts known to the AccountManager service. + * This array will be empty if there are no accounts and will never return null. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} + */ + public Account[] getAccounts() { + try { + return mService.getAccounts(null); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Query the AccountManager for the set of accounts that have a given type. If null + * is passed as the type than all accounts are returned. + * @param type the account type by which to filter, or null to get all accounts + * @return an array that contains the accounts that match the specified type. This array + * will be empty if no accounts match. It will never return null. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} + */ + public Account[] getAccountsByType(String type) { + try { + return mService.getAccounts(type); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Add an account to the AccountManager's set of known accounts. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account The account to add + * @param password The password to associate with the account. May be null. + * @param userdata A bundle of key/value pairs to set as the account's userdata. May be null. + * @return true if the account was sucessfully added, false otherwise, for example, + * if the account already exists or if the account is null + */ + public boolean addAccountExplicitly(Account account, String password, Bundle userdata) { + try { + return mService.addAccount(account, password, userdata); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Removes the given account. If this account does not exist then this call has no effect. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The {@link Account} to remove + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Boolean} that is true if the account is successfully removed + * or false if the authenticator refuses to remove the account. + */ + public AccountManagerFuture removeAccount(final Account account, + AccountManagerCallback callback, Handler handler) { + return new Future2Task(handler, callback) { + public void doWork() throws RemoteException { + mService.removeAccount(mResponse, account); + } + public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException { + if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) { + throw new AuthenticatorException("no result in response"); + } + return bundle.getBoolean(KEY_BOOLEAN_RESULT); + } + }.start(); + } + + /** + * Removes the given authtoken. If this authtoken does not exist for the given account type + * then this call has no effect. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * @param accountType the account type of the authtoken to invalidate + * @param authToken the authtoken to invalidate + */ + public void invalidateAuthToken(final String accountType, final String authToken) { + try { + mService.invalidateAuthToken(accountType, authToken); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Gets the authtoken named by "authTokenType" for the specified account if it is cached + * by the AccountManager. If no authtoken is cached then null is returned rather than + * asking the authenticaticor to generate one. If the account or the + * authtoken do not exist then null is returned. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose authtoken is to be retrieved, must not be null + * @param authTokenType the type of authtoken to retrieve + * @return an authtoken for the given account and authTokenType, if one is cached by the + * AccountManager, null otherwise. + */ + public String peekAuthToken(final Account account, final String authTokenType) { + if (account == null) { + Log.e(TAG, "peekAuthToken: the account must not be null"); + return null; + } + if (authTokenType == null) { + return null; + } + try { + return mService.peekAuthToken(account, authTokenType); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the password for the account. The password may be null. If the account does not exist + * then this call has no affect. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose password is to be set. Must not be null. + * @param password the password to set for the account. May be null. + */ + public void setPassword(final Account account, final String password) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + try { + mService.setPassword(account, password); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the password for account to null. If the account does not exist then this call + * has no effect. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * @param account the account whose password is to be cleared. Must not be null. + */ + public void clearPassword(final Account account) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + try { + mService.clearPassword(account); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets account's userdata named "key" to the specified value. If the account does not + * exist then this call has no effect. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose userdata is to be set. Must not be null. + * @param key the key of the userdata to set. Must not be null. + * @param value the value to set. May be null. + */ + public void setUserData(final Account account, final String key, final String value) { + if (account == null) { + Log.e(TAG, "the account must not be null"); + return; + } + if (key == null) { + Log.e(TAG, "the key must not be null"); + return; + } + try { + mService.setUserData(account, key, value); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Sets the authtoken named by "authTokenType" to the value specified by authToken. + * If the account does not exist then this call has no effect. + *

+ * Requires that the caller has permission + * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running + * with the same UID as the Authenticator for the account. + * @param account the account whose authtoken is to be set. Must not be null. + * @param authTokenType the type of the authtoken to set. Must not be null. + * @param authToken the authToken to set. May be null. + */ + public void setAuthToken(Account account, final String authTokenType, final String authToken) { + try { + mService.setAuthToken(account, authTokenType, authToken); + } catch (RemoteException e) { + // won't ever happen + throw new RuntimeException(e); + } + } + + /** + * Convenience method that makes a blocking call to + * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)} + * then extracts and returns the value of {@link #KEY_AUTHTOKEN} from its result. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * @param account the account whose authtoken is to be retrieved, must not be null + * @param authTokenType the type of authtoken to retrieve + * @param notifyAuthFailure if true, cause the AccountManager to put up a "sign-on" notification + * for the account if no authtoken is cached by the AccountManager and the the authenticator + * does not have valid credentials to get an authtoken. + * @return an authtoken for the given account and authTokenType, if one is cached by the + * AccountManager, null otherwise. + * @throws AuthenticatorException if the authenticator is not present, unreachable or returns + * an invalid response. + * @throws OperationCanceledException if the request is canceled for any reason + * @throws java.io.IOException if the authenticator experiences an IOException while attempting + * to communicate with its backend server. + */ + public String blockingGetAuthToken(Account account, String authTokenType, + boolean notifyAuthFailure) + throws OperationCanceledException, IOException, AuthenticatorException { + Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */, + null /* handler */).getResult(); + return bundle.getString(KEY_AUTHTOKEN); + } + + /** + * Request that an authtoken of the specified type be returned for an account. + * If the Account Manager has a cached authtoken of the requested type then it will + * service the request itself. Otherwise it will pass the request on to the authenticator. + * The authenticator can try to service this request with information it already has stored + * in the AccountManager but may need to launch an activity to prompt the + * user to enter credentials. If it is able to retrieve the authtoken it will be returned + * in the result. + *

+ * If the authenticator needs to prompt the user for credentials it will return an intent to + * the activity that will do the prompting. If an activity is supplied then that activity + * will be used to launch the intent and the result will come from it. Otherwise a result will + * be returned that contains the intent. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains: + *

    + *
  • {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture getAuthToken( + final Account account, final String authTokenType, final Bundle options, + final Activity activity, AccountManagerCallback callback, Handler handler) { + if (activity == null) throw new IllegalArgumentException("activity is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + false /* notifyOnAuthFailure */, true /* expectActivityLaunch */, + options); + } + }.start(); + } + + /** + * Request that an authtoken of the specified type be returned for an account. + * If the Account Manager has a cached authtoken of the requested type then it will + * service the request itself. Otherwise it will pass the request on to the authenticator. + * The authenticator can try to service this request with information it already has stored + * in the AccountManager but may need to launch an activity to prompt the + * user to enter credentials. If it is able to retrieve the authtoken it will be returned + * in the result. + *

+ * If the authenticator needs to prompt the user for credentials it will return an intent for + * an activity that will do the prompting. If an intent is returned and notifyAuthFailure + * is true then a notification will be created that launches this intent. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param notifyAuthFailure if true and the authenticator returns a {@link #KEY_INTENT} in the + * result then a "sign-on needed" notification will be created that will launch this intent. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + *
  • {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} + * if the authenticator is able to retrieve the auth token + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture getAuthToken( + final Account account, final String authTokenType, final boolean notifyAuthFailure, + AccountManagerCallback callback, Handler handler) { + if (account == null) throw new IllegalArgumentException("account is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(null, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + notifyAuthFailure, false /* expectActivityLaunch */, null /* options */); + } + }.start(); + } + + /** + * Request that an account be added with the given accountType. This request + * is processed by the authenticator for the account type. If no authenticator is registered + * in the system then {@link AuthenticatorException} is thrown. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType The type of account to add. This must not be null. + * @param authTokenType The account that is added should be able to service this auth token + * type. This may be null. + * @param requiredFeatures The account that is added should support these features. + * This array may be null or empty. + * @param addAccountOptions A bundle of authenticator-specific options that is passed on + * to the authenticator. This may be null. + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, or + *
  • {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} + * and {@link #KEY_AUTHTOKEN} (if an authTokenType was specified). + *
+ */ + public AccountManagerFuture addAccount(final String accountType, + final String authTokenType, final String[] requiredFeatures, + final Bundle addAccountOptions, + final Activity activity, AccountManagerCallback callback, Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + if (accountType == null) { + Log.e(TAG, "the account must not be null"); + // to unblock caller waiting on Future.get() + set(new Bundle()); + return; + } + mService.addAcount(mResponse, accountType, authTokenType, + requiredFeatures, activity != null, addAccountOptions); + } + }.start(); + } + + public AccountManagerFuture getAccountsByTypeAndFeatures( + final String type, final String[] features, + AccountManagerCallback callback, Handler handler) { + return new Future2Task(handler, callback) { + public void doWork() throws RemoteException { + if (type == null) { + Log.e(TAG, "Type is null"); + set(new Account[0]); + return; + } + mService.getAccountsByFeatures(mResponse, type, features); + } + public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException { + if (!bundle.containsKey(KEY_ACCOUNTS)) { + throw new AuthenticatorException("no result in response"); + } + final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS); + Account[] descs = new Account[parcelables.length]; + for (int i = 0; i < parcelables.length; i++) { + descs[i] = (Account) parcelables[i]; + } + return descs; + } + }.start(); + } + + /** + * Requests that the authenticator checks that the user knows the credentials for the account. + * This is typically done by returning an intent to an activity that prompts the user to + * enter the credentials. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The account whose credentials are to be checked + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + *
  • {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct + * credentials + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture confirmCredentials(final Account account, + final Bundle options, + final Activity activity, + final AccountManagerCallback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.confirmCredentials(mResponse, account, options, activity != null); + } + }.start(); + } + + /** + * Requests that the authenticator update the the credentials for a user. This is typically + * done by returning an intent to an activity that will prompt the user to update the stored + * credentials for the account. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param account The account whose credentials are to be updated. + * @param authTokenType the auth token to retrieve as part of updating the credentials. + * May be null. + * @param options authenticator specific options for the request + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + *
  • {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct + * credentials, and optionally a {@link #KEY_AUTHTOKEN} if an authTokenType was provided. + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture updateCredentials(final Account account, + final String authTokenType, + final Bundle options, final Activity activity, + final AccountManagerCallback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.updateCredentials(mResponse, account, authTokenType, activity != null, + options); + } + }.start(); + } + + /** + * Request that the properties for an authenticator be updated. This is typically done by + * returning an intent to an activity that will allow the user to make changes. This request + * is processed by the authenticator for the account. If no matching authenticator is + * registered in the system then {@link AuthenticatorException} is thrown. + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType The account type of the authenticator whose properties are to be edited. + * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then + * the intent will be started with this activity. If activity is null then the result will + * be returned as-is. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials + *
  • nothing, returned if the edit completes successfully + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture editProperties(final String accountType, + final Activity activity, final AccountManagerCallback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.editProperties(mResponse, accountType, activity != null); + } + }.start(); + } + + private void ensureNotOnMainThread() { + final Looper looper = Looper.myLooper(); + if (looper != null && looper == mContext.getMainLooper()) { + // We really want to throw an exception here, but GTalkService exercises this + // path quite a bit and needs some serious rewrite in order to work properly. + //noinspection ThrowableInstanceNeverThrow +// Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs", +// new Exception()); + // TODO remove the log and throw this exception when the callers are fixed +// throw new IllegalStateException( +// "calling this from your main thread can lead to deadlock"); + } + } + + private void postToHandler(Handler handler, final AccountManagerCallback callback, + final AccountManagerFuture future) { + handler = handler == null ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + callback.run(future); + } + }); + } + + private void postToHandler(Handler handler, final OnAccountsUpdateListener listener, + final Account[] accounts) { + final Account[] accountsCopy = new Account[accounts.length]; + // send a copy to make sure that one doesn't + // change what another sees + System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length); + handler = (handler == null) ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + try { + listener.onAccountsUpdated(accountsCopy); + } catch (SQLException e) { + // Better luck next time. If the problem was disk-full, + // the STORAGE_OK intent will re-trigger the update. + Log.e(TAG, "Can't update accounts", e); + } + } + }); + } + + private abstract class AmsTask extends FutureTask implements AccountManagerFuture { + final IAccountManagerResponse mResponse; + final Handler mHandler; + final AccountManagerCallback mCallback; + final Activity mActivity; + public AmsTask(Activity activity, Handler handler, AccountManagerCallback callback) { + super(new Callable() { + public Bundle call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + + mHandler = handler; + mCallback = callback; + mActivity = activity; + mResponse = new Response(); + } + + public final AccountManagerFuture start() { + try { + doWork(); + } catch (RemoteException e) { + setException(e); + } + return this; + } + + public abstract void doWork() throws RemoteException; + + private Bundle internalGetResult(Long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + ensureNotOnMainThread(); + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (CancellationException e) { + throw new OperationCanceledException(); + } catch (TimeoutException e) { + // fall through and cancel + } catch (InterruptedException e) { + // fall through and cancel + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof UnsupportedOperationException) { + throw new AuthenticatorException(cause); + } else if (cause instanceof AuthenticatorException) { + throw (AuthenticatorException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + throw new OperationCanceledException(); + } + + public Bundle getResult() + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(null, null); + } + + public Bundle getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(timeout, unit); + } + + protected void done() { + if (mCallback != null) { + postToHandler(mHandler, mCallback, this); + } + } + + /** Handles the responses from the AccountManager */ + private class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + Intent intent = bundle.getParcelable("intent"); + if (intent != null && mActivity != null) { + // since the user provided an Activity we will silently start intents + // that we see + mActivity.startActivity(intent); + // leave the Future running to wait for the real response to this request + } else if (bundle.getBoolean("retry")) { + try { + doWork(); + } catch (RemoteException e) { + // this will only happen if the system process is dead, which means + // we will be dying ourselves + } + } else { + set(bundle); + } + } + + public void onError(int code, String message) { + if (code == ERROR_CODE_CANCELED) { + // the authenticator indicated that this request was canceled, do so now + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + + } + + private abstract class BaseFutureTask extends FutureTask { + final public IAccountManagerResponse mResponse; + final Handler mHandler; + + public BaseFutureTask(Handler handler) { + super(new Callable() { + public T call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + mHandler = handler; + mResponse = new Response(); + } + + public abstract void doWork() throws RemoteException; + + public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException; + + protected void postRunnableToHandler(Runnable runnable) { + Handler handler = (mHandler == null) ? mMainHandler : mHandler; + handler.post(runnable); + } + + protected void startTask() { + try { + doWork(); + } catch (RemoteException e) { + setException(e); + } + } + + protected class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + try { + T result = bundleToResult(bundle); + if (result == null) { + return; + } + set(result); + return; + } catch (ClassCastException e) { + // we will set the exception below + } catch (AuthenticatorException e) { + // we will set the exception below + } + onError(ERROR_CODE_INVALID_RESPONSE, "no result in response"); + } + + public void onError(int code, String message) { + if (code == ERROR_CODE_CANCELED) { + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + } + + private abstract class Future2Task + extends BaseFutureTask implements AccountManagerFuture { + final AccountManagerCallback mCallback; + public Future2Task(Handler handler, AccountManagerCallback callback) { + super(handler); + mCallback = callback; + } + + protected void done() { + if (mCallback != null) { + postRunnableToHandler(new Runnable() { + public void run() { + mCallback.run(Future2Task.this); + } + }); + } + } + + public Future2Task start() { + startTask(); + return this; + } + + private T internalGetResult(Long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + ensureNotOnMainThread(); + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (InterruptedException e) { + // fall through and cancel + } catch (TimeoutException e) { + // fall through and cancel + } catch (CancellationException e) { + // fall through and cancel + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof UnsupportedOperationException) { + throw new AuthenticatorException(cause); + } else if (cause instanceof AuthenticatorException) { + throw (AuthenticatorException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + throw new OperationCanceledException(); + } + + public T getResult() + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(null, null); + } + + public T getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(timeout, unit); + } + + } + + private Exception convertErrorToException(int code, String message) { + if (code == ERROR_CODE_NETWORK_ERROR) { + return new IOException(message); + } + + if (code == ERROR_CODE_UNSUPPORTED_OPERATION) { + return new UnsupportedOperationException(message); + } + + if (code == ERROR_CODE_INVALID_RESPONSE) { + return new AuthenticatorException(message); + } + + if (code == ERROR_CODE_BAD_ARGUMENTS) { + return new IllegalArgumentException(message); + } + + return new AuthenticatorException(message); + } + + private class GetAuthTokenByTypeAndFeaturesTask + extends AmsTask implements AccountManagerCallback { + GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType, + final String[] features, Activity activityForPrompting, + final Bundle addAccountOptions, final Bundle loginOptions, + AccountManagerCallback callback, Handler handler) { + super(activityForPrompting, handler, callback); + if (accountType == null) throw new IllegalArgumentException("account type is null"); + mAccountType = accountType; + mAuthTokenType = authTokenType; + mFeatures = features; + mAddAccountOptions = addAccountOptions; + mLoginOptions = loginOptions; + mMyCallback = this; + } + volatile AccountManagerFuture mFuture = null; + final String mAccountType; + final String mAuthTokenType; + final String[] mFeatures; + final Bundle mAddAccountOptions; + final Bundle mLoginOptions; + final AccountManagerCallback mMyCallback; + + public void doWork() throws RemoteException { + getAccountsByTypeAndFeatures(mAccountType, mFeatures, + new AccountManagerCallback() { + public void run(AccountManagerFuture future) { + Account[] accounts; + try { + accounts = future.getResult(); + } catch (OperationCanceledException e) { + setException(e); + return; + } catch (IOException e) { + setException(e); + return; + } catch (AuthenticatorException e) { + setException(e); + return; + } + + if (accounts.length == 0) { + if (mActivity != null) { + // no accounts, add one now. pretend that the user directly + // made this request + mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures, + mAddAccountOptions, mActivity, mMyCallback, mHandler); + } else { + // send result since we can't prompt to add an account + Bundle result = new Bundle(); + result.putString(KEY_ACCOUNT_NAME, null); + result.putString(KEY_ACCOUNT_TYPE, null); + result.putString(KEY_AUTHTOKEN, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } else if (accounts.length == 1) { + // have a single account, return an authtoken for it + if (mActivity == null) { + mFuture = getAuthToken(accounts[0], mAuthTokenType, + false /* notifyAuthFailure */, mMyCallback, mHandler); + } else { + mFuture = getAuthToken(accounts[0], + mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + } else { + if (mActivity != null) { + IAccountManagerResponse chooseResponse = + new IAccountManagerResponse.Stub() { + public void onResult(Bundle value) throws RemoteException { + Account account = new Account( + value.getString(KEY_ACCOUNT_NAME), + value.getString(KEY_ACCOUNT_TYPE)); + mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + + public void onError(int errorCode, String errorMessage) + throws RemoteException { + mResponse.onError(errorCode, errorMessage); + } + }; + // have many accounts, launch the chooser + Intent intent = new Intent(); + intent.setClassName("android", + "android.accounts.ChooseAccountActivity"); + intent.putExtra(KEY_ACCOUNTS, accounts); + intent.putExtra(KEY_ACCOUNT_MANAGER_RESPONSE, + new AccountManagerResponse(chooseResponse)); + mActivity.startActivity(intent); + // the result will arrive via the IAccountManagerResponse + } else { + // send result since we can't prompt to select an account + Bundle result = new Bundle(); + result.putString(KEY_ACCOUNTS, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } + }}, mHandler); + } + + public void run(AccountManagerFuture future) { + try { + set(future.getResult()); + } catch (OperationCanceledException e) { + cancel(true /* mayInterruptIfRUnning */); + } catch (IOException e) { + setException(e); + } catch (AuthenticatorException e) { + setException(e); + } + } + } + + /** + * Convenience method that combines the functionality of {@link #getAccountsByTypeAndFeatures}, + * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)}, + * and {@link #addAccount}. It first gets the list of accounts that match accountType and the + * feature set. If there are none then {@link #addAccount} is invoked with the authTokenType + * feature set, and addAccountOptions. If there is exactly one then + * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)} is + * called with that account. If there are more than one then a chooser activity is launched + * to prompt the user to select one of them and then the authtoken is retrieved for it, + *

+ * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + *

+ * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. + * + * @param accountType the accountType to query; this must be non-null + * @param authTokenType the type of authtoken to retrieve; this must be non-null + * @param features a filter for the accounts. See {@link #getAccountsByTypeAndFeatures}. + * @param activityForPrompting The activity used to start any account management + * activities that are required to fulfill this request. This may be null. + * @param addAccountOptions authenticator-specific options used if an account needs to be added + * @param getAuthTokenOptions authenticator-specific options passed to getAuthToken + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a {@link Bundle} that contains either: + *

    + *
  • {@link #KEY_INTENT}, if no activity is supplied yet an activity needs to launched to + * fulfill the request. + *
  • {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} if the + * request completes successfully. + *
+ * If the user presses "back" then the request will be canceled. + */ + public AccountManagerFuture getAuthTokenByFeatures( + final String accountType, final String authTokenType, final String[] features, + final Activity activityForPrompting, final Bundle addAccountOptions, + final Bundle getAuthTokenOptions, + final AccountManagerCallback callback, final Handler handler) { + if (accountType == null) throw new IllegalArgumentException("account type is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + final GetAuthTokenByTypeAndFeaturesTask task = + new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features, + activityForPrompting, addAccountOptions, getAuthTokenOptions, callback, handler); + task.start(); + return task; + } + + private final HashMap mAccountsUpdatedListeners = + Maps.newHashMap(); + + /** + * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent + * so that it can read the updated list of accounts and send them to the listener + * in mAccountsUpdatedListeners. + */ + private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(final Context context, final Intent intent) { + final Account[] accounts = getAccounts(); + // send the result to the listeners + synchronized (mAccountsUpdatedListeners) { + for (Map.Entry entry : + mAccountsUpdatedListeners.entrySet()) { + postToHandler(entry.getValue(), entry.getKey(), accounts); + } + } + } + }; + + /** + * Add a {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. + * The listener is guaranteed to be invoked on the thread of the Handler that is passed + * in or the main thread's Handler if handler is null. + *

+ * You must remove this listener before the context that was used to retrieve this + * {@link AccountManager} instance goes away. This generally means when the Activity + * or Service you are running is stopped. + * @param listener the listener to add + * @param handler the Handler whose thread will be used to invoke the listener. If null + * the AccountManager context's main thread will be used. + * @param updateImmediately if true then the listener will be invoked as a result of this + * call. + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was already added + */ + public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener, + Handler handler, boolean updateImmediately) { + if (listener == null) { + throw new IllegalArgumentException("the listener is null"); + } + synchronized (mAccountsUpdatedListeners) { + if (mAccountsUpdatedListeners.containsKey(listener)) { + throw new IllegalStateException("this listener is already added"); + } + final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty(); + + mAccountsUpdatedListeners.put(listener, handler); + + if (wasEmpty) { + // Register a broadcast receiver to monitor account changes + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION); + // To recover from disk-full. + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); + } + } + + if (updateImmediately) { + postToHandler(handler, listener, getAccounts()); + } + } + + /** + * Remove an {@link OnAccountsUpdateListener} that was previously registered with + * {@link #addOnAccountsUpdatedListener}. + * @param listener the listener to remove + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was not already added + */ + public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) { + if (listener == null) { + Log.e(TAG, "Missing listener"); + return; + } + synchronized (mAccountsUpdatedListeners) { + if (!mAccountsUpdatedListeners.containsKey(listener)) { + Log.e(TAG, "Listener was not previously added"); + return; + } + mAccountsUpdatedListeners.remove(listener); + if (mAccountsUpdatedListeners.isEmpty()) { + mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver); + } + } + } +} diff --git a/core/java/android/accounts/AccountManagerCallback.java b/core/java/android/accounts/AccountManagerCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..4aa7169088c53bd7adb12b4cbae69e74486e1044 --- /dev/null +++ b/core/java/android/accounts/AccountManagerCallback.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public interface AccountManagerCallback { + void run(AccountManagerFuture future); +} \ No newline at end of file diff --git a/core/java/android/accounts/AccountManagerFuture.java b/core/java/android/accounts/AccountManagerFuture.java new file mode 100644 index 0000000000000000000000000000000000000000..a1ab00c1a9f6ad582f85ff8a99bc45db1bab0265 --- /dev/null +++ b/core/java/android/accounts/AccountManagerFuture.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.io.IOException; + +/** + * A AccountManagerFuture represents the result of an asynchronous + * {@link AccountManager} call. Methods are provided to check if the computation is + * complete, to wait for its completion, and to retrieve the result of + * the computation. The result can only be retrieved using method + * get when the computation has completed, blocking if + * necessary until it is ready. Cancellation is performed by the + * cancel method. Additional methods are provided to + * determine if the task completed normally or was cancelled. Once a + * computation has completed, the computation cannot be cancelled. + * If you would like to use a Future for the sake + * of cancellability but not provide a usable result, you can + * declare types of the form Future<?> and + * return null as a result of the underlying task. + */ +public interface AccountManagerFuture { + /** + * Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, has already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task. + * + *

After this method returns, subsequent calls to {@link #isDone} will + * always return true. Subsequent calls to {@link #isCancelled} + * will always return true if this method returned true. + * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + */ + boolean cancel(boolean mayInterruptIfRunning); + + /** + * Returns true if this task was cancelled before it completed + * normally. + * + * @return true if this task was cancelled before it completed + */ + boolean isCancelled(); + + /** + * Returns true if this task completed. + * + * Completion may be due to normal termination, an exception, or + * cancellation -- in all of these cases, this method will return + * true. + * + * @return true if this task completed + */ + boolean isDone(); + + /** + * Accessor for the future result the {@link AccountManagerFuture} represents. This + * call will block until the result is available. In order to check if the result is + * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}. + * If the request that generated this result fails or is canceled then an exception + * will be thrown rather than the call returning normally. + * @return the actual result + * @throws android.accounts.OperationCanceledException if the request was canceled for any + * reason + * @throws android.accounts.AuthenticatorException if there was an error communicating with + * the authenticator or if the authenticator returned an invalid response + * @throws java.io.IOException if the authenticator returned an error response that indicates + * that it encountered an IOException while communicating with the authentication server + */ + V getResult() throws OperationCanceledException, IOException, AuthenticatorException; + + /** + * Accessor for the future result the {@link AccountManagerFuture} represents. This + * call will block until the result is available. In order to check if the result is + * available without blocking, one may call {@link #isDone()} and {@link #isCancelled()}. + * If the request that generated this result fails or is canceled then an exception + * will be thrown rather than the call returning normally. If a timeout is specified then + * the request will automatically be canceled if it does not complete in that amount of time. + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument. This must not be null. + * @return the actual result + * @throws android.accounts.OperationCanceledException if the request was canceled for any + * reason + * @throws android.accounts.AuthenticatorException if there was an error communicating with + * the authenticator or if the authenticator returned an invalid response + * @throws java.io.IOException if the authenticator returned an error response that indicates + * that it encountered an IOException while communicating with the authentication server + */ + V getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException; +} \ No newline at end of file diff --git a/core/java/android/accounts/AccountManagerResponse.java b/core/java/android/accounts/AccountManagerResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..1cd6a74973bdecfc606e34fcde075e120d274a57 --- /dev/null +++ b/core/java/android/accounts/AccountManagerResponse.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +/** + * Used by Account Authenticators to return a response. + */ +public class AccountManagerResponse implements Parcelable { + private IAccountManagerResponse mResponse; + + /** @hide */ + public AccountManagerResponse(IAccountManagerResponse response) { + mResponse = response; + } + + /** @hide */ + public AccountManagerResponse(Parcel parcel) { + mResponse = + IAccountManagerResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + /** @hide */ + public int describeContents() { + return 0; + } + + /** @hide */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mResponse.asBinder()); + } + + /** @hide */ + public static final Creator CREATOR = + new Creator() { + public AccountManagerResponse createFromParcel(Parcel source) { + return new AccountManagerResponse(source); + } + + public AccountManagerResponse[] newArray(int size) { + return new AccountManagerResponse[size]; + } + }; +} diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java new file mode 100644 index 0000000000000000000000000000000000000000..800ad749335a20d11deb082cd04ebc9d84a3d4aa --- /dev/null +++ b/core/java/android/accounts/AccountManagerService.java @@ -0,0 +1,1704 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.RegisteredServicesCache; +import android.content.pm.PackageInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.RegisteredServicesCacheListener; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Bundle; +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.SystemClock; +import android.os.Binder; +import android.os.SystemProperties; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.app.PendingIntent; +import android.app.NotificationManager; +import android.app.Notification; +import android.Manifest; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.R; + +/** + * A system service that provides account, password, and authtoken management for all + * accounts on the device. Some of these calls are implemented with the help of the corresponding + * {@link IAccountAuthenticator} services. This service is not accessed by users directly, + * instead one uses an instance of {@link AccountManager}, which can be accessed as follows: + * AccountManager accountManager = + * (AccountManager)context.getSystemService(Context.ACCOUNT_SERVICE) + * @hide + */ +public class AccountManagerService + extends IAccountManager.Stub + implements RegisteredServicesCacheListener { + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + + private static final String NO_BROADCAST_FLAG = "nobroadcast"; + + private static final String TAG = "AccountManagerService"; + + private static final int TIMEOUT_DELAY_MS = 1000 * 60; + private static final String DATABASE_NAME = "accounts.db"; + private static final int DATABASE_VERSION = 4; + + private final Context mContext; + + private HandlerThread mMessageThread; + private final MessageHandler mMessageHandler; + + // Messages that can be sent on mHandler + private static final int MESSAGE_TIMED_OUT = 3; + private static final int MESSAGE_CONNECTED = 7; + private static final int MESSAGE_DISCONNECTED = 8; + + private final AccountAuthenticatorCache mAuthenticatorCache; + private final AuthenticatorBindHelper mBindHelper; + private final DatabaseHelper mOpenHelper; + private final SimWatcher mSimWatcher; + + private static final String TABLE_ACCOUNTS = "accounts"; + private static final String ACCOUNTS_ID = "_id"; + private static final String ACCOUNTS_NAME = "name"; + private static final String ACCOUNTS_TYPE = "type"; + private static final String ACCOUNTS_TYPE_COUNT = "count(type)"; + private static final String ACCOUNTS_PASSWORD = "password"; + + private static final String TABLE_AUTHTOKENS = "authtokens"; + private static final String AUTHTOKENS_ID = "_id"; + private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id"; + private static final String AUTHTOKENS_TYPE = "type"; + private static final String AUTHTOKENS_AUTHTOKEN = "authtoken"; + + private static final String TABLE_GRANTS = "grants"; + private static final String GRANTS_ACCOUNTS_ID = "accounts_id"; + private static final String GRANTS_AUTH_TOKEN_TYPE = "auth_token_type"; + private static final String GRANTS_GRANTEE_UID = "uid"; + + private static final String TABLE_EXTRAS = "extras"; + private static final String EXTRAS_ID = "_id"; + private static final String EXTRAS_ACCOUNTS_ID = "accounts_id"; + private static final String EXTRAS_KEY = "key"; + private static final String EXTRAS_VALUE = "value"; + + private static final String TABLE_META = "meta"; + private static final String META_KEY = "key"; + private static final String META_VALUE = "value"; + + private static final String[] ACCOUNT_NAME_TYPE_PROJECTION = + new String[]{ACCOUNTS_ID, ACCOUNTS_NAME, ACCOUNTS_TYPE}; + private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION = + new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT}; + private static final Intent ACCOUNTS_CHANGED_INTENT; + + private static final String COUNT_OF_MATCHING_GRANTS = "" + + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS + + " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID + + " AND " + GRANTS_GRANTEE_UID + "=?" + + " AND " + GRANTS_AUTH_TOKEN_TYPE + "=?" + + " AND " + ACCOUNTS_NAME + "=?" + + " AND " + ACCOUNTS_TYPE + "=?"; + + private final LinkedHashMap mSessions = new LinkedHashMap(); + private final AtomicInteger mNotificationIds = new AtomicInteger(1); + + private final HashMap, Integer>, Integer> + mCredentialsPermissionNotificationIds = + new HashMap, Integer>, Integer>(); + private final HashMap mSigninRequiredNotificationIds = + new HashMap(); + private static AtomicReference sThis = + new AtomicReference(); + + private static final boolean isDebuggableMonkeyBuild = + SystemProperties.getBoolean("ro.monkey", false) + && SystemProperties.getBoolean("ro.debuggable", false); + private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{}; + + static { + ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); + ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + } + + /** + * This should only be called by system code. One should only call this after the service + * has started. + * @return a reference to the AccountManagerService instance + * @hide + */ + public static AccountManagerService getSingleton() { + return sThis.get(); + } + + public class AuthTokenKey { + public final Account mAccount; + public final String mAuthTokenType; + private final int mHashCode; + + public AuthTokenKey(Account account, String authTokenType) { + mAccount = account; + mAuthTokenType = authTokenType; + mHashCode = computeHashCode(); + } + + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof AuthTokenKey)) { + return false; + } + AuthTokenKey other = (AuthTokenKey)o; + if (!mAccount.equals(other.mAccount)) { + return false; + } + return (mAuthTokenType == null) + ? other.mAuthTokenType == null + : mAuthTokenType.equals(other.mAuthTokenType); + } + + private int computeHashCode() { + int result = 17; + result = 31 * result + mAccount.hashCode(); + result = 31 * result + ((mAuthTokenType == null) ? 0 : mAuthTokenType.hashCode()); + return result; + } + + public int hashCode() { + return mHashCode; + } + } + + public AccountManagerService(Context context) { + mContext = context; + + mOpenHelper = new DatabaseHelper(mContext); + + mMessageThread = new HandlerThread("AccountManagerService"); + mMessageThread.start(); + mMessageHandler = new MessageHandler(mMessageThread.getLooper()); + + mAuthenticatorCache = new AccountAuthenticatorCache(mContext); + mAuthenticatorCache.setListener(this, null /* Handler */); + mBindHelper = new AuthenticatorBindHelper(mContext, mAuthenticatorCache, mMessageHandler, + MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); + + mSimWatcher = new SimWatcher(mContext); + sThis.set(this); + } + + public void onServiceChanged(AuthenticatorDescription desc, boolean removed) { + boolean accountDeleted = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, + new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}, + ACCOUNTS_TYPE + "=?", new String[]{desc.type}, null, null, null); + try { + while (cursor.moveToNext()) { + final long accountId = cursor.getLong(0); + final String accountType = cursor.getString(1); + final String accountName = cursor.getString(2); + Log.d(TAG, "deleting account " + accountName + " because type " + + accountType + " no longer has a registered authenticator"); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null); + accountDeleted = true; + } + } finally { + cursor.close(); + if (accountDeleted) { + sendAccountsChangedBroadcast(); + } + } + } + + public String getPassword(Account account) { + checkAuthenticateAccountsPermission(account); + + long identityToken = clearCallingIdentity(); + try { + return readPasswordFromDatabase(account); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private String readPasswordFromDatabase(Account account) { + if (account == null) { + return null; + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_PASSWORD}, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public String getUserData(Account account, String key) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + return readUserDataFromDatabase(account, key); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private String readUserDataFromDatabase(Account account, String key) { + if (account == null) { + return null; + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_VALUE}, + EXTRAS_ACCOUNTS_ID + + "=(select _id FROM accounts WHERE name=? AND type=?) AND " + + EXTRAS_KEY + "=?", + new String[]{account.name, account.type, key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public AuthenticatorDescription[] getAuthenticatorTypes() { + long identityToken = clearCallingIdentity(); + try { + Collection> + authenticatorCollection = mAuthenticatorCache.getAllServices(); + AuthenticatorDescription[] types = + new AuthenticatorDescription[authenticatorCollection.size()]; + int i = 0; + for (AccountAuthenticatorCache.ServiceInfo authenticator + : authenticatorCollection) { + types[i] = authenticator.type; + i++; + } + return types; + } finally { + restoreCallingIdentity(identityToken); + } + } + + public Account[] getAccountsByType(String accountType) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + final String selection = accountType == null ? null : (ACCOUNTS_TYPE + "=?"); + final String[] selectionArgs = accountType == null ? null : new String[]{accountType}; + Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_NAME_TYPE_PROJECTION, + selection, selectionArgs, null, null, null); + try { + int i = 0; + Account[] accounts = new Account[cursor.getCount()]; + while (cursor.moveToNext()) { + accounts[i] = new Account(cursor.getString(1), cursor.getString(2)); + i++; + } + return accounts; + } finally { + cursor.close(); + } + } + + public boolean addAccount(Account account, String password, Bundle extras) { + checkAuthenticateAccountsPermission(account); + + // fails if the account already exists + long identityToken = clearCallingIdentity(); + try { + return insertAccountIntoDatabase(account, password, extras); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private boolean insertAccountIntoDatabase(Account account, String password, Bundle extras) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + if (account == null) { + return false; + } + boolean noBroadcast = false; + if (account.type.equals(GOOGLE_ACCOUNT_TYPE)) { + // Look for the 'nobroadcast' flag and remove it since we don't want it to persist + // in the db. + noBroadcast = extras.getBoolean(NO_BROADCAST_FLAG, false); + extras.remove(NO_BROADCAST_FLAG); + } + + long numMatches = DatabaseUtils.longForQuery(db, + "select count(*) from " + TABLE_ACCOUNTS + + " WHERE " + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + if (numMatches > 0) { + return false; + } + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, account.name); + values.put(ACCOUNTS_TYPE, account.type); + values.put(ACCOUNTS_PASSWORD, password); + long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); + if (accountId < 0) { + return false; + } + if (extras != null) { + for (String key : extras.keySet()) { + final String value = extras.getString(key); + if (insertExtra(db, accountId, key, value) < 0) { + return false; + } + } + } + db.setTransactionSuccessful(); + if (!noBroadcast) { + sendAccountsChangedBroadcast(); + } + return true; + } finally { + db.endTransaction(); + } + } + + private long insertExtra(SQLiteDatabase db, long accountId, String key, String value) { + ContentValues values = new ContentValues(); + values.put(EXTRAS_KEY, key); + values.put(EXTRAS_ACCOUNTS_ID, accountId); + values.put(EXTRAS_VALUE, value); + return db.insert(TABLE_EXTRAS, EXTRAS_KEY, values); + } + + public void removeAccount(IAccountManagerResponse response, Account account) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new RemoveAccountSession(response, account).bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private class RemoveAccountSession extends Session { + final Account mAccount; + public RemoveAccountSession(IAccountManagerResponse response, Account account) { + super(response, account.type, false /* expectActivityLaunch */); + mAccount = account; + } + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", removeAccount" + + ", account " + mAccount; + } + + public void run() throws RemoteException { + mAuthenticator.getAccountRemovalAllowed(this, mAccount); + } + + public void onResult(Bundle result) { + if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT)) { + final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + if (removalAllowed) { + removeAccount(mAccount); + } + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + Bundle result2 = new Bundle(); + result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed); + try { + response.onResult(result2); + } catch (RemoteException e) { + // ignore + } + } + } + super.onResult(result); + } + } + + private void removeAccount(Account account) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + sendAccountsChangedBroadcast(); + } + + public void invalidateAuthToken(String accountType, String authToken) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + invalidateAuthToken(db, accountType, authToken); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void invalidateAuthToken(SQLiteDatabase db, String accountType, String authToken) { + if (authToken == null || accountType == null) { + return; + } + Cursor cursor = db.rawQuery( + "SELECT " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID + + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME + + ", " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE + + " FROM " + TABLE_ACCOUNTS + + " JOIN " + TABLE_AUTHTOKENS + + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID + + " = " + AUTHTOKENS_ACCOUNTS_ID + + " WHERE " + AUTHTOKENS_AUTHTOKEN + " = ? AND " + + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", + new String[]{authToken, accountType}); + try { + while (cursor.moveToNext()) { + long authTokenId = cursor.getLong(0); + String accountName = cursor.getString(1); + String authTokenType = cursor.getString(2); + db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null); + } + } finally { + cursor.close(); + } + } + + private boolean saveAuthTokenToDatabase(Account account, String type, String authToken) { + if (account == null || type == null) { + return false; + } + cancelNotification(getSigninRequiredNotificationId(account)); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return false; + } + db.delete(TABLE_AUTHTOKENS, + AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", + new String[]{type}); + ContentValues values = new ContentValues(); + values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); + values.put(AUTHTOKENS_TYPE, type); + values.put(AUTHTOKENS_AUTHTOKEN, authToken); + if (db.insert(TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values) >= 0) { + db.setTransactionSuccessful(); + return true; + } + return false; + } finally { + db.endTransaction(); + } + } + + public String readAuthTokenFromDatabase(Account account, String authTokenType) { + if (account == null || authTokenType == null) { + return null; + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_AUTHTOKENS, new String[]{AUTHTOKENS_AUTHTOKEN}, + AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?) AND " + + AUTHTOKENS_TYPE + "=?", + new String[]{account.name, account.type, authTokenType}, + null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + public String peekAuthToken(Account account, String authTokenType) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + return readAuthTokenFromDatabase(account, authTokenType); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setAuthToken(Account account, String authTokenType, String authToken) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + saveAuthTokenToDatabase(account, authTokenType, authToken); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setPassword(Account account, String password) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + try { + setPasswordInDB(account, password); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void setPasswordInDB(Account account, String password) { + if (account == null) { + return; + } + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_PASSWORD, password); + mOpenHelper.getWritableDatabase().update(TABLE_ACCOUNTS, values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.name, account.type}); + sendAccountsChangedBroadcast(); + } + + private void sendAccountsChangedBroadcast() { + mContext.sendBroadcast(ACCOUNTS_CHANGED_INTENT); + } + + public void clearPassword(Account account) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + setPasswordInDB(account, null); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setUserData(Account account, String key, String value) { + checkAuthenticateAccountsPermission(account); + long identityToken = clearCallingIdentity(); + if (account == null) { + return; + } + if (account.type.equals(GOOGLE_ACCOUNT_TYPE) && key.equals("broadcast")) { + sendAccountsChangedBroadcast(); + return; + } + try { + writeUserdataIntoDatabase(account, key, value); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void writeUserdataIntoDatabase(Account account, String key, String value) { + if (account == null || key == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return; + } + long extrasId = getExtrasId(db, accountId, key); + if (extrasId < 0 ) { + extrasId = insertExtra(db, accountId, key, value); + if (extrasId < 0) { + return; + } + } else { + ContentValues values = new ContentValues(); + values.put(EXTRAS_VALUE, value); + if (1 != db.update(TABLE_EXTRAS, values, EXTRAS_ID + "=" + extrasId, null)) { + return; + } + + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void onResult(IAccountManagerResponse response, Bundle result) { + try { + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote + // exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + + public void getAuthToken(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean notifyOnAuthFailure, + final boolean expectActivityLaunch, final Bundle loginOptions) { + checkBinderPermission(Manifest.permission.USE_CREDENTIALS); + final int callerUid = Binder.getCallingUid(); + final boolean permissionGranted = permissionIsGranted(account, authTokenType, callerUid); + + long identityToken = clearCallingIdentity(); + try { + // if the caller has permission, do the peek. otherwise go the more expensive + // route of starting a Session + if (permissionGranted) { + String authToken = readAuthTokenFromDatabase(account, authTokenType); + if (authToken != null) { + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_AUTHTOKEN, authToken); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + onResult(response, result); + return; + } + } + + new Session(response, account.type, expectActivityLaunch) { + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", getAuthToken" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions + + ", notifyOnAuthFailure " + notifyOnAuthFailure; + } + + public void run() throws RemoteException { + // If the caller doesn't have permission then create and return the + // "grant permission" intent instead of the "getAuthToken" intent. + if (!permissionGranted) { + mAuthenticator.getAuthTokenLabel(this, authTokenType); + } else { + mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); + } + } + + public void onResult(Bundle result) { + if (result != null) { + if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) { + Intent intent = newGrantCredentialsPermissionIntent(account, callerUid, + new AccountAuthenticatorResponse(this), + authTokenType, + result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL)); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + onResult(bundle); + return; + } + String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + if (authToken != null) { + String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); + String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "the type and name should not be empty"); + return; + } + saveAuthTokenToDatabase(new Account(name, type), + authTokenType, authToken); + } + + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + if (intent != null && notifyOnAuthFailure) { + doNotification( + account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE), + intent); + } + } + super.onResult(result); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void createNoCredentialsPermissionNotification(Account account, Intent intent) { + int uid = intent.getIntExtra( + GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1); + String authTokenType = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE); + String authTokenLabel = intent.getStringExtra( + GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL); + + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + final String titleAndSubtitle = + mContext.getString(R.string.permission_request_notification_with_subtitle, + account.name); + final int index = titleAndSubtitle.indexOf('\n'); + final String title = titleAndSubtitle.substring(0, index); + final String subtitle = titleAndSubtitle.substring(index + 1); + n.setLatestEventInfo(mContext, + title, subtitle, + PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(getCredentialPermissionNotificationId(account, authTokenType, uid), n); + } + + private Intent newGrantCredentialsPermissionIntent(Account account, int uid, + AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) { + RegisteredServicesCache.ServiceInfo serviceInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(account.type)); + if (serviceInfo == null) { + throw new IllegalArgumentException("unknown account type: " + account.type); + } + + final Context authContext; + try { + authContext = mContext.createPackageContext( + serviceInfo.type.packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException("unknown account type: " + account.type); + } + + Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory( + String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid))); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL, authTokenLabel); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT_TYPE_LABEL, + authContext.getString(serviceInfo.type.labelId)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_PACKAGES, + mContext.getPackageManager().getPackagesForUid(uid)); + intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, uid); + return intent; + } + + private Integer getCredentialPermissionNotificationId(Account account, String authTokenType, + int uid) { + Integer id; + synchronized(mCredentialsPermissionNotificationIds) { + final Pair, Integer> key = + new Pair, Integer>( + new Pair(account, authTokenType), uid); + id = mCredentialsPermissionNotificationIds.get(key); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mCredentialsPermissionNotificationIds.put(key, id); + } + } + return id; + } + + private Integer getSigninRequiredNotificationId(Account account) { + Integer id; + synchronized(mSigninRequiredNotificationIds) { + id = mSigninRequiredNotificationIds.get(account); + if (id == null) { + id = mNotificationIds.incrementAndGet(); + mSigninRequiredNotificationIds.put(account, id); + } + } + return id; + } + + + public void addAcount(final IAccountManagerResponse response, final String accountType, + final String authTokenType, final String[] requiredFeatures, + final boolean expectActivityLaunch, final Bundle options) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures, + options); + } + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", addAccount" + + ", accountType " + accountType + + ", requiredFeatures " + + (requiredFeatures != null + ? TextUtils.join(",", requiredFeatures) + : null); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void confirmCredentials(IAccountManagerResponse response, + final Account account, final Bundle options, final boolean expectActivityLaunch) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.type, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.confirmCredentials(this, account, options); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", confirmCredentials" + + ", " + account; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void updateCredentials(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean expectActivityLaunch, + final Bundle loginOptions) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.type, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); + } + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", updateCredentials" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void editProperties(IAccountManagerResponse response, final String accountType, + final boolean expectActivityLaunch) { + checkManageAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.editProperties(this, mAccountType); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", editProperties" + + ", accountType " + accountType; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private class GetAccountsByTypeAndFeatureSession extends Session { + private final String[] mFeatures; + private volatile Account[] mAccountsOfType = null; + private volatile ArrayList mAccountsWithFeatures = null; + private volatile int mCurrentAccount = 0; + + public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, + String type, String[] features) { + super(response, type, false /* expectActivityLaunch */); + mFeatures = features; + } + + public void run() throws RemoteException { + mAccountsOfType = getAccountsByType(mAccountType); + // check whether each account matches the requested features + mAccountsWithFeatures = new ArrayList(mAccountsOfType.length); + mCurrentAccount = 0; + + checkAccount(); + } + + public void checkAccount() { + if (mCurrentAccount >= mAccountsOfType.length) { + sendResult(); + return; + } + + try { + mAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures); + } catch (RemoteException e) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result == null) { + onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); + return; + } + if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) { + mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]); + } + mCurrentAccount++; + checkAccount(); + } + + public void sendResult() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + try { + Account[] accounts = new Account[mAccountsWithFeatures.size()]; + for (int i = 0; i < accounts.length; i++) { + accounts[i] = mAccountsWithFeatures.get(i); + } + Bundle result = new Bundle(); + result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts); + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", getAccountsByTypeAndFeatures" + + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); + } + } + + public Account[] getAccounts(String type) { + checkReadAccountsPermission(); + long identityToken = clearCallingIdentity(); + try { + return getAccountsByType(type); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void getAccountsByFeatures(IAccountManagerResponse response, + String type, String[] features) { + checkReadAccountsPermission(); + if (features != null && type == null) { + if (response != null) { + try { + response.onError(AccountManager.ERROR_CODE_BAD_ARGUMENTS, "type is null"); + } catch (RemoteException e) { + // ignore this + } + } + return; + } + long identityToken = clearCallingIdentity(); + try { + if (features == null || features.length == 0) { + getAccountsByType(type); + return; + } + new GetAccountsByTypeAndFeatureSession(response, type, features).bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private long getAccountId(SQLiteDatabase db, Account account) { + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID}, + "name=? AND type=?", new String[]{account.name, account.type}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private long getExtrasId(SQLiteDatabase db, long accountId, String key) { + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_ID}, + EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", + new String[]{key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private abstract class Session extends IAccountAuthenticatorResponse.Stub + implements AuthenticatorBindHelper.Callback, IBinder.DeathRecipient { + IAccountManagerResponse mResponse; + final String mAccountType; + final boolean mExpectActivityLaunch; + final long mCreationTime; + + public int mNumResults = 0; + private int mNumRequestContinued = 0; + private int mNumErrors = 0; + + + IAccountAuthenticator mAuthenticator = null; + + public Session(IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch) { + super(); + if (response == null) throw new IllegalArgumentException("response is null"); + if (accountType == null) throw new IllegalArgumentException("accountType is null"); + mResponse = response; + mAccountType = accountType; + mExpectActivityLaunch = expectActivityLaunch; + mCreationTime = SystemClock.elapsedRealtime(); + synchronized (mSessions) { + mSessions.put(toString(), this); + } + try { + response.asBinder().linkToDeath(this, 0 /* flags */); + } catch (RemoteException e) { + mResponse = null; + binderDied(); + } + } + + IAccountManagerResponse getResponseAndClose() { + if (mResponse == null) { + // this session has already been closed + return null; + } + IAccountManagerResponse response = mResponse; + close(); // this clears mResponse so we need to save the response before this call + return response; + } + + private void close() { + synchronized (mSessions) { + if (mSessions.remove(toString()) == null) { + // the session was already closed, so bail out now + return; + } + } + if (mResponse != null) { + // stop listening for response deaths + mResponse.asBinder().unlinkToDeath(this, 0 /* flags */); + + // clear this so that we don't accidentally send any further results + mResponse = null; + } + cancelTimeout(); + unbind(); + } + + public void binderDied() { + mResponse = null; + close(); + } + + protected String toDebugString() { + return toDebugString(SystemClock.elapsedRealtime()); + } + + protected String toDebugString(long now) { + return "Session: expectLaunch " + mExpectActivityLaunch + + ", connected " + (mAuthenticator != null) + + ", stats (" + mNumResults + "/" + mNumRequestContinued + + "/" + mNumErrors + ")" + + ", lifetime " + ((now - mCreationTime) / 1000.0); + } + + void bind() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "initiating bind to authenticator type " + mAccountType); + } + if (!mBindHelper.bind(mAccountType, this)) { + Log.d(TAG, "bind attempt failed for " + toDebugString()); + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); + } + } + + private void unbind() { + if (mAuthenticator != null) { + mAuthenticator = null; + mBindHelper.unbind(this); + } + } + + public void scheduleTimeout() { + mMessageHandler.sendMessageDelayed( + mMessageHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS); + } + + public void cancelTimeout() { + mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); + } + + public void onConnected(IBinder service) { + mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); + try { + run(); + } catch (RemoteException e) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "remote exception"); + } + } + + public abstract void run() throws RemoteException; + + public void onDisconnected() { + mAuthenticator = null; + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "disconnected"); + } + } + + public void onTimedOut() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, + "timeout"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) { + String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); + String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); + if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { + Account account = new Account(accountName, accountType); + cancelNotification(getSigninRequiredNotificationId(account)); + } + } + IAccountManagerResponse response; + if (mExpectActivityLaunch && result != null + && result.containsKey(AccountManager.KEY_INTENT)) { + response = mResponse; + } else { + response = getResponseAndClose(); + } + if (response != null) { + try { + if (result == null) { + response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "null bundle returned"); + } else { + response.onResult(result); + } + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + public void onRequestContinued() { + mNumRequestContinued++; + } + + public void onError(int errorCode, String errorMessage) { + mNumErrors++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: " + errorCode + ", " + errorMessage); + } + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: responding"); + } + try { + response.onError(errorCode, errorMessage); + } catch (RemoteException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: caught RemoteException while responding", e); + } + } + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: already closed"); + } + } + } + } + + private class MessageHandler extends Handler { + MessageHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + if (mBindHelper.handleMessage(msg)) { + return; + } + switch (msg.what) { + case MESSAGE_TIMED_OUT: + Session session = (Session)msg.obj; + session.onTimedOut(); + break; + + default: + throw new IllegalStateException("unhandled message: " + msg.what); + } + } + } + + private class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( " + + ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ACCOUNTS_NAME + " TEXT NOT NULL, " + + ACCOUNTS_TYPE + " TEXT NOT NULL, " + + ACCOUNTS_PASSWORD + " TEXT, " + + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); + + db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( " + + AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + AUTHTOKENS_TYPE + " TEXT NOT NULL, " + + AUTHTOKENS_AUTHTOKEN + " TEXT, " + + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))"); + + createGrantsTable(db); + + db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( " + + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + EXTRAS_ACCOUNTS_ID + " INTEGER, " + + EXTRAS_KEY + " TEXT NOT NULL, " + + EXTRAS_VALUE + " TEXT, " + + "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))"); + + db.execSQL("CREATE TABLE " + TABLE_META + " ( " + + META_KEY + " TEXT PRIMARY KEY NOT NULL, " + + META_VALUE + " TEXT)"); + + createAccountsDeletionTrigger(db); + } + + private void createAccountsDeletionTrigger(SQLiteDatabase db) { + db.execSQL("" + + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS + + " BEGIN" + + " DELETE FROM " + TABLE_AUTHTOKENS + + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_EXTRAS + + " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_GRANTS + + " WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " END"); + } + + private void createGrantsTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( " + + GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL, " + + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, " + + "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE + + "," + GRANTS_GRANTEE_UID + "))"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); + + if (oldVersion == 1) { + // no longer need to do anything since the work is done + // when upgrading from version 2 + oldVersion++; + } + + if (oldVersion == 2) { + createGrantsTable(db); + db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete"); + createAccountsDeletionTrigger(db); + oldVersion++; + } + + if (oldVersion == 3) { + db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_TYPE + + " = 'com.google' WHERE " + ACCOUNTS_TYPE + " == 'com.google.GAIA'"); + oldVersion++; + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME); + } + } + + private void setMetaValue(String key, String value) { + ContentValues values = new ContentValues(); + values.put(META_KEY, key); + values.put(META_VALUE, value); + mOpenHelper.getWritableDatabase().replace(TABLE_META, META_KEY, values); + } + + private String getMetaValue(String key) { + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_META, + new String[]{META_VALUE}, META_KEY + "=?", new String[]{key}, null, null, null); + try { + if (c.moveToNext()) { + return c.getString(0); + } + return null; + } finally { + c.close(); + } + } + + private class SimWatcher extends BroadcastReceiver { + public SimWatcher(Context context) { + // Re-scan the SIM card when the SIM state changes, and also if + // the disk recovers from a full state (we may have failed to handle + // things properly while the disk was full). + final IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + context.registerReceiver(this, filter); + } + + /** + * Compare the IMSI to the one stored in the login service's + * database. If they differ, erase all passwords and + * authtokens (and store the new IMSI). + */ + @Override + public void onReceive(Context context, Intent intent) { + // Check IMSI on every update; nothing happens if the IMSI is missing or unchanged. + String imsi = ((TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE)).getSubscriberId(); + if (TextUtils.isEmpty(imsi)) return; + + String storedImsi = getMetaValue("imsi"); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "current IMSI=" + imsi + "; stored IMSI=" + storedImsi); + } + + if (!imsi.equals(storedImsi) && !TextUtils.isEmpty(storedImsi)) { + Log.w(TAG, "wiping all passwords and authtokens because IMSI changed (" + + "stored=" + storedImsi + ", current=" + imsi + ")"); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + db.execSQL("DELETE from " + TABLE_AUTHTOKENS); + db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_PASSWORD + " = ''"); + sendAccountsChangedBroadcast(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + setMetaValue("imsi", imsi); + } + } + + public IBinder onBind(Intent intent) { + return asBinder(); + } + + /** + * Searches array of arguments for the specified string + * @param args array of argument strings + * @param value value to search for + * @return true if the value is contained in the array + */ + private static boolean scanArgs(String[] args, String value) { + if (args != null) { + for (String arg : args) { + if (value.equals(arg)) { + return true; + } + } + } + return false; + } + + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + final boolean isCheckinRequest = scanArgs(args, "--checkin") || scanArgs(args, "-c"); + + if (isCheckinRequest) { + // This is a checkin request. *Only* upload the account types and the count of each. + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION, + null, null, ACCOUNTS_TYPE, null, null); + try { + while (cursor.moveToNext()) { + // print type,count + fout.println(cursor.getString(0) + "," + cursor.getString(1)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } else { + synchronized (mSessions) { + final long now = SystemClock.elapsedRealtime(); + fout.println("AccountManagerService: " + mSessions.size() + " sessions"); + for (Session session : mSessions.values()) { + fout.println(" " + session.toDebugString(now)); + } + } + + fout.println(); + mAuthenticatorCache.dump(fd, fout, args); + } + } + + private void doNotification(Account account, CharSequence message, Intent intent) { + long identityToken = clearCallingIdentity(); + try { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "doNotification: " + message + " intent:" + intent); + } + + if (intent.getComponent() != null && + GrantCredentialsPermissionActivity.class.getName().equals( + intent.getComponent().getClassName())) { + createNoCredentialsPermissionNotification(account, intent); + } else { + final Integer notificationId = getSigninRequiredNotificationId(account); + intent.addCategory(String.valueOf(notificationId)); + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + final String notificationTitleFormat = + mContext.getText(R.string.notification_title).toString(); + n.setLatestEventInfo(mContext, + String.format(notificationTitleFormat, account.name), + message, PendingIntent.getActivity( + mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(notificationId, n); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void cancelNotification(int id) { + long identityToken = clearCallingIdentity(); + try { + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(id); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void checkBinderPermission(String permission) { + final int uid = Binder.getCallingUid(); + if (mContext.checkCallingOrSelfPermission(permission) != + PackageManager.PERMISSION_GRANTED) { + String msg = "caller uid " + uid + " lacks " + permission; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " has " + permission); + } + } + + private boolean inSystemImage(int callerUid) { + String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); + for (String name : packages) { + try { + PackageInfo packageInfo = + mContext.getPackageManager().getPackageInfo(name, 0 /* flags */); + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + return false; + } + + private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { + final boolean fromAuthenticator = account != null + && hasAuthenticatorUid(account.type, callerUid); + final boolean hasExplicitGrants = account != null + && hasExplicitlyGrantedPermission(account, authTokenType); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid " + + callerUid + ", account " + account + + ": is authenticator? " + fromAuthenticator + + ", has explicit permission? " + hasExplicitGrants); + } + return fromAuthenticator || hasExplicitGrants || inSystemImage(callerUid); + } + + private boolean hasAuthenticatorUid(String accountType, int callingUid) { + for (RegisteredServicesCache.ServiceInfo serviceInfo : + mAuthenticatorCache.getAllServices()) { + if (serviceInfo.type.type.equals(accountType)) { + return (serviceInfo.uid == callingUid) || + (mContext.getPackageManager().checkSignatures(serviceInfo.uid, callingUid) + == PackageManager.SIGNATURE_MATCH); + } + } + return false; + } + + private boolean hasExplicitlyGrantedPermission(Account account, String authTokenType) { + if (Binder.getCallingUid() == android.os.Process.SYSTEM_UID) { + return true; + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + String[] args = {String.valueOf(Binder.getCallingUid()), authTokenType, + account.name, account.type}; + final boolean permissionGranted = + DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0; + if (!permissionGranted && isDebuggableMonkeyBuild) { + // TODO: Skip this check when running automated tests. Replace this + // with a more general solution. + Log.w(TAG, "no credentials permission for usage of " + account + ", " + + authTokenType + " by uid " + Binder.getCallingUid() + + " but ignoring since this is a monkey build"); + return true; + } + return permissionGranted; + } + + private void checkCallingUidAgainstAuthenticator(Account account) { + final int uid = Binder.getCallingUid(); + if (account == null || !hasAuthenticatorUid(account.type, uid)) { + String msg = "caller uid " + uid + " is different than the authenticator's uid"; + Log.w(TAG, msg); + throw new SecurityException(msg); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "caller uid " + uid + " is the same as the authenticator's uid"); + } + } + + private void checkAuthenticateAccountsPermission(Account account) { + checkBinderPermission(Manifest.permission.AUTHENTICATE_ACCOUNTS); + checkCallingUidAgainstAuthenticator(account); + } + + private void checkReadAccountsPermission() { + checkBinderPermission(Manifest.permission.GET_ACCOUNTS); + } + + private void checkManageAccountsPermission() { + checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS); + } + + /** + * Allow callers with the given uid permission to get credentials for account/authTokenType. + *

+ * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void grantAppPermission(Account account, String authTokenType, int uid) { + if (account == null || authTokenType == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + ContentValues values = new ContentValues(); + values.put(GRANTS_ACCOUNTS_ID, accountId); + values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType); + values.put(GRANTS_GRANTEE_UID, uid); + db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } + + /** + * Don't allow callers with the given uid permission to get credentials for + * account/authTokenType. + *

+ * Although this is public it can only be accessed via the AccountManagerService object + * which is in the system. This means we don't need to protect it with permissions. + * @hide + */ + public void revokeAppPermission(Account account, String authTokenType, int uid) { + if (account == null || authTokenType == null) { + return; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId >= 0) { + db.delete(TABLE_GRANTS, + GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND " + + GRANTS_GRANTEE_UID + "=?", + new String[]{String.valueOf(accountId), authTokenType, + String.valueOf(uid)}); + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid)); + } +} diff --git a/core/java/android/accounts/AccountMonitor.java b/core/java/android/accounts/AccountMonitor.java deleted file mode 100644 index f21385eadeb70cf72b220fffddc3083e73b263dd..0000000000000000000000000000000000000000 --- a/core/java/android/accounts/AccountMonitor.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -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.database.SQLException; -import android.os.IBinder; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -/** - * A helper class that calls back on the provided - * AccountMonitorListener with the set of current accounts both when - * it gets created and whenever the set changes. It does this by - * binding to the AccountsService and registering to receive the - * intent broadcast when the set of accounts is changed. The - * connection to the accounts service is only made when it needs to - * fetch the current list of accounts (that is, when the - * AccountMonitor is first created, and when the intent is received). - */ -public class AccountMonitor extends BroadcastReceiver implements ServiceConnection { - private final Context mContext; - private final AccountMonitorListener mListener; - private boolean mClosed = false; - private int pending = 0; - - // This thread runs in the background and runs the code to update accounts - // in the listener. - private class AccountUpdater extends Thread { - private IBinder mService; - - public AccountUpdater(IBinder service) { - mService = service; - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - IAccountsService accountsService = IAccountsService.Stub.asInterface(mService); - String[] accounts = null; - do { - try { - accounts = accountsService.getAccounts(); - } catch (RemoteException e) { - // if the service was killed then the system will restart it and when it does we - // will get another onServiceConnected, at which point we will do a notify. - Log.w("AccountMonitor", "Remote exception when getting accounts", e); - return; - } - - synchronized (AccountMonitor.this) { - --pending; - if (pending == 0) { - break; - } - } - } while (true); - - mContext.unbindService(AccountMonitor.this); - - try { - mListener.onAccountsUpdated(accounts); - } catch (SQLException e) { - // Better luck next time. If the problem was disk-full, - // the STORAGE_OK intent will re-trigger the update. - Log.e("AccountMonitor", "Can't update accounts", e); - } - } - } - - /** - * Initializes the AccountMonitor and initiates a bind to the - * AccountsService to get the initial account list. For 1.0, - * the "list" is always a single account. - * - * @param context the context we are running in - * @param listener the user to notify when the account set changes - */ - public AccountMonitor(Context context, AccountMonitorListener listener) { - if (listener == null) { - throw new IllegalArgumentException("listener is null"); - } - - mContext = context; - mListener = listener; - - // Register a broadcast receiver to monitor account changes - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(AccountsServiceConstants.LOGIN_ACCOUNTS_CHANGED_ACTION); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // To recover from disk-full. - mContext.registerReceiver(this, intentFilter); - - // Send the listener the initial state now. - notifyListener(); - } - - @Override - public void onReceive(Context context, Intent intent) { - notifyListener(); - } - - public void onServiceConnected(ComponentName className, IBinder service) { - // Create a background thread to update the accounts. - new AccountUpdater(service).start(); - } - - public void onServiceDisconnected(ComponentName className) { - } - - private synchronized void notifyListener() { - if (pending == 0) { - // initiate the bind - if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, - this, Context.BIND_AUTO_CREATE)) { - // This is normal if GLS isn't part of this build. - Log.w("AccountMonitor", - "Couldn't connect to " + - AccountsServiceConstants.SERVICE_INTENT + - " (Missing service?)"); - } - } else { - // already bound. bindService will not trigger another - // call to onServiceConnected, so instead we make sure - // that the existing background thread will call - // getAccounts() after this function returns, by - // incrementing pending. - // - // Yes, this else clause contains only a comment. - } - ++pending; - } - - /** - * calls close() - * @throws Throwable - */ - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } - - /** - * Unregisters the account receiver. Consecutive calls to this - * method are harmless, but also do nothing. Once this call is - * made no more notifications will occur. - */ - public synchronized void close() { - if (!mClosed) { - mContext.unregisterReceiver(this); - mClosed = true; - } - } -} diff --git a/core/java/android/accounts/AccountsException.java b/core/java/android/accounts/AccountsException.java new file mode 100644 index 0000000000000000000000000000000000000000..b997390d652740a977be97e958ff4bfa69248b63 --- /dev/null +++ b/core/java/android/accounts/AccountsException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +public class AccountsException extends Exception { + public AccountsException() { + super(); + } + public AccountsException(String message) { + super(message); + } + public AccountsException(String message, Throwable cause) { + super(message, cause); + } + public AccountsException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/core/java/android/accounts/AccountsServiceConstants.java b/core/java/android/accounts/AccountsServiceConstants.java deleted file mode 100644 index b882e7b3ceb73ca8d1d065469b2eaf7570c9ab74..0000000000000000000000000000000000000000 --- a/core/java/android/accounts/AccountsServiceConstants.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 android.accounts; - -import android.content.Intent; - -/** - * Miscellaneous constants used by the AccountsService and its - * clients. - */ -// TODO: These constants *could* come directly from the -// IAccountsService interface, but that's not possible since the -// aidl compiler doesn't let you define constants (yet.) -public class AccountsServiceConstants { - /** This class is never instantiated. */ - private AccountsServiceConstants() { - } - - /** - * Action sent as a broadcast Intent by the AccountsService - * when accounts are added to and/or removed from the device's - * database, or when the primary account is changed. - */ - public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = - "android.accounts.LOGIN_ACCOUNTS_CHANGED"; - - /** - * Action sent as a broadcast Intent by the AccountsService - * when it starts up and no accounts are available (so some should be added). - */ - public static final String LOGIN_ACCOUNTS_MISSING_ACTION = - "android.accounts.LOGIN_ACCOUNTS_MISSING"; - - /** - * Action on the intent used to bind to the IAccountsService interface. This - * is used for services that have multiple interfaces (allowing - * them to differentiate the interface intended, and return the proper - * Binder.) - */ - private static final String ACCOUNTS_SERVICE_ACTION = "android.accounts.IAccountsService"; - - /* - * The intent uses a component in addition to the action to ensure the actual - * accounts service is bound to (a malicious third-party app could - * theoretically have a service with the same action). - */ - /** The intent used to bind to the accounts service. */ - public static final Intent SERVICE_INTENT = - new Intent() - .setClassName("com.google.android.googleapps", - "com.google.android.googleapps.GoogleLoginService") - .setAction(ACCOUNTS_SERVICE_ACTION); - - /** - * Checks whether the intent is to bind to the accounts service. - * - * @param bindIntent The Intent used to bind to the service. - * @return Whether the intent is to bind to the accounts service. - */ - public static final boolean isForAccountsService(Intent bindIntent) { - String otherAction = bindIntent.getAction(); - return otherAction != null && otherAction.equals(ACCOUNTS_SERVICE_ACTION); - } -} diff --git a/core/java/android/accounts/AuthenticatorBindHelper.java b/core/java/android/accounts/AuthenticatorBindHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..2ca1f0e492cd6f54fcb05ea9bc74e5b9227337a9 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorBindHelper.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Map; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +/** + * A helper object that simplifies binding to Account Authenticators. It uses the + * {@link AccountAuthenticatorCache} to find the component name of the authenticators, + * allowing the user to bind by account name. It also allows multiple, simultaneous binds + * to the same authenticator, with each bind call guaranteed to return either + * {@link Callback#onConnected} or {@link Callback#onDisconnected} if the bind() call + * itself succeeds, even if the authenticator is already bound internally. + * @hide + */ +public class AuthenticatorBindHelper { + private static final String TAG = "Accounts"; + private final Handler mHandler; + private final Context mContext; + private final int mMessageWhatConnected; + private final int mMessageWhatDisconnected; + private final Map mServiceConnections = Maps.newHashMap(); + private final Map> mServiceUsers = Maps.newHashMap(); + private final AccountAuthenticatorCache mAuthenticatorCache; + + public AuthenticatorBindHelper(Context context, + AccountAuthenticatorCache authenticatorCache, Handler handler, + int messageWhatConnected, int messageWhatDisconnected) { + mContext = context; + mHandler = handler; + mAuthenticatorCache = authenticatorCache; + mMessageWhatConnected = messageWhatConnected; + mMessageWhatDisconnected = messageWhatDisconnected; + } + + public interface Callback { + void onConnected(IBinder service); + void onDisconnected(); + } + + public boolean bind(String authenticatorType, Callback callback) { + // if the authenticator is connecting or connected then return true + synchronized (mServiceConnections) { + if (mServiceConnections.containsKey(authenticatorType)) { + MyServiceConnection connection = mServiceConnections.get(authenticatorType); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "service connection already exists for " + authenticatorType); + } + mServiceUsers.get(authenticatorType).add(callback); + if (connection.mService != null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service is connected, scheduling a connected message for " + + authenticatorType); + } + connection.scheduleCallbackConnectedMessage(callback); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service is *not* connected, waiting for for " + + authenticatorType); + } + } + return true; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no service connection for " + authenticatorType); + } + + // otherwise find the component name for the authenticator and initiate a bind + // if no authenticator or the bind fails then return false, otherwise return true + AccountAuthenticatorCache.ServiceInfo authenticatorInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(authenticatorType)); + if (authenticatorInfo == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no authenticator for " + authenticatorType + + ", bailing out"); + } + return false; + } + + MyServiceConnection connection = new MyServiceConnection(authenticatorType); + + Intent intent = new Intent(); + intent.setAction("android.accounts.AccountAuthenticator"); + intent.setComponent(authenticatorInfo.componentName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); + } + if (!mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); + } + return false; + } + + mServiceConnections.put(authenticatorType, connection); + mServiceUsers.put(authenticatorType, Lists.newArrayList(callback)); + return true; + } + } + + public void unbind(Callback callbackToUnbind) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "unbinding callback " + callbackToUnbind); + } + synchronized (mServiceConnections) { + for (Map.Entry> entry : mServiceUsers.entrySet()) { + final String authenticatorType = entry.getKey(); + final ArrayList serviceUsers = entry.getValue(); + for (Callback callback : serviceUsers) { + if (callback == callbackToUnbind) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "found callback in service" + authenticatorType); + } + serviceUsers.remove(callbackToUnbind); + if (serviceUsers.isEmpty()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there are no more callbacks for service " + + authenticatorType + ", unbinding service"); + } + unbindFromServiceLocked(authenticatorType); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "leaving service " + authenticatorType + + " around since there are still callbacks using it"); + } + } + return; + } + } + } + Log.e(TAG, "did not find callback " + callbackToUnbind + " in any of the services"); + } + } + + /** + * You must synchronized on mServiceConnections before calling this + */ + private void unbindFromServiceLocked(String authenticatorType) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "unbindService from " + authenticatorType); + } + mContext.unbindService(mServiceConnections.get(authenticatorType)); + mServiceUsers.remove(authenticatorType); + mServiceConnections.remove(authenticatorType); + } + + private class ConnectedMessagePayload { + public final IBinder mService; + public final Callback mCallback; + public ConnectedMessagePayload(IBinder service, Callback callback) { + mService = service; + mCallback = callback; + } + } + + private class MyServiceConnection implements ServiceConnection { + private final String mAuthenticatorType; + private IBinder mService = null; + + public MyServiceConnection(String authenticatorType) { + mAuthenticatorType = authenticatorType; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "onServiceConnected for account type " + mAuthenticatorType); + } + // post a message for each service user to tell them that the service is connected + synchronized (mServiceConnections) { + mService = service; + for (Callback callback : mServiceUsers.get(mAuthenticatorType)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service became connected, scheduling a connected " + + "message for " + mAuthenticatorType); + } + scheduleCallbackConnectedMessage(callback); + } + } + } + + private void scheduleCallbackConnectedMessage(Callback callback) { + final ConnectedMessagePayload payload = + new ConnectedMessagePayload(mService, callback); + mHandler.obtainMessage(mMessageWhatConnected, payload).sendToTarget(); + } + + public void onServiceDisconnected(ComponentName name) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "onServiceDisconnected for account type " + mAuthenticatorType); + } + // post a message for each service user to tell them that the service is disconnected, + // and unbind from the service. + synchronized (mServiceConnections) { + final ArrayList callbackList = mServiceUsers.get(mAuthenticatorType); + if (callbackList != null) { + for (Callback callback : callbackList) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service became disconnected, scheduling a " + + "disconnected message for " + + mAuthenticatorType); + } + mHandler.obtainMessage(mMessageWhatDisconnected, callback).sendToTarget(); + } + unbindFromServiceLocked(mAuthenticatorType); + } + } + } + } + + boolean handleMessage(Message message) { + if (message.what == mMessageWhatConnected) { + ConnectedMessagePayload payload = (ConnectedMessagePayload)message.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "notifying callback " + payload.mCallback + " that it is connected"); + } + payload.mCallback.onConnected(payload.mService); + return true; + } else if (message.what == mMessageWhatDisconnected) { + Callback callback = (Callback)message.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "notifying callback " + callback + " that it is disconnected"); + } + callback.onDisconnected(); + return true; + } else { + return false; + } + } +} diff --git a/core/java/android/accounts/AuthenticatorDescription.aidl b/core/java/android/accounts/AuthenticatorDescription.aidl new file mode 100644 index 0000000000000000000000000000000000000000..136361c4898ab44f1a99750ed464b361383ce88a --- /dev/null +++ b/core/java/android/accounts/AuthenticatorDescription.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +parcelable AuthenticatorDescription; diff --git a/core/java/android/accounts/AuthenticatorDescription.java b/core/java/android/accounts/AuthenticatorDescription.java new file mode 100644 index 0000000000000000000000000000000000000000..91c94e6de62eadda7222c554969210cf4ff58bf0 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorDescription.java @@ -0,0 +1,117 @@ +package android.accounts; + +import android.os.Parcelable; +import android.os.Parcel; + +/** + * A {@link Parcelable} value type that contains information about an account authenticator. + */ +public class AuthenticatorDescription implements Parcelable { + /** The string that uniquely identifies an authenticator */ + final public String type; + + /** A resource id of a label for the authenticator that is suitable for displaying */ + final public int labelId; + + /** A resource id of a icon for the authenticator */ + final public int iconId; + + /** A resource id of a smaller icon for the authenticator */ + final public int smallIconId; + + /** + * A resource id for a hierarchy of PreferenceScreen to be added to the settings page for the + * account. See {@link AbstractAccountAuthenticator} for an example. + */ + final public int accountPreferencesId; + + /** The package name that can be used to lookup the resources from above. */ + final public String packageName; + + /** A constructor for a full AuthenticatorDescription */ + public AuthenticatorDescription(String type, String packageName, int labelId, int iconId, + int smallIconId, int prefId) { + if (type == null) throw new IllegalArgumentException("type cannot be null"); + if (packageName == null) throw new IllegalArgumentException("packageName cannot be null"); + this.type = type; + this.packageName = packageName; + this.labelId = labelId; + this.iconId = iconId; + this.smallIconId = smallIconId; + this.accountPreferencesId = prefId; + } + + /** + * A factory method for creating an AuthenticatorDescription that can be used as a key + * to identify the authenticator by its type. + */ + + public static AuthenticatorDescription newKey(String type) { + if (type == null) throw new IllegalArgumentException("type cannot be null"); + return new AuthenticatorDescription(type); + } + + private AuthenticatorDescription(String type) { + this.type = type; + this.packageName = null; + this.labelId = 0; + this.iconId = 0; + this.smallIconId = 0; + this.accountPreferencesId = 0; + } + + private AuthenticatorDescription(Parcel source) { + this.type = source.readString(); + this.packageName = source.readString(); + this.labelId = source.readInt(); + this.iconId = source.readInt(); + this.smallIconId = source.readInt(); + this.accountPreferencesId = source.readInt(); + } + + /** @inheritDoc */ + public int describeContents() { + return 0; + } + + /** Returns the hashcode of the type only. */ + public int hashCode() { + return type.hashCode(); + } + + /** Compares the type only, suitable for key comparisons. */ + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof AuthenticatorDescription)) return false; + final AuthenticatorDescription other = (AuthenticatorDescription) o; + return type.equals(other.type); + } + + public String toString() { + return "AuthenticatorDescription {type=" + type + "}"; + } + + /** @inhericDoc */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(type); + dest.writeString(packageName); + dest.writeInt(labelId); + dest.writeInt(iconId); + dest.writeInt(smallIconId); + dest.writeInt(accountPreferencesId); + } + + /** Used to create the object from a parcel. */ + public static final Creator CREATOR = + new Creator() { + /** @inheritDoc */ + public AuthenticatorDescription createFromParcel(Parcel source) { + return new AuthenticatorDescription(source); + } + + /** @inheritDoc */ + public AuthenticatorDescription[] newArray(int size) { + return new AuthenticatorDescription[size]; + } + }; +} diff --git a/core/java/android/accounts/AuthenticatorException.java b/core/java/android/accounts/AuthenticatorException.java new file mode 100644 index 0000000000000000000000000000000000000000..f778d7d7cada7691684fe54258b5d1e3045802f7 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +public class AuthenticatorException extends AccountsException { + public AuthenticatorException() { + super(); + } + public AuthenticatorException(String message) { + super(message); + } + public AuthenticatorException(String message, Throwable cause) { + super(message, cause); + } + public AuthenticatorException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/ChooseAccountActivity.java b/core/java/android/accounts/ChooseAccountActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..4a0018e1f722620bb3dc7a9d7e8cd9c17894cec4 --- /dev/null +++ b/core/java/android/accounts/ChooseAccountActivity.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import android.app.ListActivity; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.util.Log; + +/** + * @hide + */ +public class ChooseAccountActivity extends ListActivity { + private static final String TAG = "AccountManager"; + private Parcelable[] mAccounts = null; + private AccountManagerResponse mAccountManagerResponse = null; + private Bundle mResult; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + mAccounts = getIntent().getParcelableArrayExtra(AccountManager.KEY_ACCOUNTS); + mAccountManagerResponse = + getIntent().getParcelableExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE); + } else { + mAccounts = savedInstanceState.getParcelableArray(AccountManager.KEY_ACCOUNTS); + mAccountManagerResponse = + savedInstanceState.getParcelable(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE); + } + + String[] mAccountNames = new String[mAccounts.length]; + for (int i = 0; i < mAccounts.length; i++) { + mAccountNames[i] = ((Account) mAccounts[i]).name; + } + + // Use an existing ListAdapter that will map an array + // of strings to TextViews + setListAdapter(new ArrayAdapter(this, + android.R.layout.simple_list_item_1, mAccountNames)); + getListView().setTextFilterEnabled(true); + } + + protected void onListItemClick(ListView l, View v, int position, long id) { + Account account = (Account) mAccounts[position]; + Log.d(TAG, "selected account " + account); + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + mResult = bundle; + finish(); + } + + public void finish() { + if (mAccountManagerResponse != null) { + if (mResult != null) { + mAccountManagerResponse.onResult(mResult); + } else { + mAccountManagerResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } +} diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..e3ed2e949a6370aa4fbd1a69b0ab773e315b9718 --- /dev/null +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import com.android.internal.R; + +/** + * @hide + */ +public class GrantCredentialsPermissionActivity extends Activity implements View.OnClickListener { + public static final String EXTRAS_ACCOUNT = "account"; + public static final String EXTRAS_AUTH_TOKEN_LABEL = "authTokenLabel"; + public static final String EXTRAS_AUTH_TOKEN_TYPE = "authTokenType"; + public static final String EXTRAS_RESPONSE = "response"; + public static final String EXTRAS_ACCOUNT_TYPE_LABEL = "accountTypeLabel"; + public static final String EXTRAS_PACKAGES = "application"; + public static final String EXTRAS_REQUESTING_UID = "uid"; + private Account mAccount; + private String mAuthTokenType; + private int mUid; + private Bundle mResultBundle = null; + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setContentView(R.layout.grant_credentials_permission); + mAccount = getIntent().getExtras().getParcelable(EXTRAS_ACCOUNT); + mAuthTokenType = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_TYPE); + mUid = getIntent().getExtras().getInt(EXTRAS_REQUESTING_UID); + final String accountTypeLabel = + getIntent().getExtras().getString(EXTRAS_ACCOUNT_TYPE_LABEL); + final String[] packages = getIntent().getExtras().getStringArray(EXTRAS_PACKAGES); + + findViewById(R.id.allow).setOnClickListener(this); + findViewById(R.id.deny).setOnClickListener(this); + + TextView messageView = (TextView) getWindow().findViewById(R.id.message); + String authTokenLabel = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_LABEL); + if (authTokenLabel.length() == 0) { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + mAccount.name, accountTypeLabel)); + } else { + CharSequence grantCredentialsPermissionFormat = getResources().getText( + R.string.grant_credentials_permission_message_with_authtokenlabel_desc); + messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), + authTokenLabel, mAccount.name, accountTypeLabel)); + } + + String[] packageLabels = new String[packages.length]; + final PackageManager pm = getPackageManager(); + for (int i = 0; i < packages.length; i++) { + try { + packageLabels[i] = + pm.getApplicationLabel(pm.getApplicationInfo(packages[i], 0)).toString(); + } catch (PackageManager.NameNotFoundException e) { + packageLabels[i] = packages[i]; + } + } + ((ListView) findViewById(R.id.packages_list)).setAdapter( + new PackagesArrayAdapter(this, packageLabels)); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.allow: + AccountManagerService.getSingleton().grantAppPermission(mAccount, mAuthTokenType, + mUid); + Intent result = new Intent(); + result.putExtra("retry", true); + setResult(RESULT_OK, result); + setAccountAuthenticatorResult(result.getExtras()); + break; + + case R.id.deny: + AccountManagerService.getSingleton().revokeAppPermission(mAccount, mAuthTokenType, + mUid); + setResult(RESULT_CANCELED); + break; + } + finish(); + } + + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + Intent intent = getIntent(); + AccountAuthenticatorResponse accountAuthenticatorResponse = + intent.getParcelableExtra(EXTRAS_RESPONSE); + if (accountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + accountAuthenticatorResponse.onResult(mResultBundle); + } else { + accountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } + + private static class PackagesArrayAdapter extends ArrayAdapter { + protected LayoutInflater mInflater; + private static final int mResource = R.layout.simple_list_item_1; + + public PackagesArrayAdapter(Context context, String[] items) { + super(context, mResource, items); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + static class ViewHolder { + TextView label; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid unneccessary calls + // to findViewById() on each row. + ViewHolder holder; + + // When convertView is not null, we can reuse it directly, there is no need + // to reinflate it. We only inflate a new View when the convertView supplied + // by ListView is null. + if (convertView == null) { + convertView = mInflater.inflate(mResource, null); + + // Creates a ViewHolder and store references to the two children views + // we want to bind data to. + holder = new ViewHolder(); + holder.label = (TextView) convertView.findViewById(R.id.text1); + + convertView.setTag(holder); + } else { + // Get the ViewHolder back to get fast access to the TextView + // and the ImageView. + holder = (ViewHolder) convertView.getTag(); + } + + holder.label.setText(getItem(position)); + + return convertView; + } + } +} diff --git a/core/java/android/accounts/IAccountAuthenticator.aidl b/core/java/android/accounts/IAccountAuthenticator.aidl new file mode 100644 index 0000000000000000000000000000000000000000..88607102883d459633e44de90b91ec705914e83e --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticator.aidl @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.accounts.IAccountAuthenticatorResponse; +import android.accounts.Account; +import android.os.Bundle; + +/** + * Service that allows the interaction with an authentication server. + * @hide + */ +oneway interface IAccountAuthenticator { + /** + * prompts the user for account information and adds the result to the IAccountManager + */ + void addAccount(in IAccountAuthenticatorResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, in Bundle options); + + /** + * prompts the user for the credentials of the account + */ + void confirmCredentials(in IAccountAuthenticatorResponse response, in Account account, + in Bundle options); + + /** + * gets the password by either prompting the user or querying the IAccountManager + */ + void getAuthToken(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * Gets the user-visible label of the given authtoken type. + */ + void getAuthTokenLabel(in IAccountAuthenticatorResponse response, String authTokenType); + + /** + * prompts the user for a new password and writes it to the IAccountManager + */ + void updateCredentials(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * launches an activity that lets the user edit and set the properties for an authenticator + */ + void editProperties(in IAccountAuthenticatorResponse response, String accountType); + + /** + * returns a Bundle where the boolean value BOOLEAN_RESULT_KEY is set if the account has the + * specified features + */ + void hasFeatures(in IAccountAuthenticatorResponse response, in Account account, + in String[] features); + + /** + * Gets whether or not the account is allowed to be removed. + */ + void getAccountRemovalAllowed(in IAccountAuthenticatorResponse response, in Account account); +} diff --git a/core/java/android/accounts/IAccountAuthenticatorResponse.aidl b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl new file mode 100644 index 0000000000000000000000000000000000000000..0c75e507f1e83fd1aab605d3bb397fef586a3c86 --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses from an {@link IAccountAuthenticator} + * @hide + */ +oneway interface IAccountAuthenticatorResponse { + void onResult(in Bundle value); + void onRequestContinued(); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl new file mode 100644 index 0000000000000000000000000000000000000000..0e318c051ff458175ac5d1e9ae2087ed96da3719 --- /dev/null +++ b/core/java/android/accounts/IAccountManager.aidl @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.accounts.IAccountManagerResponse; +import android.accounts.Account; +import android.accounts.AuthenticatorDescription; +import android.os.Bundle; + + +/** + * Central application service that provides account management. + * @hide + */ +interface IAccountManager { + String getPassword(in Account account); + String getUserData(in Account account, String key); + AuthenticatorDescription[] getAuthenticatorTypes(); + Account[] getAccounts(String accountType); + void getAccountsByFeatures(in IAccountManagerResponse response, String accountType, in String[] features); + boolean addAccount(in Account account, String password, in Bundle extras); + void removeAccount(in IAccountManagerResponse response, in Account account); + void invalidateAuthToken(String accountType, String authToken); + String peekAuthToken(in Account account, String authTokenType); + void setAuthToken(in Account account, String authTokenType, String authToken); + void setPassword(in Account account, String password); + void clearPassword(in Account account); + void setUserData(in Account account, String key, String value); + + void getAuthToken(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean notifyOnAuthFailure, boolean expectActivityLaunch, + in Bundle options); + void addAcount(in IAccountManagerResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, boolean expectActivityLaunch, + in Bundle options); + void updateCredentials(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean expectActivityLaunch, in Bundle options); + void editProperties(in IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch); + void confirmCredentials(in IAccountManagerResponse response, in Account account, + in Bundle options, boolean expectActivityLaunch); +} diff --git a/core/java/android/accounts/IAccountManagerResponse.aidl b/core/java/android/accounts/IAccountManagerResponse.aidl new file mode 100644 index 0000000000000000000000000000000000000000..ca1203d1a05891923bbf54c25c1beaf807643816 --- /dev/null +++ b/core/java/android/accounts/IAccountManagerResponse.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses for asynchronous calls to the {@link IAccountManager} + * @hide + */ +oneway interface IAccountManagerResponse { + void onResult(in Bundle value); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountsService.aidl b/core/java/android/accounts/IAccountsService.aidl deleted file mode 100644 index dda513caea71899475e5a07b382453b8a8154828..0000000000000000000000000000000000000000 --- a/core/java/android/accounts/IAccountsService.aidl +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 android.accounts; - -/** - * Central application service that allows querying the list of accounts. - */ -interface IAccountsService { - /** - * Gets the list of Accounts the user has previously logged - * in to. Accounts are of the form "username@domain". - *

- * This method will return an empty array if the device doesn't - * know about any accounts (yet). - * - * @return The accounts. The array will be zero-length if the - * AccountsService doesn't know about any accounts yet. - */ - String[] getAccounts(); - - /** - * This is an interim solution for bypassing a forgotten gesture on the - * unlock screen (it is hidden, please make sure it stays this way!). This - * will be *removed* when the unlock screen design supports additional - * authenticators. - *

- * The user will be presented with username and password fields that are - * called as parameters to this method. If true is returned, the user is - * able to define a new gesture and get back into the system. If false, the - * user can try again. - * - * @param username The username entered. - * @param password The password entered. - * @return Whether to allow the user to bypass the lock screen and define a - * new gesture. - * @hide (The package is already hidden, but just in case someone - * unhides that, this should not be revealed.) - */ - boolean shouldUnlock(String username, String password); -} diff --git a/core/java/android/accounts/NetworkErrorException.java b/core/java/android/accounts/NetworkErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..07f4ce978747d1ecd0285f6d4bdeea600000cd06 --- /dev/null +++ b/core/java/android/accounts/NetworkErrorException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public class NetworkErrorException extends AccountsException { + public NetworkErrorException() { + super(); + } + public NetworkErrorException(String message) { + super(message); + } + public NetworkErrorException(String message, Throwable cause) { + super(message, cause); + } + public NetworkErrorException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/core/java/android/accounts/AccountMonitorListener.java b/core/java/android/accounts/OnAccountsUpdateListener.java similarity index 85% rename from core/java/android/accounts/AccountMonitorListener.java rename to core/java/android/accounts/OnAccountsUpdateListener.java index d0bd9a93f3eb2c3ca081b075f383332b2ab92455..38b371d26e4fbd64ba2f660ded4fa335ee469c52 100644 --- a/core/java/android/accounts/AccountMonitorListener.java +++ b/core/java/android/accounts/OnAccountsUpdateListener.java @@ -19,11 +19,11 @@ package android.accounts; /** * An interface that contains the callback used by the AccountMonitor */ -public interface AccountMonitorListener { +public interface OnAccountsUpdateListener { /** * This invoked when the AccountMonitor starts up and whenever the account * set changes. - * @param currentAccounts the current accounts + * @param accounts the current accounts */ - void onAccountsUpdated(String[] currentAccounts); + void onAccountsUpdated(Account[] accounts); } diff --git a/core/java/android/accounts/OperationCanceledException.java b/core/java/android/accounts/OperationCanceledException.java new file mode 100644 index 0000000000000000000000000000000000000000..896d194e221ceb6e15ac3bdb573f9c27302c2e97 --- /dev/null +++ b/core/java/android/accounts/OperationCanceledException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.accounts; + +public class OperationCanceledException extends AccountsException { + public OperationCanceledException() { + super(); + } + public OperationCanceledException(String message) { + super(message); + } + public OperationCanceledException(String message, Throwable cause) { + super(message, cause); + } + public OperationCanceledException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/package.html b/core/java/android/accounts/package.html deleted file mode 100755 index c9f96a66ab3bc86235e6c4a3fe28bbaa96c12c9b..0000000000000000000000000000000000000000 --- a/core/java/android/accounts/package.html +++ /dev/null @@ -1,5 +0,0 @@ - - -{@hide} - - diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index f2905a7d80c5bf1de2cacf558f027f656fef42fd..49ebce38f7780b4111faac698922c8e566b7c421 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -24,6 +24,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IIntentSender; +import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -34,6 +35,7 @@ import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -788,8 +790,8 @@ public class Activity extends ContextThemeWrapper * @see #onPostCreate */ protected void onCreate(Bundle savedInstanceState) { - mVisibleFromClient = mWindow.getWindowStyle().getBoolean( - com.android.internal.R.styleable.Window_windowNoDisplay, true); + mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( + com.android.internal.R.styleable.Window_windowNoDisplay, false); mCalled = true; } @@ -1752,8 +1754,17 @@ public class Activity extends ContextThemeWrapper * *

If the focused view didn't want this event, this method is called. * - *

The default implementation handles KEYCODE_BACK to stop the activity - * and go back, and other default key handling if configured with {@link #setDefaultKeyMode}. + *

The default implementation takes care of {@link KeyEvent#KEYCODE_BACK} + * by calling {@link #onBackPressed()}, though the behavior varies based + * on the application compatibility mode: for + * {@link android.os.Build.VERSION_CODES#ECLAIR} or later applications, + * it will set up the dispatch to call {@link #onKeyUp} where the action + * will be performed; for earlier applications, it will perform the + * action immediately in on-down, as those versions of the platform + * behaved. + * + *

Other additional default key handling may be performed + * if configured with {@link #setDefaultKeyMode}. * * @return Return true to prevent this event from being propagated * further, or false to indicate that you have not handled @@ -1762,16 +1773,24 @@ public class Activity extends ContextThemeWrapper * @see android.view.KeyEvent */ public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - finish(); + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (getApplicationInfo().targetSdkVersion + >= Build.VERSION_CODES.ECLAIR) { + event.startTracking(); + } else { + onBackPressed(); + } return true; } if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) { return false; } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) { - return getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, - keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE); + if (getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, + keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE)) { + return true; + } + return false; } else { // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_* boolean clearSpannable = false; @@ -1780,8 +1799,8 @@ public class Activity extends ContextThemeWrapper clearSpannable = true; handled = false; } else { - handled = TextKeyListener.getInstance().onKeyDown(null, mDefaultKeySsb, - keyCode, event); + handled = TextKeyListener.getInstance().onKeyDown( + null, mDefaultKeySsb, keyCode, event); if (handled && mDefaultKeySsb.length() > 0) { // something useable has been typed - dispatch it now. @@ -1812,12 +1831,24 @@ public class Activity extends ContextThemeWrapper } } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Called when a key was released and not handled by any of the views * inside of the activity. So, for example, key presses while the cursor * is inside a TextView will not trigger the event (unless it is a navigation * to another object) because TextView handles its own key presses. * + *

The default implementation handles KEYCODE_BACK to stop the activity + * and go back. + * * @return Return true to prevent this event from being propagated * further, or false to indicate that you have not handled * this event and it should continue to be propagated. @@ -1825,6 +1856,14 @@ public class Activity extends ContextThemeWrapper * @see KeyEvent */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (getApplicationInfo().targetSdkVersion + >= Build.VERSION_CODES.ECLAIR) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + onBackPressed(); + return true; + } + } return false; } @@ -1837,6 +1876,15 @@ public class Activity extends ContextThemeWrapper return false; } + /** + * Called when the activity has detected the user's press of the back + * key. The default implementation simply finishes the current activity, + * but you can override this to do whatever you want. + */ + public void onBackPressed() { + finish(); + } + /** * Called when a touch screen event was not handled by any of the views * under it. This is most useful to process touch events that happen @@ -1909,9 +1957,10 @@ public class Activity extends ContextThemeWrapper /** * Called when the current {@link Window} of the activity gains or loses * focus. This is the best indicator of whether this activity is visible - * to the user. + * to the user. The default implementation clears the key tracking + * state, so should always be called. * - *

Note that this provides information what global focus state, which + *

Note that this provides information about global focus state, which * is managed independently of activity lifecycles. As such, while focus * changes will generally have some relation to lifecycle changes (an * activity that is stopped will not generally get window focus), you @@ -1930,10 +1979,31 @@ public class Activity extends ContextThemeWrapper * * @see #hasWindowFocus() * @see #onResume + * @see View#onWindowFocusChanged(boolean) */ public void onWindowFocusChanged(boolean hasFocus) { } + /** + * Called when the main window associated with the activity has been + * attached to the window manager. + * See {@link View#onAttachedToWindow() View.onAttachedToWindow()} + * for more information. + * @see View#onAttachedToWindow + */ + public void onAttachedToWindow() { + } + + /** + * Called when the main window associated with the activity has been + * detached from the window manager. + * See {@link View#onDetachedFromWindow() View.onDetachedFromWindow()} + * for more information. + * @see View#onDetachedFromWindow + */ + public void onDetachedFromWindow() { + } + /** * Returns true if this activity's main window currently has window focus. * Note that this is not the same as the view itself having focus. @@ -1964,10 +2034,14 @@ public class Activity extends ContextThemeWrapper */ public boolean dispatchKeyEvent(KeyEvent event) { onUserInteraction(); - if (getWindow().superDispatchKeyEvent(event)) { + Window win = getWindow(); + if (win.superDispatchKeyEvent(event)) { return true; } - return event.dispatch(this); + View decor = mDecor; + if (decor == null) decor = win.getDecorView(); + return event.dispatch(this, decor != null + ? decor.getKeyDispatcherState() : null, this); } /** @@ -2394,6 +2468,7 @@ public class Activity extends ContextThemeWrapper * * @param id The id of the managed dialog. * + * @see Dialog * @see #onCreateDialog(int) * @see #onPrepareDialog(int, Dialog) * @see #dismissDialog(int) @@ -2479,16 +2554,17 @@ public class Activity extends ContextThemeWrapper /** * This hook is called when the user signals the desire to start a search. * - *

You can use this function as a simple way to launch the search UI, in response to a - * menu item, search button, or other widgets within your activity. Unless overidden, - * calling this function is the same as calling: - *

The default implementation simply calls - * {@link #startSearch startSearch(null, false, null, false)}, launching a local search. + *

You can use this function as a simple way to launch the search UI, in response to a + * menu item, search button, or other widgets within your activity. Unless overidden, + * calling this function is the same as calling + * {@link #startSearch startSearch(null, false, null, false)}, which launches + * search for the current activity as specified in its manifest, see {@link SearchManager}. * *

You can override this function to force global search, e.g. in response to a dedicated * search key, or to block search entirely (by simply returning false). * - * @return Returns true if search launched, false if activity blocks it + * @return Returns {@code true} if search launched, and {@code false} if activity blocks it. + * The default implementation always returns {@code true}. * * @see android.app.SearchManager */ @@ -2534,6 +2610,21 @@ public class Activity extends ContextThemeWrapper appSearchData, globalSearch); } + /** + * Similar to {@link #startSearch}, but actually fires off the search query after invoking + * the search dialog. Made available for testing purposes. + * + * @param query The query to trigger. If empty, the request will be ignored. + * @param appSearchData An application can insert application-specific + * context here, in order to improve quality or specificity of its own + * searches. This data will be returned with SEARCH intent(s). Null if + * no extra data is required. + */ + public void triggerSearch(String query, Bundle appSearchData) { + ensureSearchManager(); + mSearchManager.triggerSearch(query, getComponentName(), appSearchData); + } + /** * Request that key events come to this activity. Use this if your * activity has no views with focus, but the activity still wants @@ -2608,10 +2699,8 @@ public class Activity extends ContextThemeWrapper } @Override - protected void onApplyThemeResource(Resources.Theme theme, - int resid, - boolean first) - { + protected void onApplyThemeResource(Resources.Theme theme, int resid, + boolean first) { if (mParent == null) { super.onApplyThemeResource(theme, resid, first); } else { @@ -2681,6 +2770,68 @@ public class Activity extends ContextThemeWrapper } } + /** + * Like {@link #startActivityForResult(Intent, int)}, but allowing you + * to use a IntentSender to describe the activity to be started. If + * the IntentSender is for an activity, that activity will be started + * as if you had called the regular {@link #startActivityForResult(Intent, int)} + * here; otherwise, its associated action will be executed (such as + * sending a broadcast) as if you had called + * {@link IntentSender#sendIntent IntentSender.sendIntent} on it. + * + * @param intent The IntentSender to launch. + * @param requestCode If >= 0, this code will be returned in + * onActivityResult() when the activity exits. + * @param fillInIntent If non-null, this will be provided as the + * intent parameter to {@link IntentSender#sendIntent}. + * @param flagsMask Intent flags in the original IntentSender that you + * would like to change. + * @param flagsValues Desired values for any bits set in + * flagsMask + * @param extraFlags Always set to 0. + */ + public void startIntentSenderForResult(IntentSender intent, int requestCode, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + if (mParent == null) { + startIntentSenderForResultInner(intent, requestCode, fillInIntent, + flagsMask, flagsValues, this); + } else { + mParent.startIntentSenderFromChild(this, intent, requestCode, + fillInIntent, flagsMask, flagsValues, extraFlags); + } + } + + private void startIntentSenderForResultInner(IntentSender intent, int requestCode, + Intent fillInIntent, int flagsMask, int flagsValues, Activity activity) + throws IntentSender.SendIntentException { + try { + String resolvedType = null; + if (fillInIntent != null) { + resolvedType = fillInIntent.resolveTypeIfNeeded(getContentResolver()); + } + int result = ActivityManagerNative.getDefault() + .startActivityIntentSender(mMainThread.getApplicationThread(), intent, + fillInIntent, resolvedType, mToken, activity.mEmbeddedID, + requestCode, flagsMask, flagsValues); + if (result == IActivityManager.START_CANCELED) { + throw new IntentSender.SendIntentException(); + } + Instrumentation.checkStartActivityResult(result, null); + } catch (RemoteException e) { + } + if (requestCode >= 0) { + // If this start is requesting a result, we can avoid making + // the activity visible until the result is received. Setting + // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the + // activity hidden during this time, to avoid flickering. + // This can only be done when a result is requested because + // that guarantees we will get information back when the + // activity is finished, no matter what happens to it. + mStartedActivity = true; + } + } + /** * Launch a new activity. You will not receive any information about when * the activity exits. This implementation overrides the base version, @@ -2704,6 +2855,28 @@ public class Activity extends ContextThemeWrapper startActivityForResult(intent, -1); } + /** + * Like {@link #startActivity(Intent)}, but taking a IntentSender + * to start; see + * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)} + * for more information. + * + * @param intent The IntentSender to launch. + * @param fillInIntent If non-null, this will be provided as the + * intent parameter to {@link IntentSender#sendIntent}. + * @param flagsMask Intent flags in the original IntentSender that you + * would like to change. + * @param flagsValues Desired values for any bits set in + * flagsMask + * @param extraFlags Always set to 0. + */ + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + startIntentSenderForResult(intent, -1, fillInIntent, flagsMask, + flagsValues, extraFlags); + } + /** * A special variation to launch an activity only if a new activity * instance is needed to handle the given Intent. In other words, this is @@ -2824,6 +2997,37 @@ public class Activity extends ContextThemeWrapper } } + /** + * Like {@link #startActivityFromChild(Activity, Intent, int)}, but + * taking a IntentSender; see + * {@link #startIntentSenderForResult(IntentSender, int, Intent, int, int, int)} + * for more information. + */ + public void startIntentSenderFromChild(Activity child, IntentSender intent, + int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, + int extraFlags) + throws IntentSender.SendIntentException { + startIntentSenderForResultInner(intent, requestCode, fillInIntent, + flagsMask, flagsValues, child); + } + + /** + * Call immediately after one of the flavors of {@link #startActivity(Intent)} + * or {@link #finish} to specify an explicit transition animation to + * perform next. + * @param enterAnim A resource ID of the animation resource to use for + * the incoming activity. Use 0 for no animation. + * @param exitAnim A resource ID of the animation resource to use for + * the outgoing activity. Use 0 for no animation. + */ + public void overridePendingTransition(int enterAnim, int exitAnim) { + try { + ActivityManagerNative.getDefault().overridePendingTransition( + mToken, getPackageName(), enterAnim, exitAnim); + } catch (RemoteException e) { + } + } + /** * Call this to set the result that your activity will return to its * caller. @@ -3255,7 +3459,7 @@ public class Activity extends ContextThemeWrapper throw new IllegalArgumentException("no ident"); } } - mSearchManager.setIdent(ident); + mSearchManager.setIdent(ident, getComponentName()); } @Override diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 07520c9d65f7dc57cc2bb9ee08bfe621006fe32d..d709debbb28688f116acf35cb1f3410e1df955d6 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -22,10 +22,12 @@ import android.content.Intent; import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; import android.graphics.Bitmap; +import android.os.Debug; import android.os.RemoteException; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; +import android.os.SystemProperties; import android.text.TextUtils; import java.util.List; @@ -45,6 +47,26 @@ public class ActivityManager { mHandler = handler; } + /** + * Return the approximate per-application memory class of the current + * device. This gives you an idea of how hard a memory limit you should + * impose on your application to let the overall system work best. The + * returned value is in megabytes; the baseline Android memory class is + * 16 (which happens to be the Java heap limit of those devices); some + * device with more memory may return 24 or even higher numbers. + */ + public int getMemoryClass() { + return staticGetMemoryClass(); + } + + /** @hide */ + static public int staticGetMemoryClass() { + // Really brain dead right now -- just take this from the configured + // vm heap size, and assume it is in megabytes and thus ends with "m". + String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m"); + return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); + } + /** * Information you can retrieve about tasks that the user has most recently * started or visited. @@ -288,6 +310,11 @@ public class ActivityManager { */ public int pid; + /** + * The UID that owns this service. + */ + public int uid; + /** * The name of the process this service runs in. */ @@ -299,7 +326,7 @@ public class ActivityManager { public boolean foreground; /** - * The time when the service was first made activity, either by someone + * The time when the service was first made active, either by someone * starting or binding to it. */ public long activeSince; @@ -332,6 +359,48 @@ public class ActivityManager { */ public long restarting; + /** + * Bit for {@link #flags}: set if this service has been + * explicitly started. + */ + public static final int FLAG_STARTED = 1<<0; + + /** + * Bit for {@link #flags}: set if the service has asked to + * run as a foreground process. + */ + public static final int FLAG_FOREGROUND = 1<<1; + + /** + * Bit for {@link #flags): set if the service is running in a + * core system process. + */ + public static final int FLAG_SYSTEM_PROCESS = 1<<2; + + /** + * Bit for {@link #flags): set if the service is running in a + * persistent process. + */ + public static final int FLAG_PERSISTENT_PROCESS = 1<<3; + + /** + * Running flags. + */ + public int flags; + + /** + * For special services that are bound to by system code, this is + * the package that holds the binding. + */ + public String clientPackage; + + /** + * For special services that are bound to by system code, this is + * a string resource providing a user-visible label for who the + * client is. + */ + public int clientLabel; + public RunningServiceInfo() { } @@ -342,6 +411,7 @@ public class ActivityManager { public void writeToParcel(Parcel dest, int flags) { ComponentName.writeToParcel(service, dest); dest.writeInt(pid); + dest.writeInt(uid); dest.writeString(process); dest.writeInt(foreground ? 1 : 0); dest.writeLong(activeSince); @@ -350,11 +420,15 @@ public class ActivityManager { dest.writeInt(crashCount); dest.writeLong(lastActivityTime); dest.writeLong(restarting); + dest.writeInt(this.flags); + dest.writeString(clientPackage); + dest.writeInt(clientLabel); } public void readFromParcel(Parcel source) { service = ComponentName.readFromParcel(source); pid = source.readInt(); + uid = source.readInt(); process = source.readString(); foreground = source.readInt() != 0; activeSince = source.readLong(); @@ -363,6 +437,9 @@ public class ActivityManager { crashCount = source.readInt(); lastActivityTime = source.readLong(); restarting = source.readLong(); + flags = source.readInt(); + clientPackage = source.readString(); + clientLabel = source.readInt(); } public static final Creator CREATOR = new Creator() { @@ -400,6 +477,22 @@ public class ActivityManager { } } + /** + * Returns a PendingIntent you can start to show a control panel for the + * given running service. If the service does not have a control panel, + * null is returned. + */ + public PendingIntent getRunningServiceControlPanel(ComponentName service) + throws SecurityException { + try { + return ActivityManagerNative.getDefault() + .getRunningServiceControlPanel(service); + } catch (RemoteException e) { + // System dead, we will be dead too soon! + return null; + } + } + /** * Information you can retrieve about the available memory through * {@link ActivityManager#getMemoryInfo}. @@ -613,6 +706,11 @@ public class ActivityManager { */ public int pid; + /** + * The user id of this process. + */ + public int uid; + public String pkgList[]; /** @@ -666,8 +764,51 @@ public class ActivityManager { */ public int lru; + /** + * Constant for {@link #importanceReasonCode}: nothing special has + * been specified for the reason for this level. + */ + public static final int REASON_UNKNOWN = 0; + + /** + * Constant for {@link #importanceReasonCode}: one of the application's + * content providers is being used by another process. The pid of + * the client process is in {@link #importanceReasonPid} and the + * target provider in this process is in + * {@link #importanceReasonComponent}. + */ + public static final int REASON_PROVIDER_IN_USE = 1; + + /** + * Constant for {@link #importanceReasonCode}: one of the application's + * content providers is being used by another process. The pid of + * the client process is in {@link #importanceReasonPid} and the + * target provider in this process is in + * {@link #importanceReasonComponent}. + */ + public static final int REASON_SERVICE_IN_USE = 2; + + /** + * The reason for {@link #importance}, if any. + */ + public int importanceReasonCode; + + /** + * For the specified values of {@link #importanceReasonCode}, this + * is the process ID of the other process that is a client of this + * process. This will be 0 if no other process is using this one. + */ + public int importanceReasonPid; + + /** + * For the specified values of {@link #importanceReasonCode}, this + * is the name of the component that is being used in this process. + */ + public ComponentName importanceReasonComponent; + public RunningAppProcessInfo() { importance = IMPORTANCE_FOREGROUND; + importanceReasonCode = REASON_UNKNOWN; } public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) { @@ -683,17 +824,25 @@ public class ActivityManager { public void writeToParcel(Parcel dest, int flags) { dest.writeString(processName); dest.writeInt(pid); + dest.writeInt(uid); dest.writeStringArray(pkgList); dest.writeInt(importance); dest.writeInt(lru); + dest.writeInt(importanceReasonCode); + dest.writeInt(importanceReasonPid); + ComponentName.writeToParcel(importanceReasonComponent, dest); } public void readFromParcel(Parcel source) { processName = source.readString(); pid = source.readInt(); + uid = source.readInt(); pkgList = source.readStringArray(); importance = source.readInt(); lru = source.readInt(); + importanceReasonCode = source.readInt(); + importanceReasonPid = source.readInt(); + importanceReasonComponent = ComponentName.readFromParcel(source); } public static final Creator CREATOR = @@ -726,6 +875,22 @@ public class ActivityManager { } } + /** + * Return information about the memory usage of one or more processes. + * + * @param pids The pids of the processes whose memory usage is to be + * retrieved. + * @return Returns an array of memory information, one for each + * requested pid. + */ + public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) { + try { + return ActivityManagerNative.getDefault().getProcessMemoryInfo(pids); + } catch (RemoteException e) { + return null; + } + } + /** * Have the system perform a force stop of everything associated with * the given application package. All processes that share its uid diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 447512a01d10395331d7c62d9ed08790a6382188..3b8aee9afc23ed34c70d7a49a9223c95c5ba93db 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IIntentSender; import android.content.IIntentReceiver; +import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; @@ -29,6 +30,7 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Binder; import android.os.Bundle; +import android.os.Debug; import android.os.Parcelable; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -39,9 +41,6 @@ import android.text.TextUtils; import android.util.Config; import android.util.Log; -import java.io.FileNotFoundException; -import java.io.FileDescriptor; -import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -146,6 +145,30 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeInt(result); return true; } + + case START_ACTIVITY_INTENT_SENDER_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + IBinder b = data.readStrongBinder(); + IApplicationThread app = ApplicationThreadNative.asInterface(b); + IntentSender intent = IntentSender.CREATOR.createFromParcel(data); + Intent fillInIntent = null; + if (data.readInt() != 0) { + fillInIntent = Intent.CREATOR.createFromParcel(data); + } + String resolvedType = data.readString(); + IBinder resultTo = data.readStrongBinder(); + String resultWho = data.readString(); + int requestCode = data.readInt(); + int flagsMask = data.readInt(); + int flagsValues = data.readInt(); + int result = startActivityIntentSender(app, intent, + fillInIntent, resolvedType, resultTo, resultWho, + requestCode, flagsMask, flagsValues); + reply.writeNoException(); + reply.writeInt(result); + return true; + } case START_NEXT_MATCHING_ACTIVITY_TRANSACTION: { @@ -292,8 +315,12 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM case ACTIVITY_IDLE_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); + Configuration config = null; + if (data.readInt() != 0) { + config = Configuration.CREATOR.createFromParcel(data); + } if (token != null) { - activityIdle(token); + activityIdle(token, config); } reply.writeNoException(); return true; @@ -512,6 +539,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_RUNNING_SERVICE_CONTROL_PANEL_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + ComponentName comp = ComponentName.CREATOR.createFromParcel(data); + PendingIntent pi = getRunningServiceControlPanel(comp); + reply.writeNoException(); + PendingIntent.writePendingIntentOrNullToParcel(pi, reply); + return true; + } + case START_SERVICE_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder b = data.readStrongBinder(); @@ -551,8 +587,13 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); ComponentName className = ComponentName.readFromParcel(data); IBinder token = data.readStrongBinder(); - boolean isForeground = data.readInt() != 0; - setServiceForeground(className, token, isForeground); + int id = data.readInt(); + Notification notification = null; + if (data.readInt() != 0) { + notification = Notification.CREATOR.createFromParcel(data); + } + boolean removeNotification = data.readInt() != 0; + setServiceForeground(className, token, id, notification, removeNotification); reply.writeNoException(); return true; } @@ -606,7 +647,10 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM case SERVICE_DONE_EXECUTING_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); - serviceDoneExecuting(token); + int type = data.readInt(); + int startId = data.readInt(); + int res = data.readInt(); + serviceDoneExecuting(token, type, startId, res); reply.writeNoException(); return true; } @@ -935,13 +979,6 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case SYSTEM_READY_TRANSACTION: { - data.enforceInterface(IActivityManager.descriptor); - systemReady(); - reply.writeNoException(); - return true; - } - case HANDLE_APPLICATION_ERROR_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder app = data.readStrongBinder(); @@ -1102,6 +1139,34 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeNoException(); return true; } + + case GET_PROCESS_MEMORY_INFO_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int[] pids = data.createIntArray(); + Debug.MemoryInfo[] res = getProcessMemoryInfo(pids); + reply.writeNoException(); + reply.writeTypedArray(res, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + return true; + } + + case KILL_APPLICATION_PROCESS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String processName = data.readString(); + int uid = data.readInt(); + killApplicationProcess(processName, uid); + reply.writeNoException(); + return true; + } + + case OVERRIDE_PENDING_TRANSITION_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + String packageName = data.readString(); + int enterAnim = data.readInt(); + int exitAnim = data.readInt(); + overridePendingTransition(token, packageName, enterAnim, exitAnim); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -1152,6 +1217,34 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); return result; } + public int startActivityIntentSender(IApplicationThread caller, + IntentSender intent, Intent fillInIntent, String resolvedType, + IBinder resultTo, String resultWho, int requestCode, + int flagsMask, int flagsValues) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(caller != null ? caller.asBinder() : null); + intent.writeToParcel(data, 0); + if (fillInIntent != null) { + data.writeInt(1); + fillInIntent.writeToParcel(data, 0); + } else { + data.writeInt(0); + } + data.writeString(resolvedType); + data.writeStrongBinder(resultTo); + data.writeString(resultWho); + data.writeInt(requestCode); + data.writeInt(flagsMask); + data.writeInt(flagsValues); + mRemote.transact(START_ACTIVITY_INTENT_SENDER_TRANSACTION, data, reply, 0); + reply.readException(); + int result = reply.readInt(); + reply.recycle(); + data.recycle(); + return result; + } public boolean startNextMatchingActivity(IBinder callingActivity, Intent intent) throws RemoteException { Parcel data = Parcel.obtain(); @@ -1308,12 +1401,18 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - public void activityIdle(IBinder token) throws RemoteException + public void activityIdle(IBinder token, Configuration config) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); + if (config != null) { + data.writeInt(1); + config.writeToParcel(data, 0); + } else { + data.writeInt(0); + } mRemote.transact(ACTIVITY_IDLE_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); data.recycle(); @@ -1616,6 +1715,21 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } + public PendingIntent getRunningServiceControlPanel(ComponentName service) + throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + service.writeToParcel(data, 0); + mRemote.transact(GET_RUNNING_SERVICE_CONTROL_PANEL_TRANSACTION, data, reply, 0); + reply.readException(); + PendingIntent res = PendingIntent.readPendingIntentOrNullFromParcel(reply); + data.recycle(); + reply.recycle(); + return res; + } + public ComponentName startService(IApplicationThread caller, Intent service, String resolvedType) throws RemoteException { @@ -1664,13 +1778,20 @@ class ActivityManagerProxy implements IActivityManager return res; } public void setServiceForeground(ComponentName className, IBinder token, - boolean isForeground) throws RemoteException { + int id, Notification notification, boolean removeNotification) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); ComponentName.writeToParcel(className, data); data.writeStrongBinder(token); - data.writeInt(isForeground ? 1 : 0); + data.writeInt(id); + if (notification != null) { + data.writeInt(1); + notification.writeToParcel(data, 0); + } else { + data.writeInt(0); + } + data.writeInt(removeNotification ? 1 : 0); mRemote.transact(SET_SERVICE_FOREGROUND_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -1737,11 +1858,15 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } - public void serviceDoneExecuting(IBinder token) throws RemoteException { + public void serviceDoneExecuting(IBinder token, int type, int startId, + int res) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); + data.writeInt(type); + data.writeInt(startId); + data.writeInt(res); mRemote.transact(SERVICE_DONE_EXECUTING_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); data.recycle(); @@ -2212,16 +2337,6 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - public void systemReady() throws RemoteException - { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - data.writeInterfaceToken(IActivityManager.descriptor); - mRemote.transact(SYSTEM_READY_TRANSACTION, data, reply, 0); - reply.readException(); - data.recycle(); - reply.recycle(); - } public boolean testIsSystemReady() { /* this base class version is never called */ @@ -2408,6 +2523,47 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } + + public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeIntArray(pids); + mRemote.transact(GET_PROCESS_MEMORY_INFO_TRANSACTION, data, reply, 0); + reply.readException(); + Debug.MemoryInfo[] res = reply.createTypedArray(Debug.MemoryInfo.CREATOR); + data.recycle(); + reply.recycle(); + return res; + } + + public void killApplicationProcess(String processName, int uid) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(processName); + data.writeInt(uid); + mRemote.transact(KILL_APPLICATION_PROCESS_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + public void overridePendingTransition(IBinder token, String packageName, + int enterAnim, int exitAnim) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + data.writeString(packageName); + data.writeInt(enterAnim); + data.writeInt(exitAnim); + mRemote.transact(OVERRIDE_PENDING_TRANSITION_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e045105ce94539a742f04e65882d7356309a1ad1..b116bf8fd3064e0b3de7cf60ff16f6ff6b4425a4 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -68,6 +68,7 @@ import android.view.WindowManagerImpl; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; +import com.android.internal.os.SamplingProfilerIntegration; import com.android.internal.util.ArrayUtils; import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl; @@ -87,6 +88,8 @@ import java.util.Map; import java.util.TimeZone; import java.util.regex.Pattern; +import dalvik.system.SamplingProfiler; + final class IntentReceiverLeaked extends AndroidRuntimeException { public IntentReceiverLeaked(String msg) { super(msg); @@ -119,14 +122,15 @@ public final class ActivityThread { private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; private static final boolean DEBUG_BROADCAST = false; private static final boolean DEBUG_RESULTS = false; - private static final boolean DEBUG_BACKUP = true; + private static final boolean DEBUG_BACKUP = false; + private static final boolean DEBUG_CONFIGURATION = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";"); private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003; private static final int LOG_ON_PAUSE_CALLED = 30021; private static final int LOG_ON_RESUME_CALLED = 30022; - + public static final ActivityThread currentActivityThread() { return (ActivityThread)sThreadLocal.get(); } @@ -289,9 +293,9 @@ public final class ActivityThread { } public PackageInfo(ActivityThread activityThread, String name, - Context systemContext) { + Context systemContext, ApplicationInfo info) { mActivityThread = activityThread; - mApplicationInfo = new ApplicationInfo(); + mApplicationInfo = info != null ? info : new ApplicationInfo(); mApplicationInfo.packageName = name; mPackageName = name; mAppDir = null; @@ -314,7 +318,7 @@ public final class ActivityThread { public ApplicationInfo getApplicationInfo() { return mApplicationInfo; } - + public boolean isSecurityViolation() { return mSecurityViolation; } @@ -322,7 +326,7 @@ public final class ActivityThread { /** * Gets the array of shared libraries that are listed as * used by the given package. - * + * * @param packageName the name of the package (note: not its * file name) * @return null-ok; the array of shared libraries, each one @@ -350,7 +354,7 @@ public final class ActivityThread { * result is a single string with the names of the libraries * separated by colons, or null if both lists * were null or empty. - * + * * @param list1 null-ok; the first list * @param list2 null-ok; the second list * @return null-ok; the combination @@ -378,7 +382,7 @@ public final class ActivityThread { if (dupCheck && ArrayUtils.contains(list1, s)) { continue; } - + if (first) { first = false; } else { @@ -390,7 +394,7 @@ public final class ActivityThread { return result.toString(); } - + public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader != null) { @@ -428,7 +432,7 @@ public final class ActivityThread { if ((mSharedLibraries != null) || (instrumentationLibs != null)) { - zip = + zip = combineLibs(mSharedLibraries, instrumentationLibs) + ':' + zip; } @@ -481,13 +485,14 @@ public final class ActivityThread { return mResources; } - public Application makeApplication(boolean forceDefaultAppClass) { + public Application makeApplication(boolean forceDefaultAppClass, + Instrumentation instrumentation) { if (mApplication != null) { return mApplication; } - + Application app = null; - + String appClass = mApplicationInfo.className; if (forceDefaultAppClass || (appClass == null)) { appClass = "android.app.Application"; @@ -508,9 +513,23 @@ public final class ActivityThread { } } mActivityThread.mAllApplications.add(app); - return mApplication = app; + mApplication = app; + + if (instrumentation != null) { + try { + instrumentation.callApplicationOnCreate(app); + } catch (Exception e) { + if (!instrumentation.onException(app, e)) { + throw new RuntimeException( + "Unable to create application " + app.getClass().getName() + + ": " + e.toString(), e); + } + } + } + + return app; } - + public void removeContextRegistrations(Context context, String who, String what) { HashMap rmap = @@ -643,13 +662,13 @@ public final class ActivityThread { final static class InnerReceiver extends IIntentReceiver.Stub { final WeakReference mDispatcher; final ReceiverDispatcher mStrongRef; - + InnerReceiver(ReceiverDispatcher rd, boolean strong) { mDispatcher = new WeakReference(rd); mStrongRef = strong ? rd : null; } public void performReceive(Intent intent, int resultCode, - String data, Bundle extras, boolean ordered) { + String data, Bundle extras, boolean ordered, boolean sticky) { ReceiverDispatcher rd = mDispatcher.get(); if (DEBUG_BROADCAST) { int seq = intent.getIntExtra("seq", -1); @@ -657,11 +676,12 @@ public final class ActivityThread { + " to " + rd); } if (rd != null) { - rd.performReceive(intent, resultCode, data, extras, ordered); + rd.performReceive(intent, resultCode, data, extras, + ordered, sticky); } } } - + final IIntentReceiver.Stub mIIntentReceiver; final BroadcastReceiver mReceiver; final Context mContext; @@ -677,6 +697,7 @@ public final class ActivityThread { private String mCurData; private Bundle mCurMap; private boolean mCurOrdered; + private boolean mCurSticky; public void run() { BroadcastReceiver receiver = mReceiver; @@ -702,6 +723,7 @@ public final class ActivityThread { receiver.setResult(mCurCode, mCurData, mCurMap); receiver.clearAbortBroadcast(); receiver.setOrderedHint(mCurOrdered); + receiver.setInitialStickyHint(mCurSticky); receiver.onReceive(mContext, intent); } catch (Exception e) { if (mRegistered && mCurOrdered) { @@ -770,7 +792,7 @@ public final class ActivityThread { BroadcastReceiver getIntentReceiver() { return mReceiver; } - + IIntentReceiver getIIntentReceiver() { return mIIntentReceiver; } @@ -784,7 +806,7 @@ public final class ActivityThread { } public void performReceive(Intent intent, int resultCode, - String data, Bundle extras, boolean ordered) { + String data, Bundle extras, boolean ordered, boolean sticky) { if (DEBUG_BROADCAST) { int seq = intent.getIntExtra("seq", -1); Log.i(TAG, "Enqueueing broadcast " + intent.getAction() + " seq=" + seq @@ -796,6 +818,7 @@ public final class ActivityThread { args.mCurData = data; args.mCurMap = extras; args.mCurOrdered = ordered; + args.mCurSticky = sticky; if (!mActivityThread.post(args)) { if (mRegistered) { IActivityManager mgr = ActivityManagerNative.getDefault(); @@ -901,7 +924,7 @@ public final class ActivityThread { private static class InnerConnection extends IServiceConnection.Stub { final WeakReference mDispatcher; - + InnerConnection(ServiceDispatcher sd) { mDispatcher = new WeakReference(sd); } @@ -913,7 +936,7 @@ public final class ActivityThread { } } } - + private final HashMap mActiveConnections = new HashMap(); @@ -965,7 +988,7 @@ public final class ActivityThread { IServiceConnection getIServiceConnection() { return mIServiceConnection; } - + int getFlags() { return mFlags; } @@ -1112,6 +1135,7 @@ public final class ActivityThread { boolean stopped; boolean hideForNow; Configuration newConfig; + Configuration createdConfig; ActivityRecord nextIdle; ActivityInfo activityInfo; @@ -1191,7 +1215,7 @@ public final class ActivityThread { + " mode=" + backupMode + "}"; } } - + private static final class CreateServiceData { IBinder token; ServiceInfo info; @@ -1215,6 +1239,7 @@ public final class ActivityThread { private static final class ServiceArgsData { IBinder token; int startId; + int flags; Intent args; public String toString() { return "ServiceArgsData{token=" + token + " startId=" + startId @@ -1270,10 +1295,10 @@ public final class ActivityThread { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; private static final String TWO_COUNT_COLUMNS = "%17s %8d %17s %8d"; - + // Formatting for checkin service - update version if row format changes private static final int ACTIVITY_THREAD_CHECKIN_VERSION = 1; - + public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges) { queueOrSendMessage( @@ -1342,7 +1367,7 @@ public final class ActivityThread { synchronized (mRelaunchingActivities) { mRelaunchingActivities.add(r); } - + queueOrSendMessage(H.RELAUNCH_ACTIVITY, r, configChanges); } @@ -1417,10 +1442,11 @@ public final class ActivityThread { } public final void scheduleServiceArgs(IBinder token, int startId, - Intent args) { + int flags ,Intent args) { ServiceArgsData s = new ServiceArgsData(); s.token = token; s.startId = startId; + s.flags = flags; s.args = args; queueOrSendMessage(H.SERVICE_ARGS, s); @@ -1436,7 +1462,6 @@ public final class ActivityThread { Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, int debugMode, boolean isRestrictedBackupMode, Configuration config, Map services) { - Process.setArgV0(processName); if (services != null) { // Setup the service cache in the ServiceManager @@ -1461,6 +1486,10 @@ public final class ActivityThread { queueOrSendMessage(H.EXIT_APPLICATION, null); } + public final void scheduleSuicide() { + queueOrSendMessage(H.SUICIDE, null); + } + public void requestThumbnail(IBinder token) { queueOrSendMessage(H.REQUEST_THUMBNAIL, token); } @@ -1504,11 +1533,11 @@ public final class ActivityThread { // correctly ordered, since these are one-way calls and the binder driver // applies transaction ordering per object for such calls. public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, - int resultCode, String dataStr, Bundle extras, boolean ordered) - throws RemoteException { - receiver.performReceive(intent, resultCode, dataStr, extras, ordered); + int resultCode, String dataStr, Bundle extras, boolean ordered, + boolean sticky) throws RemoteException { + receiver.performReceive(intent, resultCode, dataStr, extras, ordered, sticky); } - + public void scheduleLowMemory() { queueOrSendMessage(H.LOW_MEMORY, null); } @@ -1524,7 +1553,7 @@ public final class ActivityThread { } catch (RemoteException e) { } } - + public void profilerControl(boolean start, String path, ParcelFileDescriptor fd) { ProfilerControlData pcd = new ProfilerControlData(); pcd.path = path; @@ -1543,7 +1572,11 @@ public final class ActivityThread { Log.w(TAG, "Failed setting process group to " + group, e); } } - + + public void getMemoryInfo(Debug.MemoryInfo outInfo) { + Debug.getMemoryInfo(outInfo); + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { long nativeMax = Debug.getNativeHeapSize() / 1024; @@ -1579,7 +1612,7 @@ public final class ActivityThread { long sqliteAllocated = SQLiteDebug.getHeapAllocatedSize() / 1024; SQLiteDebug.PagerStats stats = new SQLiteDebug.PagerStats(); SQLiteDebug.getPagerStats(stats); - + // Check to see if we were called by checkin server. If so, print terse format. boolean doCheckinFormat = false; if (args != null) { @@ -1587,79 +1620,79 @@ public final class ActivityThread { if ("-c".equals(arg)) doCheckinFormat = true; } } - + // For checkin, we print one long comma-separated list of values if (doCheckinFormat) { // NOTE: if you change anything significant below, also consider changing // ACTIVITY_THREAD_CHECKIN_VERSION. - String processName = (mBoundApplication != null) + String processName = (mBoundApplication != null) ? mBoundApplication.processName : "unknown"; - + // Header pw.print(ACTIVITY_THREAD_CHECKIN_VERSION); pw.print(','); pw.print(Process.myPid()); pw.print(','); pw.print(processName); pw.print(','); - + // Heap info - max pw.print(nativeMax); pw.print(','); pw.print(dalvikMax); pw.print(','); pw.print("N/A,"); pw.print(nativeMax + dalvikMax); pw.print(','); - + // Heap info - allocated pw.print(nativeAllocated); pw.print(','); pw.print(dalvikAllocated); pw.print(','); pw.print("N/A,"); pw.print(nativeAllocated + dalvikAllocated); pw.print(','); - + // Heap info - free pw.print(nativeFree); pw.print(','); pw.print(dalvikFree); pw.print(','); pw.print("N/A,"); pw.print(nativeFree + dalvikFree); pw.print(','); - + // Heap info - proportional set size pw.print(memInfo.nativePss); pw.print(','); pw.print(memInfo.dalvikPss); pw.print(','); pw.print(memInfo.otherPss); pw.print(','); pw.print(memInfo.nativePss + memInfo.dalvikPss + memInfo.otherPss); pw.print(','); - + // Heap info - shared - pw.print(nativeShared); pw.print(','); - pw.print(dalvikShared); pw.print(','); - pw.print(otherShared); pw.print(','); + pw.print(nativeShared); pw.print(','); + pw.print(dalvikShared); pw.print(','); + pw.print(otherShared); pw.print(','); pw.print(nativeShared + dalvikShared + otherShared); pw.print(','); - + // Heap info - private - pw.print(nativePrivate); pw.print(','); + pw.print(nativePrivate); pw.print(','); pw.print(dalvikPrivate); pw.print(','); pw.print(otherPrivate); pw.print(','); pw.print(nativePrivate + dalvikPrivate + otherPrivate); pw.print(','); - + // Object counts pw.print(viewInstanceCount); pw.print(','); pw.print(viewRootInstanceCount); pw.print(','); pw.print(appContextInstanceCount); pw.print(','); pw.print(activityInstanceCount); pw.print(','); - + pw.print(globalAssetCount); pw.print(','); pw.print(globalAssetManagerCount); pw.print(','); pw.print(binderLocalObjectCount); pw.print(','); pw.print(binderProxyObjectCount); pw.print(','); - + pw.print(binderDeathObjectCount); pw.print(','); pw.print(openSslSocketCount); pw.print(','); - + // SQL pw.print(sqliteAllocated); pw.print(','); - pw.print(stats.databaseBytes / 1024); pw.print(','); + pw.print(stats.databaseBytes / 1024); pw.print(','); pw.print(stats.numPagers); pw.print(','); pw.print((stats.totalBytes - stats.referencedBytes) / 1024); pw.print(','); pw.print(stats.referencedBytes / 1024); pw.print('\n'); - + return; } - + // otherwise, show human-readable format printRow(pw, HEAP_COLUMN, "", "native", "dalvik", "other", "total"); printRow(pw, HEAP_COLUMN, "size:", nativeMax, dalvikMax, "N/A", nativeMax + dalvikMax); @@ -1692,7 +1725,7 @@ public final class ActivityThread { printRow(pw, ONE_COUNT_COLUMN, "Death Recipients:", binderDeathObjectCount); printRow(pw, ONE_COUNT_COLUMN, "OpenSSL Sockets:", openSslSocketCount); - + // SQLite mem info pw.println(" "); pw.println(" SQL"); @@ -1701,7 +1734,7 @@ public final class ActivityThread { printRow(pw, TWO_COUNT_COLUMNS, "numPagers:", stats.numPagers, "inactivePageKB:", (stats.totalBytes - stats.referencedBytes) / 1024); printRow(pw, ONE_COUNT_COLUMN, "activePageKB:", stats.referencedBytes / 1024); - + // Asset details. String assetAlloc = AssetManager.getAssetAllocations(); if (assetAlloc != null) { @@ -1717,6 +1750,10 @@ public final class ActivityThread { } private final class H extends Handler { + private H() { + SamplingProfiler.getInstance().setEventThread(mLooper.getThread()); + } + public static final int LAUNCH_ACTIVITY = 100; public static final int PAUSE_ACTIVITY = 101; public static final int PAUSE_ACTIVITY_FINISHING= 102; @@ -1746,7 +1783,9 @@ public final class ActivityThread { public static final int RELAUNCH_ACTIVITY = 126; public static final int PROFILER_CONTROL = 127; public static final int CREATE_BACKUP_AGENT = 128; - public static final int DESTROY_BACKUP_AGENT = 129; + public static final int DESTROY_BACKUP_AGENT = 129; + public static final int SUICIDE = 130; + public static final int REMOVE_PROVIDER = 131; String codeToString(int code) { if (localLOGV) { switch (code) { @@ -1780,6 +1819,8 @@ public final class ActivityThread { case PROFILER_CONTROL: return "PROFILER_CONTROL"; case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT"; case DESTROY_BACKUP_AGENT: return "DESTROY_BACKUP_AGENT"; + case SUICIDE: return "SUICIDE"; + case REMOVE_PROVIDER: return "REMOVE_PROVIDER"; } } return "(unknown)"; @@ -1799,6 +1840,7 @@ public final class ActivityThread { } break; case PAUSE_ACTIVITY: handlePauseActivity((IBinder)msg.obj, false, msg.arg1 != 0, msg.arg2); + maybeSnapshot(); break; case PAUSE_ACTIVITY_FINISHING: handlePauseActivity((IBinder)msg.obj, true, msg.arg1 != 0, msg.arg2); @@ -1841,6 +1883,7 @@ public final class ActivityThread { break; case RECEIVER: handleReceiver((ReceiverData)msg.obj); + maybeSnapshot(); break; case CREATE_SERVICE: handleCreateService((CreateServiceData)msg.obj); @@ -1856,6 +1899,7 @@ public final class ActivityThread { break; case STOP_SERVICE: handleStopService((IBinder)msg.obj); + maybeSnapshot(); break; case REQUEST_THUMBNAIL: handleRequestThumbnail((IBinder)msg.obj); @@ -1888,6 +1932,19 @@ public final class ActivityThread { case DESTROY_BACKUP_AGENT: handleDestroyBackupAgent((CreateBackupAgentData)msg.obj); break; + case SUICIDE: + Process.killProcess(Process.myPid()); + break; + case REMOVE_PROVIDER: + completeRemoveProvider((IContentProvider)msg.obj); + break; + } + } + + void maybeSnapshot() { + if (mBoundApplication != null) { + SamplingProfilerIntegration.writeSnapshot( + mBoundApplication.processName); } } } @@ -1906,7 +1963,8 @@ public final class ActivityThread { (a.activity != null ? a.activity.mFinished : false)); if (a.activity != null && !a.activity.mFinished) { try { - am.activityIdle(a.token); + am.activityIdle(a.token, a.createdConfig); + a.createdConfig = null; } catch (RemoteException ex) { } } @@ -1930,13 +1988,13 @@ public final class ActivityThread { final private String mResDir; final private float mScale; final private int mHash; - + ResourcesKey(String resDir, float scale) { mResDir = resDir; mScale = scale; mHash = mResDir.hashCode() << 2 + (int) (mScale * 2); } - + @Override public int hashCode() { return mHash; @@ -1987,7 +2045,7 @@ public final class ActivityThread { final ArrayList mRelaunchingActivities = new ArrayList(); Configuration mPendingConfiguration = null; - + // These can be accessed by multiple threads; mPackages is the lock. // XXX For now we keep around information about all packages we have // seen, not removing entries from this map. @@ -2122,7 +2180,7 @@ public final class ActivityThread { return false; } } - + ActivityThread() { } @@ -2155,17 +2213,17 @@ public final class ActivityThread { public Application getApplication() { return mInitialApplication; } - + public String getProcessName() { return mBoundApplication.processName; } - + public ApplicationContext getSystemContext() { synchronized (this) { if (mSystemContext == null) { ApplicationContext context = ApplicationContext.createSystemContext(this); - PackageInfo info = new PackageInfo(this, "android", context); + PackageInfo info = new PackageInfo(this, "android", context, null); context.init(info, null, this); context.getResources().updateConfiguration( getConfiguration(), getDisplayMetricsLocked(false)); @@ -2177,6 +2235,13 @@ public final class ActivityThread { return mSystemContext; } + public void installSystemApplicationInfo(ApplicationInfo info) { + synchronized (this) { + ApplicationContext context = getSystemContext(); + context.init(new PackageInfo(this, "android", context, info), null, this); + } + } + void scheduleGcIdler() { if (!mGcIdlerScheduled) { mGcIdlerScheduled = true; @@ -2214,7 +2279,7 @@ public final class ActivityThread { } return aInfo; } - + public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, Object lastNonConfigurationInstance) { @@ -2297,7 +2362,7 @@ public final class ActivityThread { r.packageInfo = getPackageInfo(aInfo.applicationInfo, Context.CONTEXT_INCLUDE_CODE); } - + ComponentName component = r.intent.getComponent(); if (component == null) { component = r.intent.resolveActivity( @@ -2328,8 +2393,8 @@ public final class ActivityThread { } try { - Application app = r.packageInfo.makeApplication(false); - + Application app = r.packageInfo.makeApplication(false, mInstrumentation); + if (localLOGV) Log.v(TAG, "Performing launch of " + r); if (localLOGV) Log.v( TAG, r + ": app=" + app @@ -2344,11 +2409,13 @@ public final class ActivityThread { appContext.setOuterContext(activity); CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); Configuration config = new Configuration(mConfiguration); + if (DEBUG_CONFIGURATION) Log.v(TAG, "Launching activity " + + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstance, r.lastNonConfigurationChildInstances, config); - + if (customIntent != null) { activity.mIntent = customIntent; } @@ -2417,6 +2484,7 @@ public final class ActivityThread { Activity a = performLaunchActivity(r, customIntent); if (a != null) { + r.createdConfig = new Configuration(a.getResources().getConfiguration()); handleResumeActivity(r.token, false, r.isForward); if (!r.activity.mFinished && r.startsNotResumed) { @@ -2486,7 +2554,7 @@ public final class ActivityThread { } } } - + private final void handleNewIntent(NewIntentData data) { performNewIntents(data.token, data.intents); } @@ -2523,8 +2591,8 @@ public final class ActivityThread { } try { - Application app = packageInfo.makeApplication(false); - + Application app = packageInfo.makeApplication(false, mInstrumentation); + if (localLOGV) Log.v( TAG, "Performing receive of " + data.intent + ": app=" + app @@ -2581,7 +2649,7 @@ public final class ActivityThread { + " already exists"); return; } - + BackupAgent agent = null; String classname = data.appInfo.backupAgentName; if (classname == null) { @@ -2635,7 +2703,7 @@ public final class ActivityThread { // Tear down a BackupAgent private final void handleDestroyBackupAgent(CreateBackupAgentData data) { if (DEBUG_BACKUP) Log.v(TAG, "handleDestroyBackupAgent: " + data); - + PackageInfo packageInfo = getPackageInfoNoCheck(data.appInfo); String packageName = packageInfo.mPackageName; BackupAgent agent = mBackupAgents.get(packageName); @@ -2677,14 +2745,15 @@ public final class ActivityThread { ApplicationContext context = new ApplicationContext(); context.init(packageInfo, null, this); - Application app = packageInfo.makeApplication(false); + Application app = packageInfo.makeApplication(false, mInstrumentation); context.setOuterContext(service); service.attach(context, this, data.info.name, data.token, app, ActivityManagerNative.getDefault()); service.onCreate(); mServices.put(data.token, service); try { - ActivityManagerNative.getDefault().serviceDoneExecuting(data.token); + ActivityManagerNative.getDefault().serviceDoneExecuting( + data.token, 0, 0, 0); } catch (RemoteException e) { // nothing to do. } @@ -2710,7 +2779,7 @@ public final class ActivityThread { } else { s.onRebind(data.intent); ActivityManagerNative.getDefault().serviceDoneExecuting( - data.token); + data.token, 0, 0, 0); } } catch (RemoteException ex) { } @@ -2736,7 +2805,7 @@ public final class ActivityThread { data.token, data.intent, doRebind); } else { ActivityManagerNative.getDefault().serviceDoneExecuting( - data.token); + data.token, 0, 0, 0); } } catch (RemoteException ex) { } @@ -2773,9 +2842,10 @@ public final class ActivityThread { if (data.args != null) { data.args.setExtrasClassLoader(s.getClassLoader()); } - s.onStart(data.args, data.startId); + int res = s.onStartCommand(data.args, data.flags, data.startId); try { - ActivityManagerNative.getDefault().serviceDoneExecuting(data.token); + ActivityManagerNative.getDefault().serviceDoneExecuting( + data.token, 1, data.startId, res); } catch (RemoteException e) { // nothing to do. } @@ -2801,7 +2871,8 @@ public final class ActivityThread { ((ApplicationContext) context).scheduleFinalCleanup(who, "Service"); } try { - ActivityManagerNative.getDefault().serviceDoneExecuting(token); + ActivityManagerNative.getDefault().serviceDoneExecuting( + token, 0, 0, 0); } catch (RemoteException e) { // nothing to do. } @@ -2837,9 +2908,9 @@ public final class ActivityThread { } r.activity.performResume(); - EventLog.writeEvent(LOG_ON_RESUME_CALLED, + EventLog.writeEvent(LOG_ON_RESUME_CALLED, r.activity.getComponentName().getClassName()); - + r.paused = false; r.stopped = false; if (r.activity.mStartedActivity) { @@ -2875,7 +2946,7 @@ public final class ActivityThread { final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; - + // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. @@ -2904,9 +2975,11 @@ public final class ActivityThread { // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. - if (!r.activity.mFinished && r.activity.mDecor != null - && !r.hideForNow) { + if (!r.activity.mFinished && !a.mStartedActivity + && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { + if (DEBUG_CONFIGURATION) Log.v(TAG, "Resuming activity " + + r.activityInfo.name + " with newConfig " + r.newConfig); performConfigurationChanged(r.activity, r.newConfig); r.newConfig = null; } @@ -2919,9 +2992,11 @@ public final class ActivityThread { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; - ViewManager wm = a.getWindowManager(); - View decor = r.window.getDecorView(); - wm.updateViewLayout(decor, l); + if (r.activity.mVisibleFromClient) { + ViewManager wm = a.getWindowManager(); + View decor = r.window.getDecorView(); + wm.updateViewLayout(decor, l); + } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; @@ -2994,7 +3069,7 @@ public final class ActivityThread { if (userLeaving) { performUserLeavingActivity(r); } - + r.activity.mConfigChangeFlags |= configChanges; Bundle state = performPauseActivity(token, finished, true); @@ -3146,6 +3221,8 @@ public final class ActivityThread { } } if (r.newConfig != null) { + if (DEBUG_CONFIGURATION) Log.v(TAG, "Updating activity vis " + + r.activityInfo.name + " with new config " + r.newConfig); performConfigurationChanged(r.activity, r.newConfig); r.newConfig = null; } @@ -3171,7 +3248,7 @@ public final class ActivityThread { + " win=" + r.window); updateVisibility(r, show); - + // Tell activity manager we have been stopped. try { ActivityManagerNative.getDefault().activityStopped( @@ -3287,7 +3364,7 @@ public final class ActivityThread { try { r.activity.mCalled = false; mInstrumentation.callActivityOnPause(r.activity); - EventLog.writeEvent(LOG_ON_PAUSE_CALLED, + EventLog.writeEvent(LOG_ON_PAUSE_CALLED, r.activity.getComponentName().getClassName()); if (!r.activity.mCalled) { throw new SuperNotCalledException( @@ -3344,7 +3421,7 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - + } try { r.activity.mCalled = false; @@ -3426,6 +3503,10 @@ public final class ActivityThread { unscheduleGcIdler(); Configuration changedConfig = null; + + if (DEBUG_CONFIGURATION) Log.v(TAG, "Relaunching activity " + + tmp.token + " with configChanges=0x" + + Integer.toHexString(configChanges)); // First: make sure we have the most recent configuration and most // recent version of the activity, or skip it if some previous call @@ -3443,38 +3524,42 @@ public final class ActivityThread { N--; } } - + if (tmp == null) { + if (DEBUG_CONFIGURATION) Log.v(TAG, "Abort, activity not relaunching!"); return; } - + if (mPendingConfiguration != null) { changedConfig = mPendingConfiguration; mPendingConfiguration = null; } } + + if (DEBUG_CONFIGURATION) Log.v(TAG, "Relaunching activity " + + tmp.token + ": changedConfig=" + changedConfig); // If there was a pending configuration change, execute it first. if (changedConfig != null) { handleConfigurationChanged(changedConfig); } - + ActivityRecord r = mActivities.get(tmp.token); - if (localLOGV) Log.v(TAG, "Handling relaunch of " + r); + if (DEBUG_CONFIGURATION) Log.v(TAG, "Handling relaunch of " + r); if (r == null) { return; } - + r.activity.mConfigChangeFlags |= configChanges; Intent currentIntent = r.activity.mIntent; - + Bundle savedState = null; if (!r.paused) { savedState = performPauseActivity(r.token, false, true); } - + handleDestroyActivity(r.token, false, configChanges, true); - + r.activity = null; r.window = null; r.hideForNow = false; @@ -3498,7 +3583,7 @@ public final class ActivityThread { if (savedState != null) { r.state = savedState; } - + handleLaunchActivity(r, currentIntent); } @@ -3528,7 +3613,7 @@ public final class ActivityThread { boolean allActivities, Configuration newConfig) { ArrayList callbacks = new ArrayList(); - + if (mActivities.size() > 0) { Iterator it = mActivities.values().iterator(); while (it.hasNext()) { @@ -3546,6 +3631,8 @@ public final class ActivityThread { // the activity manager may, before then, decide the // activity needs to be destroyed to handle its new // configuration. + if (DEBUG_CONFIGURATION) Log.v(TAG, "Setting activity " + + ar.activityInfo.name + " newConfig=" + newConfig); ar.newConfig = newConfig; } } @@ -3569,10 +3656,10 @@ public final class ActivityThread { for (int i=0; i callbacks = new ArrayList(); @@ -3710,7 +3805,7 @@ public final class ActivityThread { synchronized(mPackages) { callbacks = collectComponentCallbacksLocked(true, null); } - + final int N = callbacks.size(); for (int i=0; i providers = data.providers; @@ -3992,22 +4088,51 @@ public final class ActivityThread { } else { prc.count--; if(prc.count == 0) { - mProviderRefCountMap.remove(jBinder); - //invoke removeProvider to dereference provider - removeProviderLocked(provider); + // Schedule the actual remove asynchronously, since we + // don't know the context this will be called in. + // TODO: it would be nice to post a delayed message, so + // if we come back and need the same provider quickly + // we will still have it available. + Message msg = mH.obtainMessage(H.REMOVE_PROVIDER, provider); + mH.sendMessage(msg); } //end if } //end else } //end synchronized return true; } - public final void removeProviderLocked(IContentProvider provider) { + final void completeRemoveProvider(IContentProvider provider) { + IBinder jBinder = provider.asBinder(); + String name = null; + synchronized(mProviderMap) { + ProviderRefCount prc = mProviderRefCountMap.get(jBinder); + if(prc != null && prc.count == 0) { + mProviderRefCountMap.remove(jBinder); + //invoke removeProvider to dereference provider + name = removeProviderLocked(provider); + } + } + + if (name != null) { + try { + if(localLOGV) Log.v(TAG, "removeProvider::Invoking " + + "ActivityManagerNative.removeContentProvider(" + name); + ActivityManagerNative.getDefault().removeContentProvider( + getApplicationThread(), name); + } catch (RemoteException e) { + //do nothing content provider object is dead any way + } //end catch + } + } + + public final String removeProviderLocked(IContentProvider provider) { if (provider == null) { - return; + return null; } IBinder providerBinder = provider.asBinder(); - boolean amRemoveFlag = false; + String name = null; + // remove the provider from mProviderMap Iterator iter = mProviderMap.values().iterator(); while (iter.hasNext()) { @@ -4017,7 +4142,7 @@ public final class ActivityThread { //find if its published by this process itself if(pr.mLocalProvider != null) { if(localLOGV) Log.i(TAG, "removeProvider::found local provider returning"); - return; + return name; } if(localLOGV) Log.v(TAG, "removeProvider::Not local provider Unlinking " + "death recipient"); @@ -4025,18 +4150,13 @@ public final class ActivityThread { myBinder.unlinkToDeath(pr, 0); iter.remove(); //invoke remove only once for the very first name seen - if(!amRemoveFlag) { - try { - if(localLOGV) Log.v(TAG, "removeProvider::Invoking " + - "ActivityManagerNative.removeContentProvider("+pr.mName); - ActivityManagerNative.getDefault().removeContentProvider(getApplicationThread(), pr.mName); - amRemoveFlag = true; - } catch (RemoteException e) { - //do nothing content provider object is dead any way - } //end catch + if(name == null) { + name = pr.mName; } } //end if myBinder } //end while iter + + return name; } final void removeDeadProvider(String name, IContentProvider provider) { @@ -4193,6 +4313,8 @@ public final class ActivityThread { } public static final void main(String[] args) { + SamplingProfilerIntegration.start(); + Process.setArgV0(""); Looper.prepareMainLooper(); @@ -4207,9 +4329,9 @@ public final class ActivityThread { } thread.detach(); - String name; - if (thread.mInitialApplication != null) name = thread.mInitialApplication.getPackageName(); - else name = ""; + String name = (thread.mInitialApplication != null) + ? thread.mInitialApplication.getPackageName() + : ""; Log.i(TAG, "Main thread of " + name + " is now exiting"); } } diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index 92929ea56748eb4c8ee5ba7291a75ffadc5047ae..f48f15092f8d3bf9b8c0e560f5aaf3edda6a66b4 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -22,8 +22,6 @@ import com.google.android.collect.Maps; import org.xmlpull.v1.XmlPullParserException; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.IBluetoothDevice; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -40,6 +38,7 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; +import android.content.pm.FeatureInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageInstallObserver; @@ -59,8 +58,6 @@ import android.content.res.XmlResourceParser; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.hardware.SensorManager; import android.location.ILocationManager; @@ -78,7 +75,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.IPowerManager; import android.os.Looper; -import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; @@ -88,14 +84,14 @@ import android.os.FileUtils.FileStatus; import android.telephony.TelephonyManager; import android.text.ClipboardManager; import android.util.AndroidRuntimeException; -import android.util.DisplayMetrics; import android.util.Log; import android.view.ContextThemeWrapper; -import android.view.Display; import android.view.LayoutInflater; import android.view.WindowManagerImpl; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; +import android.accounts.AccountManager; +import android.accounts.IAccountManager; import java.io.File; import java.io.FileInputStream; @@ -160,9 +156,6 @@ class ApplicationContext extends Context { private static ConnectivityManager sConnectivityManager; private static WifiManager sWifiManager; private static LocationManager sLocationManager; - private static boolean sIsBluetoothDeviceCached = false; - private static BluetoothDevice sBluetoothDevice; - private static IWallpaperService sWallpaperService; private static final HashMap sSharedPrefs = new HashMap(); @@ -177,8 +170,8 @@ class ApplicationContext extends Context { private Resources.Theme mTheme = null; private PackageManager mPackageManager; private NotificationManager mNotificationManager = null; - private AccessibilityManager mAccessibilityManager = null; private ActivityManager mActivityManager = null; + private WallpaperManager mWallpaperManager = null; private Context mReceiverRestrictedContext = null; private SearchManager mSearchManager = null; private SensorManager mSensorManager = null; @@ -188,6 +181,7 @@ class ApplicationContext extends Context { private TelephonyManager mTelephonyManager = null; private ClipboardManager mClipboardManager = null; private boolean mRestricted; + private AccountManager mAccountManager; // protected by mSync private final Object mSync = new Object(); @@ -198,9 +192,6 @@ class ApplicationContext extends Context { private File mCacheDir; - private Drawable mWallpaper; - private IWallpaperServiceCallback mWallpaperCallback = null; - private static long sInstanceCount = 0; private static final String[] EMPTY_FILE_LIST = {}; @@ -520,127 +511,37 @@ class ApplicationContext extends Context { @Override public Drawable getWallpaper() { - Drawable dr = peekWallpaper(); - return dr != null ? dr : getResources().getDrawable( - com.android.internal.R.drawable.default_wallpaper); + return getWallpaperManager().getDrawable(); } @Override - public synchronized Drawable peekWallpaper() { - if (mWallpaper != null) { - return mWallpaper; - } - mWallpaperCallback = new WallpaperCallback(this); - mWallpaper = getCurrentWallpaperLocked(); - return mWallpaper; - } - - private Drawable getCurrentWallpaperLocked() { - try { - ParcelFileDescriptor fd = getWallpaperService().getWallpaper(mWallpaperCallback); - if (fd != null) { - Bitmap bm = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor()); - if (bm != null) { - // For now clear the density until we figure out how - // to deal with it for wallpapers. - bm.setDensity(0); - return new BitmapDrawable(getResources(), bm); - } - } - } catch (RemoteException e) { - } - return null; + public Drawable peekWallpaper() { + return getWallpaperManager().peekDrawable(); } @Override public int getWallpaperDesiredMinimumWidth() { - try { - return getWallpaperService().getWidthHint(); - } catch (RemoteException e) { - // Shouldn't happen! - return 0; - } + return getWallpaperManager().getDesiredMinimumWidth(); } @Override public int getWallpaperDesiredMinimumHeight() { - try { - return getWallpaperService().getHeightHint(); - } catch (RemoteException e) { - // Shouldn't happen! - return 0; - } + return getWallpaperManager().getDesiredMinimumHeight(); } @Override public void setWallpaper(Bitmap bitmap) throws IOException { - try { - ParcelFileDescriptor fd = getWallpaperService().setWallpaper(); - if (fd == null) { - return; - } - FileOutputStream fos = null; - try { - fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); - bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); - } finally { - if (fos != null) { - fos.close(); - } - } - } catch (RemoteException e) { - } + getWallpaperManager().setBitmap(bitmap); } @Override public void setWallpaper(InputStream data) throws IOException { - try { - ParcelFileDescriptor fd = getWallpaperService().setWallpaper(); - if (fd == null) { - return; - } - FileOutputStream fos = null; - try { - fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); - setWallpaper(data, fos); - } finally { - if (fos != null) { - fos.close(); - } - } - } catch (RemoteException e) { - } - } - - private void setWallpaper(InputStream data, FileOutputStream fos) - throws IOException { - byte[] buffer = new byte[32768]; - int amt; - while ((amt=data.read(buffer)) > 0) { - fos.write(buffer, 0, amt); - } + getWallpaperManager().setStream(data); } @Override public void clearWallpaper() throws IOException { - try { - /* Set the wallpaper to the default values */ - ParcelFileDescriptor fd = getWallpaperService().setWallpaper(); - if (fd != null) { - FileOutputStream fos = null; - try { - fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); - setWallpaper(getResources().openRawResource( - com.android.internal.R.drawable.default_wallpaper), - fos); - } finally { - if (fos != null) { - fos.close(); - } - } - } - } catch (RemoteException e) { - } + getWallpaperManager().clear(); } @Override @@ -655,6 +556,27 @@ class ApplicationContext extends Context { getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1); } + @Override + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + try { + String resolvedType = null; + if (fillInIntent != null) { + resolvedType = fillInIntent.resolveTypeIfNeeded(getContentResolver()); + } + int result = ActivityManagerNative.getDefault() + .startActivityIntentSender(mMainThread.getApplicationThread(), intent, + fillInIntent, resolvedType, null, null, + 0, flagsMask, flagsValues); + if (result == IActivityManager.START_CANCELED) { + throw new IntentSender.SendIntentException(); + } + Instrumentation.checkStartActivityResult(result, null); + } catch (RemoteException e) { + } + } + @Override public void sendBroadcast(Intent intent) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); @@ -732,6 +654,38 @@ class ApplicationContext extends Context { } } + @Override + public void sendStickyOrderedBroadcast(Intent intent, + BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + IIntentReceiver rd = null; + if (resultReceiver != null) { + if (mPackageInfo != null) { + if (scheduler == null) { + scheduler = mMainThread.getHandler(); + } + rd = mPackageInfo.getReceiverDispatcher( + resultReceiver, getOuterContext(), scheduler, + mMainThread.getInstrumentation(), false); + } else { + if (scheduler == null) { + scheduler = mMainThread.getHandler(); + } + rd = new ActivityThread.PackageInfo.ReceiverDispatcher( + resultReceiver, getOuterContext(), scheduler, null, false).getIIntentReceiver(); + } + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); + try { + ActivityManagerNative.getDefault().broadcastIntent( + mMainThread.getApplicationThread(), intent, resolvedType, rd, + initialCode, initialData, initialExtras, null, + true, true); + } catch (RemoteException e) { + } + } + @Override public void removeStickyBroadcast(Intent intent) { String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); @@ -901,8 +855,12 @@ class ApplicationContext extends Context { } } else if (ACTIVITY_SERVICE.equals(name)) { return getActivityManager(); + } else if (INPUT_METHOD_SERVICE.equals(name)) { + return InputMethodManager.getInstance(this); } else if (ALARM_SERVICE.equals(name)) { return getAlarmManager(); + } else if (ACCOUNT_SERVICE.equals(name)) { + return getAccountManager(); } else if (POWER_SERVICE.equals(name)) { return getPowerManager(); } else if (CONNECTIVITY_SERVICE.equals(name)) { @@ -919,10 +877,8 @@ class ApplicationContext extends Context { return getLocationManager(); } else if (SEARCH_SERVICE.equals(name)) { return getSearchManager(); - } else if ( SENSOR_SERVICE.equals(name)) { + } else if (SENSOR_SERVICE.equals(name)) { return getSensorManager(); - } else if (BLUETOOTH_SERVICE.equals(name)) { - return getBluetoothDevice(); } else if (VIBRATOR_SERVICE.equals(name)) { return getVibrator(); } else if (STATUS_BAR_SERVICE.equals(name)) { @@ -938,13 +894,24 @@ class ApplicationContext extends Context { return getTelephonyManager(); } else if (CLIPBOARD_SERVICE.equals(name)) { return getClipboardManager(); - } else if (INPUT_METHOD_SERVICE.equals(name)) { - return InputMethodManager.getInstance(this); + } else if (WALLPAPER_SERVICE.equals(name)) { + return getWallpaperManager(); } return null; } + private AccountManager getAccountManager() { + synchronized (mSync) { + if (mAccountManager == null) { + IBinder b = ServiceManager.getService(ACCOUNT_SERVICE); + IAccountManager service = IAccountManager.Stub.asInterface(b); + mAccountManager = new AccountManager(this, service); + } + return mAccountManager; + } + } + private ActivityManager getActivityManager() { synchronized (mSync) { if (mActivityManager == null) { @@ -1001,8 +968,7 @@ class ApplicationContext extends Context { return sWifiManager; } - private NotificationManager getNotificationManager() - { + private NotificationManager getNotificationManager() { synchronized (mSync) { if (mNotificationManager == null) { mNotificationManager = new NotificationManager( @@ -1013,6 +979,16 @@ class ApplicationContext extends Context { return mNotificationManager; } + private WallpaperManager getWallpaperManager() { + synchronized (mSync) { + if (mWallpaperManager == null) { + mWallpaperManager = new WallpaperManager(getOuterContext(), + mMainThread.getHandler()); + } + } + return mWallpaperManager; + } + private TelephonyManager getTelephonyManager() { synchronized (mSync) { if (mTelephonyManager == null) { @@ -1052,23 +1028,6 @@ class ApplicationContext extends Context { return mSearchManager; } - private BluetoothDevice getBluetoothDevice() { - if (sIsBluetoothDeviceCached) { - return sBluetoothDevice; - } - synchronized (sSync) { - IBinder b = ServiceManager.getService(BLUETOOTH_SERVICE); - if (b == null) { - sBluetoothDevice = null; - } else { - IBluetoothDevice service = IBluetoothDevice.Stub.asInterface(b); - sBluetoothDevice = new BluetoothDevice(service); - } - sIsBluetoothDeviceCached = true; - } - return sBluetoothDevice; - } - private SensorManager getSensorManager() { synchronized (mSync) { if (mSensorManager == null) { @@ -1087,16 +1046,6 @@ class ApplicationContext extends Context { return mVibrator; } - private IWallpaperService getWallpaperService() { - synchronized (sSync) { - if (sWallpaperService == null) { - IBinder b = ServiceManager.getService(WALLPAPER_SERVICE); - sWallpaperService = IWallpaperService.Stub.asInterface(b); - } - } - return sWallpaperService; - } - private AudioManager getAudioManager() { if (mAudioManager == null) { @@ -1709,6 +1658,24 @@ class ApplicationContext extends Context { } } + @Override + public FeatureInfo[] getSystemAvailableFeatures() { + try { + return mPM.getSystemAvailableFeatures(); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } + + @Override + public boolean hasSystemFeature(String name) { + try { + return mPM.hasSystemFeature(name); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } + @Override public int checkPermission(String permName, String pkgName) { try { @@ -1745,6 +1712,15 @@ class ApplicationContext extends Context { } } + @Override + public int checkSignatures(int uid1, int uid2) { + try { + return mPM.checkUidSignatures(uid1, uid2); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } + @Override public String[] getPackagesForUid(int uid) { try { @@ -2766,6 +2742,7 @@ class ApplicationContext extends Context { if (mFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); + return false; } } @@ -2801,25 +2778,4 @@ class ApplicationContext extends Context { return false; } } - - private static class WallpaperCallback extends IWallpaperServiceCallback.Stub { - private WeakReference mContext; - - public WallpaperCallback(ApplicationContext context) { - mContext = new WeakReference(context); - } - - public synchronized void onWallpaperChanged() { - - /* The wallpaper has changed but we shouldn't eagerly load the - * wallpaper as that would be inefficient. Reset the cached wallpaper - * to null so if the user requests the wallpaper again then we'll - * fetch it. - */ - final ApplicationContext applicationContext = mContext.get(); - if (applicationContext != null) { - applicationContext.mWallpaper = null; - } - } - } } diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index 6b172363296e7a6a6516f72bdcda2d255ccddfe8..aeae5f913045f7a029136dfd4b9238dec4b11532 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -170,6 +170,11 @@ public class ApplicationErrorReport implements Parcelable { */ public String throwMethodName; + /** + * Line number the exception was thrown from. + */ + public int throwLineNumber; + /** * Stack trace. */ @@ -190,6 +195,7 @@ public class ApplicationErrorReport implements Parcelable { throwFileName = in.readString(); throwClassName = in.readString(); throwMethodName = in.readString(); + throwLineNumber = in.readInt(); stackTrace = in.readString(); } @@ -202,6 +208,7 @@ public class ApplicationErrorReport implements Parcelable { dest.writeString(throwFileName); dest.writeString(throwClassName); dest.writeString(throwMethodName); + dest.writeInt(throwLineNumber); dest.writeString(stackTrace); } @@ -214,6 +221,7 @@ public class ApplicationErrorReport implements Parcelable { pw.println(prefix + "throwFileName: " + throwFileName); pw.println(prefix + "throwClassName: " + throwClassName); pw.println(prefix + "throwMethodName: " + throwMethodName); + pw.println(prefix + "throwLineNumber: " + throwLineNumber); pw.println(prefix + "stackTrace: " + stackTrace); } } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index a3c63253d73d82f4d41756557de75063f98add23..a772a8f78ddf7d65db7f743570b8dc43e2f53ee7 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.os.Binder; import android.os.Bundle; +import android.os.Debug; import android.os.Parcelable; import android.os.RemoteException; import android.os.IBinder; @@ -206,8 +207,14 @@ public abstract class ApplicationThreadNative extends Binder data.enforceInterface(IApplicationThread.descriptor); IBinder token = data.readStrongBinder(); int startId = data.readInt(); - Intent args = Intent.CREATOR.createFromParcel(data); - scheduleServiceArgs(token, startId, args); + int fl = data.readInt(); + Intent args; + if (data.readInt() != 0) { + args = Intent.CREATOR.createFromParcel(data); + } else { + args = null; + } + scheduleServiceArgs(token, startId, fl, args); return true; } @@ -251,6 +258,13 @@ public abstract class ApplicationThreadNative extends Binder return true; } + case SCHEDULE_SUICIDE_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + scheduleSuicide(); + return true; + } + case REQUEST_THUMBNAIL_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); @@ -303,8 +317,9 @@ public abstract class ApplicationThreadNative extends Binder String dataStr = data.readString(); Bundle extras = data.readBundle(); boolean ordered = data.readInt() != 0; + boolean sticky = data.readInt() != 0; scheduleRegisteredReceiver(receiver, intent, - resultCode, dataStr, extras, ordered); + resultCode, dataStr, extras, ordered, sticky); return true; } @@ -364,6 +379,16 @@ public abstract class ApplicationThreadNative extends Binder scheduleDestroyBackupAgent(appInfo); return true; } + + case GET_MEMORY_INFO_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + Debug.MemoryInfo mi = new Debug.MemoryInfo(); + getMemoryInfo(mi); + reply.writeNoException(); + mi.writeToParcel(reply, 0); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -524,7 +549,8 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInterfaceToken(IApplicationThread.descriptor); app.writeToParcel(data, 0); data.writeInt(backupMode); - mRemote.transact(SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION, data, null, 0); + mRemote.transact(SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); data.recycle(); } @@ -532,7 +558,8 @@ class ApplicationThreadProxy implements IApplicationThread { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); app.writeToParcel(data, 0); - mRemote.transact(SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION, data, null, 0); + mRemote.transact(SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); data.recycle(); } @@ -571,12 +598,18 @@ class ApplicationThreadProxy implements IApplicationThread { } public final void scheduleServiceArgs(IBinder token, int startId, - Intent args) throws RemoteException { + int flags, Intent args) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); data.writeInt(startId); - args.writeToParcel(data, 0); + data.writeInt(flags); + if (args != null) { + data.writeInt(1); + args.writeToParcel(data, 0); + } else { + data.writeInt(0); + } mRemote.transact(SCHEDULE_SERVICE_ARGS_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); @@ -627,7 +660,15 @@ class ApplicationThreadProxy implements IApplicationThread { IBinder.FLAG_ONEWAY); data.recycle(); } - + + public final void scheduleSuicide() throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + mRemote.transact(SCHEDULE_SUICIDE_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + data.recycle(); + } + public final void requestThumbnail(IBinder token) throws RemoteException { Parcel data = Parcel.obtain(); @@ -676,7 +717,7 @@ class ApplicationThreadProxy implements IApplicationThread { } public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, - int resultCode, String dataStr, Bundle extras, boolean ordered) + int resultCode, String dataStr, Bundle extras, boolean ordered, boolean sticky) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); @@ -686,6 +727,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeString(dataStr); data.writeBundle(extras); data.writeInt(ordered ? 1 : 0); + data.writeInt(sticky ? 1 : 0); mRemote.transact(SCHEDULE_REGISTERED_RECEIVER_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); @@ -742,5 +784,16 @@ class ApplicationThreadProxy implements IApplicationThread { IBinder.FLAG_ONEWAY); data.recycle(); } + + public void getMemoryInfo(Debug.MemoryInfo outInfo) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + mRemote.transact(GET_MEMORY_INFO_TRANSACTION, data, reply, 0); + reply.readException(); + outInfo.readFromParcel(reply); + data.recycle(); + reply.recycle(); + } } diff --git a/core/java/android/app/BackupAgent.java b/core/java/android/app/BackupAgent.java index 0ac8a1e4cfdc390ab45a90917bdb66bdbf108fbf..b207998409465dd5fdc87586a36575ef0ee71163 100644 --- a/core/java/android/app/BackupAgent.java +++ b/core/java/android/app/BackupAgent.java @@ -36,6 +36,7 @@ import java.io.IOException; */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; + private static final boolean DEBUG = false; public BackupAgent() { super(null); @@ -116,7 +117,7 @@ public abstract class BackupAgent extends ContextWrapper { ParcelFileDescriptor data, ParcelFileDescriptor newState) throws RemoteException { // !!! TODO - real implementation; for now just invoke the callbacks directly - Log.v(TAG, "doBackup() invoked"); + if (DEBUG) Log.v(TAG, "doBackup() invoked"); BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); try { BackupAgent.this.onBackup(oldState, output, newState); @@ -132,7 +133,7 @@ public abstract class BackupAgent extends ContextWrapper { public void doRestore(ParcelFileDescriptor data, int appVersionCode, ParcelFileDescriptor newState) throws RemoteException { // !!! TODO - real implementation; for now just invoke the callbacks directly - Log.v(TAG, "doRestore() invoked"); + if (DEBUG) Log.v(TAG, "doRestore() invoked"); BackupDataInput input = new BackupDataInput(data.getFileDescriptor()); try { BackupAgent.this.onRestore(input, appVersionCode, newState); diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 943275514caff9b377d6d27975bf93a9564fedcb..58e8b32125475e4c578780bf9942f37e803c498d 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -21,6 +21,7 @@ import com.android.internal.policy.PolicyManager; import android.content.Context; import android.content.DialogInterface; import android.content.ComponentName; +import android.content.ContextWrapper; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -480,30 +481,45 @@ public class Dialog implements DialogInterface, Window.Callback, * *

If the focused view didn't want this event, this method is called. * - *

The default implementation handles KEYCODE_BACK to close the - * dialog. + *

The default implementation consumed the KEYCODE_BACK to later + * handle it in {@link #onKeyUp}. * * @see #onKeyUp * @see android.view.KeyEvent */ public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mCancelable) { - cancel(); - } + event.startTracking(); return true; } return false; } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * A key was released. * + *

The default implementation handles KEYCODE_BACK to close the + * dialog. + * * @see #onKeyDown * @see KeyEvent */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + onBackPressed(); + return true; + } return false; } @@ -516,6 +532,17 @@ public class Dialog implements DialogInterface, Window.Callback, return false; } + /** + * Called when the dialog has detected the user's press of the back + * key. The default implementation simply cancels the dialog (only if + * it is cancelable), but you can override this to do whatever you want. + */ + public void onBackPressed() { + if (mCancelable) { + cancel(); + } + } + /** * Called when a touch screen event was not handled by any of the views * under it. This is most useful to process touch events that happen outside @@ -576,6 +603,12 @@ public class Dialog implements DialogInterface, Window.Callback, public void onWindowFocusChanged(boolean hasFocus) { } + public void onAttachedToWindow() { + } + + public void onDetachedFromWindow() { + } + /** * Called to process key events. You can override this to intercept all * key events before they are dispatched to the window. Be sure to call @@ -592,7 +625,8 @@ public class Dialog implements DialogInterface, Window.Callback, if (mWindow.superDispatchKeyEvent(event)) { return true; } - return event.dispatch(this); + return event.dispatch(this, mDecor != null + ? mDecor.getKeyDispatcherState() : null, this); } /** @@ -795,14 +829,31 @@ public class Dialog implements DialogInterface, Window.Callback, // associate search with owner activity if possible (otherwise it will default to // global search). - final ComponentName appName = mOwnerActivity == null ? null - : mOwnerActivity.getComponentName(); + final ComponentName appName = getAssociatedActivity(); final boolean globalSearch = (appName == null); searchManager.startSearch(null, false, appName, null, globalSearch); dismiss(); return true; } + /** + * @return The activity associated with this dialog, or null if there is no assocaited activity. + */ + private ComponentName getAssociatedActivity() { + Activity activity = mOwnerActivity; + Context context = getContext(); + while (activity == null && context != null) { + if (context instanceof Activity) { + activity = (Activity) context; // found it! + } else { + context = (context instanceof ContextWrapper) ? + ((ContextWrapper) context).getBaseContext() : // unwrap one level + null; // done + } + } + return activity == null ? null : activity.getComponentName(); + } + /** * Request that key events come to this dialog. Use this if your diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index f6ef549d842779c1d21f30920cfadcb81594b2c6..9f505acc854a623502a3c5e874f5dd2f2c744c69 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IIntentSender; import android.content.IIntentReceiver; +import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; @@ -30,6 +31,7 @@ import android.content.pm.ProviderInfo; import android.content.res.Configuration; import android.graphics.Bitmap; import android.net.Uri; +import android.os.Debug; import android.os.RemoteException; import android.os.IBinder; import android.os.IInterface; @@ -76,10 +78,16 @@ public interface IActivityManager extends IInterface { public static final int START_CLASS_NOT_FOUND = -2; public static final int START_FORWARD_AND_REQUEST_CONFLICT = -3; public static final int START_PERMISSION_DENIED = -4; + public static final int START_NOT_ACTIVITY = -5; + public static final int START_CANCELED = -6; public int startActivity(IApplicationThread caller, Intent intent, String resolvedType, Uri[] grantedUriPermissions, int grantedMode, IBinder resultTo, String resultWho, int requestCode, boolean onlyIfNeeded, boolean debug) throws RemoteException; + public int startActivityIntentSender(IApplicationThread caller, + IntentSender intent, Intent fillInIntent, String resolvedType, + IBinder resultTo, String resultWho, int requestCode, + int flagsMask, int flagsValues) throws RemoteException; public boolean startNextMatchingActivity(IBinder callingActivity, Intent intent) throws RemoteException; public boolean finishActivity(IBinder token, int code, Intent data) @@ -101,7 +109,7 @@ public interface IActivityManager extends IInterface { public void setPersistent(IBinder token, boolean isPersistent) throws RemoteException; public void attachApplication(IApplicationThread app) throws RemoteException; /* oneway */ - public void activityIdle(IBinder token) throws RemoteException; + public void activityIdle(IBinder token, Configuration config) throws RemoteException; public void activityPaused(IBinder token, Bundle state) throws RemoteException; /* oneway */ public void activityStopped(IBinder token, @@ -125,13 +133,15 @@ public interface IActivityManager extends IInterface { public void finishOtherInstances(IBinder token, ComponentName className) throws RemoteException; /* oneway */ public void reportThumbnail(IBinder token, - Bitmap thumbnail, CharSequence description) throws RemoteException; + Bitmap thumbnail, CharSequence description) throws RemoteException; public ContentProviderHolder getContentProvider(IApplicationThread caller, - String name) throws RemoteException; + String name) throws RemoteException; public void removeContentProvider(IApplicationThread caller, - String name) throws RemoteException; + String name) throws RemoteException; public void publishContentProviders(IApplicationThread caller, - List providers) throws RemoteException; + List providers) throws RemoteException; + public PendingIntent getRunningServiceControlPanel(ComponentName service) + throws RemoteException; public ComponentName startService(IApplicationThread caller, Intent service, String resolvedType) throws RemoteException; public int stopService(IApplicationThread caller, Intent service, @@ -139,7 +149,7 @@ public interface IActivityManager extends IInterface { public boolean stopServiceToken(ComponentName className, IBinder token, int startId) throws RemoteException; public void setServiceForeground(ComponentName className, IBinder token, - boolean isForeground) throws RemoteException; + int id, Notification notification, boolean keepNotification) throws RemoteException; public int bindService(IApplicationThread caller, IBinder token, Intent service, String resolvedType, IServiceConnection connection, int flags) throws RemoteException; @@ -149,13 +159,15 @@ public interface IActivityManager extends IInterface { public void unbindFinished(IBinder token, Intent service, boolean doRebind) throws RemoteException; /* oneway */ - public void serviceDoneExecuting(IBinder token) throws RemoteException; + public void serviceDoneExecuting(IBinder token, int type, int startId, + int res) throws RemoteException; public IBinder peekService(Intent service, String resolvedType) throws RemoteException; public boolean bindBackupAgent(ApplicationInfo appInfo, int backupRestoreMode) throws RemoteException; public void backupAgentCreated(String packageName, IBinder agent) throws RemoteException; public void unbindBackupAgent(ApplicationInfo appInfo) throws RemoteException; + public void killApplicationProcess(String processName, int uid) throws RemoteException; public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, IInstrumentationWatcher watcher) @@ -230,7 +242,6 @@ public interface IActivityManager extends IInterface { // Special low-level communication with activity manager. public void startRunning(String pkg, String cls, String action, String data) throws RemoteException; - public void systemReady() throws RemoteException; // Returns 1 if the user wants to debug. public int handleApplicationError(IBinder app, int flags, /* 1 == can debug */ @@ -271,6 +282,12 @@ public interface IActivityManager extends IInterface { public void closeSystemDialogs(String reason) throws RemoteException; + public Debug.MemoryInfo[] getProcessMemoryInfo(int[] pids) + throws RemoteException; + + public void overridePendingTransition(IBinder token, String packageName, + int enterAnim, int exitAnim) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -362,7 +379,7 @@ public interface IActivityManager extends IInterface { int PUBLISH_CONTENT_PROVIDERS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+29; int SET_PERSISTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+30; int FINISH_SUB_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+31; - int SYSTEM_READY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32; + int GET_RUNNING_SERVICE_CONTROL_PANEL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32; int START_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33; int STOP_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34; int BIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35; @@ -427,4 +444,8 @@ public interface IActivityManager extends IInterface { int START_ACTIVITY_IN_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+94; int KILL_APPLICATION_WITH_UID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+95; int CLOSE_SYSTEM_DIALOGS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+96; + int GET_PROCESS_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+97; + int KILL_APPLICATION_PROCESS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+98; + int START_ACTIVITY_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+99; + int OVERRIDE_PENDING_TRANSITION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+100; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index c9157702ea57580f7c42e2d145cc834ffe87bbb1..89a52fd70d50ea077f3cd9fc576012d2f2b1047e 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -25,6 +25,7 @@ import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.os.Bundle; +import android.os.Debug; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.IBinder; @@ -71,7 +72,8 @@ public interface IApplicationThread extends IInterface { Intent intent, boolean rebind) throws RemoteException; void scheduleUnbindService(IBinder token, Intent intent) throws RemoteException; - void scheduleServiceArgs(IBinder token, int startId, Intent args) throws RemoteException; + void scheduleServiceArgs(IBinder token, int startId, int flags, Intent args) + throws RemoteException; void scheduleStopService(IBinder token) throws RemoteException; static final int DEBUG_OFF = 0; static final int DEBUG_ON = 1; @@ -81,6 +83,7 @@ public interface IApplicationThread extends IInterface { IInstrumentationWatcher testWatcher, int debugMode, boolean restrictedBackupMode, Configuration config, Map services) throws RemoteException; void scheduleExit() throws RemoteException; + void scheduleSuicide() throws RemoteException; void requestThumbnail(IBinder token) throws RemoteException; void scheduleConfigurationChanged(Configuration config) throws RemoteException; void updateTimeZone() throws RemoteException; @@ -88,7 +91,7 @@ public interface IApplicationThread extends IInterface { void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args) throws RemoteException; void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, - int resultCode, String data, Bundle extras, boolean ordered) + int resultCode, String data, Bundle extras, boolean ordered, boolean sticky) throws RemoteException; void scheduleLowMemory() throws RemoteException; void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException; @@ -96,6 +99,7 @@ public interface IApplicationThread extends IInterface { void profilerControl(boolean start, String path, ParcelFileDescriptor fd) throws RemoteException; void setSchedulingGroup(int group) throws RemoteException; + void getMemoryInfo(Debug.MemoryInfo outInfo) throws RemoteException; String descriptor = "android.app.IApplicationThread"; @@ -129,4 +133,6 @@ public interface IApplicationThread extends IInterface { int SET_SCHEDULING_GROUP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+28; int SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+29; int SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+30; + int GET_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+31; + int SCHEDULE_SUICIDE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32; } diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index c1035b6762ae2bd0dfbae178ec71fbc671443977..4d5238c582f7b0445dc8db05d03437bd9fb16a68 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -24,11 +24,15 @@ import android.content.Intent; /** {@hide} */ interface INotificationManager { + /** @deprecated use {@link #enqueueNotificationWithTag} instead */ void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived); + /** @deprecated use {@link #cancelNotificationWithTag} instead */ void cancelNotification(String pkg, int id); void cancelAllNotifications(String pkg); void enqueueToast(String pkg, ITransientNotification callback, int duration); void cancelToast(String pkg, ITransientNotification callback); + void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived); + void cancelNotificationWithTag(String pkg, String tag, int id); } diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl index bd725443e13a9a4dee48359d231a3e2346f9b154..a7d6378910147b8348ff1b212aeb2a7c35856539 100644 --- a/core/java/android/app/ISearchManager.aidl +++ b/core/java/android/app/ISearchManager.aidl @@ -36,6 +36,16 @@ interface ISearchManager { boolean globalSearch, ISearchManagerCallback searchManagerCallback, int ident); + + void triggerSearch(in String query, + in ComponentName launchActivity, + in Bundle appSearchData, + ISearchManagerCallback searchManagerCallback, + int ident); + void stopSearch(); + + boolean isVisible(); + } diff --git a/core/java/android/app/IWallpaperService.aidl b/core/java/android/app/IWallpaperManager.aidl similarity index 70% rename from core/java/android/app/IWallpaperService.aidl rename to core/java/android/app/IWallpaperManager.aidl index a332b1aea9533e6c27ceb199d5d666e4b5449ae5..69f64a11fde3bde20dee2018bd5674ec27c97df7 100644 --- a/core/java/android/app/IWallpaperService.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -16,21 +16,35 @@ package android.app; +import android.os.Bundle; import android.os.ParcelFileDescriptor; -import android.app.IWallpaperServiceCallback; +import android.app.IWallpaperManagerCallback; +import android.app.WallpaperInfo; +import android.content.ComponentName; /** @hide */ -interface IWallpaperService { +interface IWallpaperManager { /** * Set the wallpaper. */ - ParcelFileDescriptor setWallpaper(); + ParcelFileDescriptor setWallpaper(String name); + + /** + * Set the live wallpaper. + */ + void setWallpaperComponent(in ComponentName name); /** * Get the wallpaper. */ - ParcelFileDescriptor getWallpaper(IWallpaperServiceCallback cb); + ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb, + out Bundle outParams); + + /** + * Get information about a live wallpaper. + */ + WallpaperInfo getWallpaperInfo(); /** * Clear the wallpaper. diff --git a/core/java/android/app/IWallpaperServiceCallback.aidl b/core/java/android/app/IWallpaperManagerCallback.aidl similarity index 89% rename from core/java/android/app/IWallpaperServiceCallback.aidl rename to core/java/android/app/IWallpaperManagerCallback.aidl index 6086f400267d474c5e1b0e84c18f45311cadc8e3..991b2bc924b5fa34fd0ab84d7449bd7062452693 100644 --- a/core/java/android/app/IWallpaperServiceCallback.aidl +++ b/core/java/android/app/IWallpaperManagerCallback.aidl @@ -17,13 +17,13 @@ package android.app; /** - * Callback interface used by IWallpaperService to send asynchronous + * Callback interface used by IWallpaperManager to send asynchronous * notifications back to its clients. Note that this is a * one-way interface so the server does not block waiting for the client. * * @hide */ -oneway interface IWallpaperServiceCallback { +oneway interface IWallpaperManagerCallback { /** * Called when the wallpaper has changed */ diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index e31f4f834404d9562e6cab0e3757cda156b844e7..b8c3aa3622641996b5340bde6c52418cfb3f396e 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -24,6 +24,7 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; +import android.os.PerformanceCollector; import android.os.RemoteException; import android.os.Debug; import android.os.IBinder; @@ -83,10 +84,8 @@ public class Instrumentation { private List mWaitingActivities; private List mActivityMonitors; private IInstrumentationWatcher mWatcher; - private long mPreCpuTime; - private long mStart; private boolean mAutomaticPerformanceSnapshots = false; - private Bundle mPrePerfMetrics = new Bundle(); + private PerformanceCollector mPerformanceCollector; private Bundle mPerfMetrics = new Bundle(); public Instrumentation() { @@ -191,96 +190,21 @@ public class Instrumentation { public void setAutomaticPerformanceSnapshots() { mAutomaticPerformanceSnapshots = true; + mPerformanceCollector = new PerformanceCollector(); } public void startPerformanceSnapshot() { - mStart = 0; if (!isProfiling()) { - // Add initial binder counts - Bundle binderCounts = getBinderCounts(); - for (String key: binderCounts.keySet()) { - addPerfMetricLong("pre_" + key, binderCounts.getLong(key)); - } - - // Force a GC and zero out the performance counters. Do this - // before reading initial CPU/wall-clock times so we don't include - // the cost of this setup in our final metrics. - startAllocCounting(); - - // Record CPU time up to this point, and start timing. Note: this - // must happen at the end of this method, otherwise the timing will - // include noise. - mStart = SystemClock.uptimeMillis(); - mPreCpuTime = Process.getElapsedCpuTime(); + mPerformanceCollector.beginSnapshot(null); } } public void endPerformanceSnapshot() { if (!isProfiling()) { - // Stop the timing. This must be done first before any other counting is stopped. - long cpuTime = Process.getElapsedCpuTime(); - long duration = SystemClock.uptimeMillis(); - - stopAllocCounting(); - - long nativeMax = Debug.getNativeHeapSize() / 1024; - long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; - long nativeFree = Debug.getNativeHeapFreeSize() / 1024; - - Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); - Debug.getMemoryInfo(memInfo); - - Runtime runtime = Runtime.getRuntime(); - - long dalvikMax = runtime.totalMemory() / 1024; - long dalvikFree = runtime.freeMemory() / 1024; - long dalvikAllocated = dalvikMax - dalvikFree; - - // Add final binder counts - Bundle binderCounts = getBinderCounts(); - for (String key: binderCounts.keySet()) { - addPerfMetricLong(key, binderCounts.getLong(key)); - } - - // Add alloc counts - Bundle allocCounts = getAllocCounts(); - for (String key: allocCounts.keySet()) { - addPerfMetricLong(key, allocCounts.getLong(key)); - } - - addPerfMetricLong("execution_time", duration - mStart); - addPerfMetricLong("pre_cpu_time", mPreCpuTime); - addPerfMetricLong("cpu_time", cpuTime - mPreCpuTime); - - addPerfMetricLong("native_size", nativeMax); - addPerfMetricLong("native_allocated", nativeAllocated); - addPerfMetricLong("native_free", nativeFree); - addPerfMetricInt("native_pss", memInfo.nativePss); - addPerfMetricInt("native_private_dirty", memInfo.nativePrivateDirty); - addPerfMetricInt("native_shared_dirty", memInfo.nativeSharedDirty); - - addPerfMetricLong("java_size", dalvikMax); - addPerfMetricLong("java_allocated", dalvikAllocated); - addPerfMetricLong("java_free", dalvikFree); - addPerfMetricInt("java_pss", memInfo.dalvikPss); - addPerfMetricInt("java_private_dirty", memInfo.dalvikPrivateDirty); - addPerfMetricInt("java_shared_dirty", memInfo.dalvikSharedDirty); - - addPerfMetricInt("other_pss", memInfo.otherPss); - addPerfMetricInt("other_private_dirty", memInfo.otherPrivateDirty); - addPerfMetricInt("other_shared_dirty", memInfo.otherSharedDirty); - + mPerfMetrics = mPerformanceCollector.endSnapshot(); } } - private void addPerfMetricLong(String key, long value) { - mPerfMetrics.putLong("performance." + key, value); - } - - private void addPerfMetricInt(String key, int value) { - mPerfMetrics.putInt("performance." + key, value); - } - /** * Called when the instrumented application is stopping, after all of the * normal application cleanup has occurred. @@ -1468,7 +1392,7 @@ public class Instrumentation { mWatcher = watcher; } - /*package*/ static void checkStartActivityResult(int res, Intent intent) { + /*package*/ static void checkStartActivityResult(int res, Object intent) { if (res >= IActivityManager.START_SUCCESS) { return; } @@ -1476,10 +1400,10 @@ public class Instrumentation { switch (res) { case IActivityManager.START_INTENT_NOT_RESOLVED: case IActivityManager.START_CLASS_NOT_FOUND: - if (intent.getComponent() != null) + if (intent instanceof Intent && ((Intent)intent).getComponent() != null) throw new ActivityNotFoundException( "Unable to find explicit activity class " - + intent.getComponent().toShortString() + + ((Intent)intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?"); throw new ActivityNotFoundException( "No Activity found to handle " + intent); @@ -1489,6 +1413,9 @@ public class Instrumentation { case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: throw new AndroidRuntimeException( "FORWARD_RESULT_FLAG used while also requesting a result"); + case IActivityManager.START_NOT_ACTIVITY: + throw new IllegalArgumentException( + "PendingIntent is not an activity"); default: throw new AndroidRuntimeException("Unknown error code " + res + " when starting " + intent); diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java index 2b12a2a1304d133861d743bb9af145d3aefd4c3a..804c8eb6285feac947a7889ae56a1e3a4ec24df6 100644 --- a/core/java/android/app/IntentService.java +++ b/core/java/android/app/IntentService.java @@ -18,6 +18,7 @@ public abstract class IntentService extends Service { private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private String mName; + private boolean mRedelivery; private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { @@ -36,6 +37,19 @@ public abstract class IntentService extends Service { mName = name; } + /** + * Control redelivery of intents. If called with true, + * {@link #onStartCommand(Intent, int, int)} will return + * {@link Service#START_REDELIVER_INTENT} instead of + * {@link Service#START_NOT_STICKY}, so that if this service's process + * is called while it is executing the Intent in + * {@link #onHandleIntent(Intent)}, then when later restarted the same Intent + * will be re-delivered to it, to retry its execution. + */ + public void setIntentRedelivery(boolean enabled) { + mRedelivery = enabled; + } + @Override public void onCreate() { super.onCreate(); @@ -48,13 +62,18 @@ public abstract class IntentService extends Service { @Override public void onStart(Intent intent, int startId) { - super.onStart(intent, startId); Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); } + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + onStart(intent, startId); + return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; + } + @Override public void onDestroy() { mServiceLooper.quit(); diff --git a/core/java/android/app/LauncherActivity.java b/core/java/android/app/LauncherActivity.java index d788c43d7d632e6daee9d25783f43a1a6f1a3587..0ece2fc23101189e4da38f82e1c48cd31f30c681 100644 --- a/core/java/android/app/LauncherActivity.java +++ b/core/java/android/app/LauncherActivity.java @@ -18,6 +18,7 @@ package android.app; import android.content.Context; import android.content.Intent; +import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -52,9 +53,9 @@ import java.util.List; * */ public abstract class LauncherActivity extends ListActivity { - Intent mIntent; PackageManager mPackageManager; + IconResizer mIconResizer; /** * An item in the list @@ -70,13 +71,17 @@ public abstract class LauncherActivity extends ListActivity { ListItem(PackageManager pm, ResolveInfo resolveInfo, IconResizer resizer) { this.resolveInfo = resolveInfo; label = resolveInfo.loadLabel(pm); - if (label == null && resolveInfo.activityInfo != null) { + ComponentInfo ci = resolveInfo.activityInfo; + if (ci == null) ci = resolveInfo.serviceInfo; + if (label == null && ci != null) { label = resolveInfo.activityInfo.name; } - icon = resizer.createIconThumbnail(resolveInfo.loadIcon(pm)); - packageName = resolveInfo.activityInfo.applicationInfo.packageName; - className = resolveInfo.activityInfo.name; + if (resizer != null) { + icon = resizer.createIconThumbnail(resolveInfo.loadIcon(pm)); + } + packageName = ci.applicationInfo.packageName; + className = ci.name; } public ListItem() { @@ -90,13 +95,15 @@ public abstract class LauncherActivity extends ListActivity { private final Object lock = new Object(); private ArrayList mOriginalValues; + protected final IconResizer mIconResizer; protected final LayoutInflater mInflater; protected List mActivitiesList; private Filter mFilter; - public ActivityAdapter() { + public ActivityAdapter(IconResizer resizer) { + mIconResizer = resizer; mInflater = (LayoutInflater) LauncherActivity.this.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mActivitiesList = makeListItems(); @@ -151,6 +158,10 @@ public abstract class LauncherActivity extends ListActivity { private void bindView(View view, ListItem item) { TextView text = (TextView) view; text.setText(item.label); + if (item.icon == null) { + item.icon = mIconResizer.createIconThumbnail( + item.resolveInfo.loadIcon(getPackageManager())); + } text.setCompoundDrawablesWithIntrinsicBounds(item.icon, null, null, null); } @@ -325,12 +336,13 @@ public abstract class LauncherActivity extends ListActivity { requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setProgressBarIndeterminateVisibility(true); - setContentView(com.android.internal.R.layout.activity_list); - + onSetContentView(); + mIconResizer = new IconResizer(); + mIntent = new Intent(getTargetIntent()); mIntent.setComponent(null); - mAdapter = new ActivityAdapter(); + mAdapter = new ActivityAdapter(mIconResizer); setListAdapter(mAdapter); getListView().setTextFilterEnabled(true); @@ -338,10 +350,17 @@ public abstract class LauncherActivity extends ListActivity { setProgressBarIndeterminateVisibility(false); } + /** + * Override to call setContentView() with your own content view to + * customize the list layout. + */ + protected void onSetContentView() { + setContentView(com.android.internal.R.layout.activity_list); + } + @Override protected void onListItemClick(ListView l, View v, int position, long id) { - Intent intent = ((ActivityAdapter)mAdapter).intentForPosition(position); - + Intent intent = intentForPosition(position); startActivity(intent); } @@ -373,22 +392,27 @@ public abstract class LauncherActivity extends ListActivity { return new Intent(); } + /** + * Perform query on package manager for list items. The default + * implementation queries for activities. + */ + protected List onQueryPackageManager(Intent queryIntent) { + return mPackageManager.queryIntentActivities(queryIntent, /* no flags */ 0); + } + /** * Perform the query to determine which results to show and return a list of them. */ public List makeListItems() { // Load all matching activities and sort correctly - List list = mPackageManager.queryIntentActivities(mIntent, - /* no flags */ 0); + List list = onQueryPackageManager(mIntent); Collections.sort(list, new ResolveInfo.DisplayNameComparator(mPackageManager)); - IconResizer resizer = new IconResizer(); - ArrayList result = new ArrayList(list.size()); int listSize = list.size(); for (int i = 0; i < listSize; i++) { ResolveInfo resolveInfo = list.get(i); - result.add(new ListItem(mPackageManager, resolveInfo, resizer)); + result.add(new ListItem(mPackageManager, resolveInfo, null)); } return result; diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 9834c75c58d4b6b932c567f469427b0b2b4cf6d6..be5a7d3b8457f7638bbe5ca46c62ffb176ee9560 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -257,6 +257,13 @@ public class Notification implements Parcelable */ public static final int FLAG_NO_CLEAR = 0x00000020; + /** + * Bit to be bitwise-ored into the {@link #flags} field that should be + * set if this notification represents a currently running service. This + * will normally be set for you by {@link Service#startForeground}. + */ + public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; + public int flags; /** @@ -458,7 +465,9 @@ public class Notification implements Parcelable sb.append(this.vibrate[i]); sb.append(','); } - sb.append(this.vibrate[N]); + if (N != -1) { + sb.append(this.vibrate[N]); + } sb.append("]"); } else if ((this.defaults & DEFAULT_VIBRATE) != 0) { sb.append("default"); diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 39edab70d141584cdb1638e67db0cf955e7a5209..6fe12fcf0bda64d95ad4d6fe93d81a91512db055 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -61,7 +61,8 @@ public class NotificationManager private static INotificationManager sService; - static private INotificationManager getService() + /** @hide */ + static public INotificationManager getService() { if (sService != null) { return sService; @@ -85,13 +86,28 @@ public class NotificationManager * notify the user, other than the view you're providing. Must not be null. */ public void notify(int id, Notification notification) + { + notify(null, id, notification); + } + + /** + * Persistent notification on the status bar, + * + * @param tag An string identifier for this notification unique within your + * application. + * @param notification A {@link Notification} object describing how to + * notify the user, other than the view you're providing. Must not be null. + * @return the id of the notification that is associated with the string identifier that + * can be used to cancel the notification + */ + public void notify(String tag, int id, Notification notification) { int[] idOut = new int[1]; INotificationManager service = getService(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); try { - service.enqueueNotification(pkg, id, notification, idOut); + service.enqueueNotificationWithTag(pkg, tag, id, notification, idOut); if (id != idOut[0]) { Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } @@ -105,12 +121,22 @@ public class NotificationManager * bar. */ public void cancel(int id) + { + cancel(null, id); + } + + /** + * Cancel a previously shown notification. If it's transient, the view + * will be hidden. If it's persistent, it will be removed from the status + * bar. + */ + public void cancel(String tag, int id) { INotificationManager service = getService(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); try { - service.cancelNotification(pkg, id); + service.cancelNotificationWithTag(pkg, tag, id); } catch (RemoteException e) { } } diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index f7479bcdb8bf0fd83b605ec28bfb71938c054366..be1dc4acb150d6e6b3f20e136b0edcb2c3edaf60 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -147,7 +147,7 @@ public final class PendingIntent implements Parcelable { mHandler = handler; } public void performReceive(Intent intent, int resultCode, - String data, Bundle extras, boolean serialized) { + String data, Bundle extras, boolean serialized, boolean sticky) { mIntent = intent; mResultCode = resultCode; mResultData = data; @@ -519,7 +519,8 @@ public final class PendingIntent implements Parcelable { mTarget = IIntentSender.Stub.asInterface(target); } - /*package*/ IIntentSender getTarget() { + /** @hide */ + public IIntentSender getTarget() { return mTarget; } } diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 5844079e616cbb0a1dc48254a1176416f2f4f2ff..e5a769bb59dd6161be38dd1d6e2831106650c611 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -30,7 +30,6 @@ import android.content.pm.ResolveInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -51,7 +50,6 @@ import android.util.Log; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; -import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -65,9 +63,9 @@ import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; -import android.widget.ListAdapter; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; @@ -98,6 +96,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // The extra key used in an intent to the speech recognizer for in-app voice search. private static final String EXTRA_CALLING_PACKAGE = "calling_package"; + + // The string used for privateImeOptions to identify to the IME that it should not show + // a microphone button since one already exists in the search dialog. + private static final String IME_OPTION_NO_MICROPHONE = "nm"; private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; @@ -129,8 +131,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private ArrayList mPreviousComponents; // For voice searching - private Intent mVoiceWebSearchIntent; - private Intent mVoiceAppSearchIntent; + private final Intent mVoiceWebSearchIntent; + private final Intent mVoiceAppSearchIntent; // support for AutoCompleteTextView suggestions display private SuggestionsAdapter mSuggestionsAdapter; @@ -158,18 +160,25 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ public SearchDialog(Context context) { super(context, com.android.internal.R.style.Theme_GlobalSearchBar); + + // Save voice intent for later queries/launching + mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); + + mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } /** - * We create the search dialog just once, and it stays around (hidden) - * until activated by the user. + * Create the search dialog and any resources that are used for the + * entire lifetime of the dialog. */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(com.android.internal.R.layout.search_bar); - Window theWindow = getWindow(); WindowManager.LayoutParams lp = theWindow.getAttributes(); lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR; @@ -182,7 +191,21 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; theWindow.setAttributes(lp); + // Touching outside of the search dialog will dismiss it + setCanceledOnTouchOutside(true); + } + + /** + * We recreate the dialog view each time it becomes visible so as to limit + * the scope of any problems with the contained resources. + */ + private void createContentView() { + setContentView(com.android.internal.R.layout.search_bar); + // get the view elements for local access + SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar); + searchBar.setSearchDialog(this); + mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); mSearchAutoComplete = (SearchAutoComplete) findViewById(com.android.internal.R.id.search_src_text); @@ -192,7 +215,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchPlate = findViewById(com.android.internal.R.id.search_plate); mWorkingSpinner = getContext().getResources(). getDrawable(com.android.internal.R.drawable.search_spinner); - + mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( + null, null, mWorkingSpinner, null); + setWorking(false); + // attach listeners mSearchAutoComplete.addTextChangedListener(mTextWatcher); mSearchAutoComplete.setOnKeyListener(mTextKeyListener); @@ -203,25 +229,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mVoiceButton.setOnClickListener(mVoiceButtonClickListener); mVoiceButton.setOnKeyListener(mButtonsKeyListener); - mSearchAutoComplete.setSearchDialog(this); - // pre-hide all the extraneous elements mBadgeLabel.setVisibility(View.GONE); // Additional adjustments to make Dialog work for Search - - // Touching outside of the search dialog will dismiss it - setCanceledOnTouchOutside(true); - - // Save voice intent for later queries/launching - mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); - mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, - RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); - - mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); - mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions(); } @@ -356,9 +367,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // isDefaultSearchable() should always give the same result. mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable); mActivityContext = mSearchable.getActivityContext(getContext()); - + // show the dialog. this will call onStart(). - if (!isShowing()) { + if (!isShowing()) { + // Recreate the search bar view every time the dialog is shown, to get rid + // of any bad state in the AutoCompleteTextView etc + createContentView(); + // The Dialog uses a ContextThemeWrapper for the context; use this to change the // theme out from underneath us, between the global search theme and the in-app // search theme. They are identical except that the global search theme does not @@ -408,15 +423,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @param working true to show spinner, false to hide spinner */ public void setWorking(boolean working) { - if (working) { - mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( - null, null, mWorkingSpinner, null); - ((Animatable) mWorkingSpinner).start(); - } else { - mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( - null, null, null, null); - ((Animatable) mWorkingSpinner).stop(); - } + mWorkingSpinner.setAlpha(working ? 255 : 0); + mWorkingSpinner.setVisible(working, false); + mWorkingSpinner.invalidateSelf(); } /** @@ -502,6 +511,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS updateSearchAppIcon(); updateSearchBadge(); updateQueryHint(); + mSearchAutoComplete.showDropDownAfterLayout(); } } @@ -537,6 +547,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchAutoComplete.setInputType(inputType); mSearchAutoCompleteImeOptions = mSearchable.getImeOptions(); mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions); + + // If the search dialog is going to show a voice search button, then don't let + // the soft keyboard display a microphone button if it would have otherwise. + if (mSearchable.getVoiceSearchEnabled()) { + mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); + } else { + mSearchAutoComplete.setPrivateImeOptions(null); + } } } @@ -740,17 +758,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return false; } - // handle back key to go back to previous searchable, etc. - if (handleBackKey(keyCode, event)) { + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) { + event.startTracking(); + // Consume search key for later use. return true; } - - if (keyCode == KeyEvent.KEYCODE_SEARCH) { - // If the search key is pressed, toggle between global and in-app search. If we are - // currently doing global search and there is no in-app search context to toggle to, - // just don't do anything. - return toggleGlobalSearch(); - } // if it's an action specified by the searchable activity, launch the // entered query with the action key @@ -759,8 +771,26 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); return true; } - - return false; + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")"); + if (mSearchable == null) { + return false; + } + + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking() + && !event.isCanceled()) { + // If the search key is pressed, toggle between global and in-app search. If we are + // currently doing global search and there is no in-app search context to toggle to, + // just don't do anything. + return toggleGlobalSearch(); + } + + return super.onKeyUp(keyCode, event); } /** @@ -1120,7 +1150,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS /** * Launch a search for the text in the query text field. */ - protected void launchQuerySearch() { + public void launchQuerySearch() { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); } @@ -1136,7 +1166,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS String query = mSearchAutoComplete.getText().toString(); String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH; Intent intent = createIntent(action, null, null, query, null, - actionKey, actionMsg); + actionKey, actionMsg, null); + // Allow GlobalSearch to log and create shortcut for searches launched by + // the search button, enter key or an action key. + if (mGlobalSearchMode) { + mSuggestionsAdapter.reportSearch(query); + } launchIntent(intent); } @@ -1169,7 +1204,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // report back about the click if (mGlobalSearchMode) { // in global search mode, do it via cursor - mSuggestionsAdapter.callCursorOnClick(c, position); + mSuggestionsAdapter.callCursorOnClick(c, position, actionKey, actionMsg); } else if (intent != null && mPreviousComponents != null && !mPreviousComponents.isEmpty()) { @@ -1206,7 +1241,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction()); cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString()); cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME, - intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY)); + intent.getComponent().flattenToShortString()); // ensure the icons will work for global search cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, @@ -1292,6 +1327,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // intent, and to avoid the extra step of going through GlobalSearch. if (mGlobalSearchMode) { launchGlobalSearchIntent(intent); + if (mStoredComponentName != null) { + // If we're embedded in an application, dismiss the dialog. + // This ensures that if the intent is handled by the current + // activity, it's not obscured by the dialog. + dismiss(); + } } else { // If the intent was created from a suggestion, it will always have an explicit // component here. @@ -1462,6 +1503,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchAutoComplete.setListSelection(index); } + /** + * Checks if there are any previous searchable components in the history stack. + */ + private boolean hasPreviousComponent() { + return mPreviousComponents != null && !mPreviousComponents.isEmpty(); + } + /** * Saves the previous component that was searched, so that we can go * back to it. @@ -1480,14 +1528,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * no previous component. */ private ComponentName popPreviousComponent() { - if (mPreviousComponents == null) { - return null; - } - int size = mPreviousComponents.size(); - if (size == 0) { + if (!hasPreviousComponent()) { return null; } - return mPreviousComponents.remove(size - 1); + return mPreviousComponents.remove(mPreviousComponents.size() - 1); } /** @@ -1500,17 +1544,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (previous == null) { return false; } + if (!show(previous, mAppSearchData, false)) { Log.w(LOG_TAG, "Failed to switch to source " + previous); return false; } - + // must touch text to trigger suggestions // TODO: should this be the text as it was when the user left // the source that we are now going back to? String query = mSearchAutoComplete.getText().toString(); setUserQuery(query); - return true; } @@ -1563,9 +1607,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); + String mode = mGlobalSearchMode ? SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION : null; return createIntent(action, dataUri, extraData, query, componentName, actionKey, - actionMsg); + actionMsg, mode); } catch (RuntimeException e ) { int rowNum; try { // be really paranoid now @@ -1591,13 +1636,21 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, * or null if none. + * @param mode The search mode, one of the acceptable values for + * {@link SearchManager#SEARCH_MODE}, or {@code null}. * @return The intent. */ private Intent createIntent(String action, Uri data, String extraData, String query, - String componentName, int actionKey, String actionMsg) { + String componentName, int actionKey, String actionMsg, String mode) { // Now build the Intent Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // We need CLEAR_TOP to avoid reusing an old task that has other activities + // on top of the one we want. We don't want to do this in in-app search though, + // as it can be destructive to the activity stack. + if (mGlobalSearchMode) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + } if (data != null) { intent.setData(data); } @@ -1618,6 +1671,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS intent.putExtra(SearchManager.ACTION_KEY, actionKey); intent.putExtra(SearchManager.ACTION_MSG, actionMsg); } + if (mode != null) { + intent.putExtra(SearchManager.SEARCH_MODE, mode); + } // Only allow 3rd-party intents from GlobalSearch if (!mGlobalSearchMode) { intent.setComponent(mSearchable.getSearchActivity()); @@ -1648,15 +1704,59 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } return result; } - + + /** + * The root element in the search bar layout. This is a custom view just to override + * the handling of the back button. + */ + public static class SearchBar extends LinearLayout { + + private SearchDialog mSearchDialog; + + public SearchBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SearchBar(Context context) { + super(context); + } + + public void setSearchDialog(SearchDialog searchDialog) { + mSearchDialog = searchDialog; + } + + /** + * Overrides the handling of the back key to move back to the previous sources or dismiss + * the search dialog, instead of dismissing the input method. + */ + @Override + public boolean dispatchKeyEventPreIme(KeyEvent event) { + if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); + if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + KeyEvent.DispatcherState state = getKeyDispatcherState(); + if (state != null) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + state.startTracking(event, this); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP + && !event.isCanceled() && state.isTracking(event)) { + mSearchDialog.onBackPressed(); + return true; + } + } + } + return super.dispatchKeyEventPreIme(event); + } + } + /** * Local subclass for AutoCompleteTextView. */ public static class SearchAutoComplete extends AutoCompleteTextView { private int mThreshold; - private SearchDialog mSearchDialog; - + public SearchAutoComplete(Context context) { super(context); mThreshold = getThreshold(); @@ -1672,10 +1772,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mThreshold = getThreshold(); } - private void setSearchDialog(SearchDialog searchDialog) { - mSearchDialog = searchDialog; - } - @Override public void setThreshold(int threshold) { super.setThreshold(threshold); @@ -1729,54 +1825,26 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return mThreshold <= 0 || super.enoughToFilter(); } - /** - * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK, - * so we must override this method to modify the BACK behavior. - */ - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (mSearchDialog.mSearchable == null) { - return false; - } - if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { - if (mSearchDialog.backToPreviousComponent()) { - return true; - } - // If the drop-down obscures the keyboard, the user wouldn't see anything - // happening when pressing back, so we dismiss the entire dialog instead. - // - // also: if there is no text entered, we also want to dismiss the whole dialog, - // not just the soft keyboard. the exception to this is if there are shortcuts - // that aren't displayed (e.g are being obscured by the soft keyboard); in that - // case we want to dismiss the soft keyboard so the user can see the rest of the - // shortcuts. - if (isInputMethodNotNeeded() || - (isEmpty() && getDropDownChildCount() >= getAdapterCount())) { - mSearchDialog.cancel(); - return true; - } - return false; // will dismiss soft keyboard if necessary - } - return false; - } + } - private int getAdapterCount() { - final ListAdapter adapter = getAdapter(); - return adapter == null ? 0 : adapter.getCount(); + @Override + public void onBackPressed() { + // If the input method is covering the search dialog completely, + // e.g. in landscape mode with no hard keyboard, dismiss just the input method + InputMethodManager imm = (InputMethodManager)getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && imm.isFullscreenMode() && + imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { + return; } - } - - protected boolean handleBackKey(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { - if (backToPreviousComponent()) { - return true; - } + // Otherwise, go back to any previous source (e.g. back to QSB when + // pivoted into a source. + if (!backToPreviousComponent()) { + // If no previous source, close search dialog cancel(); - return true; } - return false; } - + /** * Implements OnItemClickListener */ diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 2245562763cfb702cd349d1990f715609321b918..7f5a1e7bac53801f2b9d6fe4e992353eaf3fac3c 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -27,6 +27,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.server.search.SearchableInfo; +import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; @@ -329,8 +330,8 @@ import java.util.List; * you'll need to update your searchable activity (or other activities) to receive the intents * as you've defined them. *

  • Implement a Content Provider that provides suggestions. If you already have one, and it - * has access to your suggestions data. If not, you'll have to create one. - * You'll also provide information about your Content Provider in your + * has access to your suggestions data, you can use that provider. If not, you'll have to create + * one. You'll also provide information about your Content Provider in your * package's manifest.
  • *
  • Update your searchable activity's XML configuration file. There are two categories of * information used for suggestions: @@ -768,8 +769,11 @@ import java.util.List; * * * android:icon - * If provided, this icon will be used in place of the label string. This - * is provided in order to present logos or other non-textual banners. + * If provided, this icon will be shown in place of the label above the search box. + * This is a reference to a drawable (icon) resource. Note that the application icon + * is also used as an icon to the left of the search box and you cannot modify this + * behavior, so including the icon attribute is unecessary and this may be + * deprecated in the future. * No * * @@ -778,11 +782,6 @@ import java.util.List; * entered. * No * - * - * android:searchButtonText - * If provided, this text will replace the default text in the "Search" button. - * No - * * * android:searchMode * If provided and non-zero, sets additional modes for control of the search @@ -791,15 +790,17 @@ import java.util.List; * * showSearchLabelAsBadge * If set, this flag enables the display of the search target (label) - * within the search bar. If this flag and showSearchIconAsBadge + * above the search box. If this flag and showSearchIconAsBadge * (see below) are both not set, no badge will be shown. * * showSearchIconAsBadge - * If set, this flag enables the display of the search target (icon) within - * the search bar. If this flag and showSearchLabelAsBadge + * If set, this flag enables the display of the search target (icon) + * above the search box. If this flag and showSearchLabelAsBadge * (see above) are both not set, no badge will be shown. If both flags * are set, showSearchIconAsBadge has precedence and the icon will be - * shown. + * shown. Because the application icon is now used to the left of the + * search box by default, using this search mode is no longer necessary + * and may be deprecated in the future. * * queryRewriteFromData * If set, this flag causes the suggestion column SUGGEST_COLUMN_INTENT_DATA @@ -1180,7 +1181,7 @@ import java.util.List; * Bundle appData = new Bundle(); * appData.put...(); * appData.put...(); - * startSearch(null, false, appData); + * startSearch(null, false, appData, false); * return true; * } * @@ -1287,6 +1288,25 @@ public class SearchManager */ public final static String SOURCE = "source"; + /** + * Intent extra data key: Use {@link android.content.Intent#getBundleExtra + * content.Intent.getBundleExtra(SEARCH_MODE)} to get the search mode used + * to launch the intent. + * The only current value for this is {@link #MODE_GLOBAL_SEARCH_SUGGESTION}. + * + * @hide + */ + public final static String SEARCH_MODE = "search_mode"; + + /** + * Value for the {@link #SEARCH_MODE} key. + * This is used if the intent was launched by clicking a suggestion in global search + * mode (Quick Search Box). + * + * @hide + */ + public static final String MODE_GLOBAL_SEARCH_SUGGESTION = "global_search_suggestion"; + /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getIntExtra content.Intent.getIntExtra()} @@ -1342,6 +1362,10 @@ public class SearchManager = "DialogCursorProtocol.CLICK.sendPosition"; public final static String CLICK_SEND_MAX_DISPLAY_POS = "DialogCursorProtocol.CLICK.sendDisplayPosition"; + public final static String CLICK_SEND_ACTION_KEY + = "DialogCursorProtocol.CLICK.sendActionKey"; + public final static String CLICK_SEND_ACTION_MSG + = "DialogCursorProtocol.CLICK.sendActionMsg"; public final static String CLICK_RECEIVE_SELECTED_POS = "DialogCursorProtocol.CLICK.receiveSelectedPosition"; @@ -1349,6 +1373,14 @@ public class SearchManager * When the threshold received in {@link #POST_REFRESH_RECEIVE_DISPLAY_NOTIFY} is displayed. */ public final static int THRESH_HIT = 3; + + /** + * When a search is started without using a suggestion. + */ + public final static int SEARCH = 4; + public final static String SEARCH_SEND_MAX_DISPLAY_POS + = "DialogCursorProtocol.SEARCH.sendDisplayPosition"; + public final static String SEARCH_SEND_QUERY = "DialogCursorProtocol.SEARCH.query"; } /** @@ -1558,6 +1590,12 @@ public class SearchManager */ public final static String SUGGEST_NEVER_MAKE_SHORTCUT = "_-1"; + /** + * Query parameter added to suggestion queries to limit the number of suggestions returned. + * This limit is only advisory and suggestion providers may chose to ignore it. + */ + public final static String SUGGEST_PARAMETER_LIMIT = "limit"; + /** * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, * the search dialog will switch to a different suggestion source when the @@ -1636,7 +1674,17 @@ public class SearchManager private final Context mContext; + /** + * compact representation of the activity associated with this search manager so + * we can say who we are when starting search. the search managerservice, in turn, + * uses this to properly handle the back stack. + */ private int mIdent; + + /** + * The package associated with this seach manager. + */ + private String mAssociatedPackage; // package private since they are used by the inner class SearchManagerCallback /* package */ final Handler mHandler; @@ -1656,11 +1704,15 @@ public class SearchManager return mIdent != 0; } - /*package*/ void setIdent(int ident) { + /*package*/ void setIdent(int ident, ComponentName component) { if (mIdent != 0) { throw new IllegalStateException("mIdent already set"); } + if (component == null) { + throw new IllegalArgumentException("component must be non-null"); + } mIdent = ident; + mAssociatedPackage = component.getPackageName(); } /** @@ -1710,12 +1762,50 @@ public class SearchManager boolean globalSearch) { if (mIdent == 0) throw new IllegalArgumentException( "Called from outside of an Activity context"); + if (!globalSearch && !mAssociatedPackage.equals(launchActivity.getPackageName())) { + Log.w(TAG, "invoking app search on a different package " + + "not associated with this search manager"); + } try { // activate the search manager and start it up! mService.startSearch(initialQuery, selectInitialQuery, launchActivity, appSearchData, globalSearch, mSearchManagerCallback, mIdent); } catch (RemoteException ex) { - Log.e(TAG, "startSearch() failed: " + ex); + Log.e(TAG, "startSearch() failed.", ex); + } + } + + /** + * Similar to {@link #startSearch} but actually fires off the search query after invoking + * the search dialog. Made available for testing purposes. + * + * @param query The query to trigger. If empty, request will be ignored. + * @param launchActivity The ComponentName of the activity that has launched this search. + * @param appSearchData An application can insert application-specific + * context here, in order to improve quality or specificity of its own + * searches. This data will be returned with SEARCH intent(s). Null if + * no extra data is required. + * + * @see #startSearch + */ + public void triggerSearch(String query, + ComponentName launchActivity, + Bundle appSearchData) { + if (mIdent == 0) throw new IllegalArgumentException( + "Called from outside of an Activity context"); + if (!mAssociatedPackage.equals(launchActivity.getPackageName())) { + throw new IllegalArgumentException("invoking app search on a different package " + + "not associated with this search manager"); + } + if (query == null || TextUtils.getTrimmedLength(query) == 0) { + Log.w(TAG, "triggerSearch called with empty query, ignoring."); + return; + } + try { + mService.triggerSearch(query, launchActivity, appSearchData, mSearchManagerCallback, + mIdent); + } catch (RemoteException ex) { + Log.e(TAG, "triggerSearch() failed.", ex); } } @@ -1836,6 +1926,7 @@ public class SearchManager /** * @deprecated This method is an obsolete internal implementation detail. Do not use. */ + @Deprecated public void onCancel(DialogInterface dialog) { throw new UnsupportedOperationException(); } @@ -1843,6 +1934,7 @@ public class SearchManager /** * @deprecated This method is an obsolete internal implementation detail. Do not use. */ + @Deprecated public void onDismiss(DialogInterface dialog) { throw new UnsupportedOperationException(); } @@ -1889,6 +1981,21 @@ public class SearchManager * @hide because SearchableInfo is not part of the API. */ public Cursor getSuggestions(SearchableInfo searchable, String query) { + return getSuggestions(searchable, query, -1); + } + + /** + * Gets a cursor with search suggestions. + * + * @param searchable Information about how to get the suggestions. + * @param query The search text entered (so far). + * @param limit The query limit to pass to the suggestion provider. This is advisory, + * the returned cursor may contain more rows. Pass {@code -1} for no limit. + * @return a cursor with suggestions, or null the suggestion query failed. + * + * @hide because SearchableInfo is not part of the API. + */ + public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) { if (searchable == null) { return null; } @@ -1900,7 +2007,9 @@ public class SearchManager Uri.Builder uriBuilder = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority(authority); + .authority(authority) + .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() + .fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel() // if content path provided, insert it now final String contentPath = searchable.getSuggestPath(); @@ -1908,7 +2017,7 @@ public class SearchManager uriBuilder.appendEncodedPath(contentPath); } - // append standard suggestion query path + // append standard suggestion query path uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); // get the query selection, may be null @@ -1921,10 +2030,11 @@ public class SearchManager uriBuilder.appendPath(query); } - Uri uri = uriBuilder - .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() - .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() - .build(); + if (limit > 0) { + uriBuilder.appendQueryParameter(SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); + } + + Uri uri = uriBuilder.build(); // finally, make the query return mContext.getContentResolver().query(uri, null, selection, selArgs, null); @@ -2000,4 +2110,4 @@ public class SearchManager Thread thread = Thread.currentThread(); Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")"); } -} +} \ No newline at end of file diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index d2fb6052e9ade4501c412a322d080018ab393261..30e1712d796ed396dc97ed0504449b574d9f6473 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -22,8 +22,10 @@ import android.content.Intent; import android.content.ContextWrapper; import android.content.Context; import android.content.res.Configuration; +import android.os.Build; import android.os.RemoteException; import android.os.IBinder; +import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -62,18 +64,28 @@ import java.io.PrintWriter; *

    There are two reasons that a service can be run by the system. If someone * calls {@link android.content.Context#startService Context.startService()} then the system will * retrieve the service (creating it and calling its {@link #onCreate} method - * if needed) and then call its {@link #onStart} method with the + * if needed) and then call its {@link #onStartCommand} method with the * arguments supplied by the client. The service will at this point continue * running until {@link android.content.Context#stopService Context.stopService()} or * {@link #stopSelf()} is called. Note that multiple calls to * Context.startService() do not nest (though they do result in multiple corresponding - * calls to onStart()), so no matter how many times it is started a service - * will be stopped once Context.stopService() or stopSelf() is called. + * calls to onStartCommand()), so no matter how many times it is started a service + * will be stopped once Context.stopService() or stopSelf() is called; however, + * services can use their {@link #stopSelf(int)} method to ensure the service is + * not stopped until started intents have been processed. + * + *

    For started services, there are two additional major modes of operation + * they can decide to run in, depending on the value they return from + * onStartCommand(): {@link #START_STICKY} is used for services that are + * explicitly started and stopped as needed, while {@link #START_NOT_STICKY} + * or {@link #START_REDELIVER_INTENT} are used for services that should only + * remain running while processing any commands sent to them. See the linked + * documentation for more detail on the semantics. * *

    Clients can also use {@link android.content.Context#bindService Context.bindService()} to * obtain a persistent connection to a service. This likewise creates the * service if it is not already running (calling {@link #onCreate} while - * doing so), but does not call onStart(). The client will receive the + * doing so), but does not call onStartCommand(). The client will receive the * {@link android.os.IBinder} object that the service returns from its * {@link #onBind} method, allowing the client to then make calls back * to the service. The service will remain running as long as the connection @@ -120,7 +132,7 @@ import java.io.PrintWriter; * *

      *
    • If the service is currently executing code in its - * {@link #onCreate onCreate()}, {@link #onStart onStart()}, + * {@link #onCreate onCreate()}, {@link #onStartCommand onStartCommand()}, * or {@link #onDestroy onDestroy()} methods, then the hosting process will * be a foreground process to ensure this code can execute without * being killed. @@ -133,16 +145,22 @@ import java.io.PrintWriter; * process is never less important than the most important client. That is, * if one of its clients is visible to the user, then the service itself is * considered to be visible. + *

    • A started service can use the {@link #startForeground(int, Notification)} + * API to put the service in a foreground state, where the system considers + * it to be something the user is actively aware of and thus not a candidate + * for killing when low on memory. (It is still theoretically possible for + * the service to be killed under extreme memory pressure from the current + * foreground application, but in practice this should not be a concern.) *

    * *

    Note this means that most of the time your service is running, it may * be killed by the system if it is under heavy memory pressure. If this * happens, the system will later try to restart the service. An important - * consequence of this is that if you implement {@link #onStart onStart()} + * consequence of this is that if you implement {@link #onStartCommand onStartCommand()} * to schedule work to be done asynchronously or in another thread, then you - * may want to write information about that work into persistent storage - * during the onStart() call so that it does not get lost if the service later - * gets killed. + * may want to use {@link #START_FLAG_REDELIVERY} to have the system + * re-deliver an Intent for you so that it does not get lost if your service + * is killed while processing it. * *

    Other application components running in the same process as the service * (such as an {@link android.app.Activity}) can, of course, increase the @@ -167,21 +185,128 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac public void onCreate() { } + /** + * @deprecated Implement {@link #onStartCommand(Intent, int, int)} instead. + */ + @Deprecated + public void onStart(Intent intent, int startId) { + } + + /** + * Bits returned by {@link #onStartCommand} describing how to continue + * the service if it is killed. May be {@link #START_STICKY}, + * {@link #START_NOT_STICKY}, {@link #START_REDELIVER_INTENT}, + * or {@link #START_STICKY_COMPATIBILITY}. + */ + public static final int START_CONTINUATION_MASK = 0xf; + + /** + * Constant to return from {@link #onStartCommand}: compatibility + * version of {@link #START_STICKY} that does not guarantee that + * {@link #onStartCommand} will be called again after being killed. + */ + public static final int START_STICKY_COMPATIBILITY = 0; + + /** + * Constant to return from {@link #onStartCommand}: if this service's + * process is killed while it is started (after returning from + * {@link #onStartCommand}), then leave it in the started state but + * don't retain this delivered intent. Later the system will try to + * re-create the service. Because it is in the started state, it will + * guarantee to call {@link #onStartCommand} after creating the new + * service instance; if there are not any pending start commands to be + * delivered to the service, it will be called with a null intent + * object, so you must take care to check for this. + * + *

    This mode makes sense for things that will be explicitly started + * and stopped to run for arbitrary periods of time, such as a service + * performing background music playback. + */ + public static final int START_STICKY = 1; + + /** + * Constant to return from {@link #onStartCommand}: if this service's + * process is killed while it is started (after returning from + * {@link #onStartCommand}), and there are no new start intents to + * deliver to it, then take the service out of the started state and + * don't recreate until a future explicit call to + * {@link Context#startService Context.startService(Intent)}. The + * service will not receive a {@link #onStartCommand(Intent, int, int)} + * call with a null Intent because it will not be re-started if there + * are no pending Intents to deliver. + * + *

    This mode makes sense for things that want to do some work as a + * result of being started, but can be stopped when under memory pressure + * and will explicit start themselves again later to do more work. An + * example of such a service would be one that polls for data from + * a server: it could schedule an alarm to poll every N minutes by having + * the alarm start its service. When its {@link #onStartCommand} is + * called from the alarm, it schedules a new alarm for N minutes later, + * and spawns a thread to do its networking. If its process is killed + * while doing that check, the service will not be restarted until the + * alarm goes off. + */ + public static final int START_NOT_STICKY = 2; + + /** + * Constant to return from {@link #onStartCommand}: if this service's + * process is killed while it is started (after returning from + * {@link #onStartCommand}), then it will be scheduled for a restart + * and the last delivered Intent re-delivered to it again via + * {@link #onStartCommand}. This Intent will remain scheduled for + * redelivery until the service calls {@link #stopSelf(int)} with the + * start ID provided to {@link #onStartCommand}. The + * service will not receive a {@link #onStartCommand(Intent, int, int)} + * call with a null Intent because it will will only be re-started if + * it is not finished processing all Intents sent to it (and any such + * pending events will be delivered at the point of restart). + */ + public static final int START_REDELIVER_INTENT = 3; + + /** + * This flag is set in {@link #onStartCommand} if the Intent is a + * re-delivery of a previously delivered intent, because the service + * had previously returned {@link #START_REDELIVER_INTENT} but had been + * killed before calling {@link #stopSelf(int)} for that Intent. + */ + public static final int START_FLAG_REDELIVERY = 0x0001; + + /** + * This flag is set in {@link #onStartCommand} if the Intent is a + * a retry because the original attempt never got to or returned from + * {@link #onStartCommand(Intent, int, int)}. + */ + public static final int START_FLAG_RETRY = 0x0002; + /** * Called by the system every time a client explicitly starts the service by calling * {@link android.content.Context#startService}, providing the arguments it supplied and a * unique integer token representing the start request. Do not call this method directly. - * + * + *

    For backwards compatibility, the default implementation calls + * {@link #onStart} and returns either {@link #START_STICKY} + * or {@link #START_STICKY_COMPATIBILITY}. + * * @param intent The Intent supplied to {@link android.content.Context#startService}, - * as given. + * as given. This may be null if the service is being restarted after + * its process has gone away, and it had previously returned anything + * except {@link #START_STICKY_COMPATIBILITY}. + * @param flags Additional data about this start request. Currently either + * 0, {@link #START_FLAG_REDELIVERY}, or {@link #START_FLAG_RETRY}. * @param startId A unique integer representing this specific request to - * start. Use with {@link #stopSelfResult(int)}. + * start. Use with {@link #stopSelfResult(int)}. + * + * @return The return value indicates what semantics the system should + * use for the service's current started state. It may be one of the + * constants associated with the {@link #START_CONTINUATION_MASK} bits. * * @see #stopSelfResult(int) */ - public void onStart(Intent intent, int startId) { + public int onStartCommand(Intent intent, int flags, int startId) { + onStart(intent, startId); + return mStartCompatibility ? START_STICKY_COMPATIBILITY : START_STICKY; } - + /** * Called by the system to notify a Service that it is no longer used and is being removed. The * service should clean up an resources it holds (threads, registered @@ -284,6 +409,13 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * safely avoid stopping if there is a start request from a client that you * haven't yet seen in {@link #onStart}. * + *

    Be careful about ordering of your calls to this function.. + * If you call this function with the most-recently received ID before + * you have called it for previously received IDs, the service will be + * immediately stopped anyway. If you may end up processing IDs out + * of order (such as by dispatching them on separate threads), then you + * are responsible for stopping them in the same order you received them.

    + * * @param startId The most recent start identifier received in {@link * #onStart}. * @return Returns true if the startId matches the last start request @@ -304,24 +436,61 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } /** - * Control whether this service is considered to be a foreground service. + * @deprecated This is a now a no-op, use + * {@link #startForeground(int, Notification)} instead. This method + * has been turned into a no-op rather than simply being deprecated + * because analysis of numerous poorly behaving devices has shown that + * increasingly often the trouble is being caused in part by applications + * that are abusing it. Thus, given a choice between introducing + * problems in existing applications using this API (by allowing them to + * be killed when they would like to avoid it), vs allowing the performance + * of the entire system to be decreased, this method was deemed less + * important. + */ + @Deprecated + public final void setForeground(boolean isForeground) { + Log.w(TAG, "setForeground: ignoring old API call on " + getClass().getName()); + } + + /** + * Make this service run in the foreground, supplying the ongoing + * notification to be shown to the user while in this state. * By default services are background, meaning that if the system needs to * kill them to reclaim more memory (such as to display a large page in a * web browser), they can be killed without too much harm. You can set this - * flag if killing your service would be disruptive to the user: such as + * flag if killing your service would be disruptive to the user, such as * if your service is performing background music playback, so the user * would notice if their music stopped playing. * - * @param isForeground Determines whether this service is considered to - * be foreground (true) or background (false). + * @param id The identifier for this notification as per + * {@link NotificationManager#notify(int, Notification) + * NotificationManager.notify(int, Notification)}. + * @param notification The Notification to be displayed. + * + * @see #stopForeground(boolean) */ - public final void setForeground(boolean isForeground) { - if (mActivityManager == null) { - return; + public final void startForeground(int id, Notification notification) { + try { + mActivityManager.setServiceForeground( + new ComponentName(this, mClassName), mToken, id, + notification, true); + } catch (RemoteException ex) { } + } + + /** + * Remove this service from foreground state, allowing it to be killed if + * more memory is needed. + * @param removeNotification If true, the notification previously provided + * to {@link #startForeground} will be removed. Otherwise it will remain + * until a later call removes it (or the service is destroyed). + * @see #startForeground(int, Notification) + */ + public final void stopForeground(boolean removeNotification) { try { mActivityManager.setServiceForeground( - new ComponentName(this, mClassName), mToken, isForeground); + new ComponentName(this, mClassName), mToken, 0, null, + removeNotification); } catch (RemoteException ex) { } } @@ -363,6 +532,8 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac mToken = token; mApplication = application; mActivityManager = (IActivityManager)activityManager; + mStartCompatibility = getApplicationInfo().targetSdkVersion + < Build.VERSION_CODES.ECLAIR; } final String getClassName() { @@ -375,4 +546,5 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac private IBinder mToken = null; private Application mApplication = null; private IActivityManager mActivityManager = null; + private boolean mStartCompatibility = false; } diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 90f8c503607b31c84e12907ab6eae8b9087bb69f..12be97cb690b1000928f0a444380727a7c2cb2a0 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -20,6 +20,7 @@ import android.app.SearchManager.DialogCursorProtocol; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.ContentResolver.OpenResourceIdResult; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -27,7 +28,6 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.DrawableContainer; import android.graphics.drawable.StateListDrawable; import android.net.Uri; import android.os.Bundle; @@ -36,12 +36,13 @@ import android.text.Html; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; +import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.Filter; import android.widget.ImageView; import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import android.widget.Filter; import java.io.FileNotFoundException; import java.io.IOException; @@ -57,6 +58,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private static final boolean DBG = false; private static final String LOG_TAG = "SuggestionsAdapter"; + private static final int QUERY_LIMIT = 50; private SearchManager mSearchManager; private SearchDialog mSearchDialog; @@ -185,7 +187,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); } try { - final Cursor cursor = mSearchManager.getSuggestions(mSearchable, query); + final Cursor cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT); // trigger fill window so the spinner stays up until the results are copied over and // closer to being ready if (!mGlobalSearchMode && cursor != null) cursor.getCount(); @@ -288,18 +290,39 @@ class SuggestionsAdapter extends ResourceCursorAdapter { * @param cursor The cursor * @param position The position that was clicked. */ - void callCursorOnClick(Cursor cursor, int position) { + void callCursorOnClick(Cursor cursor, int position, int actionKey, String actionMsg) { if (!mGlobalSearchMode) return; - final Bundle request = new Bundle(1); + final Bundle request = new Bundle(5); request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.CLICK); request.putInt(DialogCursorProtocol.CLICK_SEND_POSITION, position); request.putInt(DialogCursorProtocol.CLICK_SEND_MAX_DISPLAY_POS, mMaxDisplayed); + if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { + request.putInt(DialogCursorProtocol.CLICK_SEND_ACTION_KEY, actionKey); + request.putString(DialogCursorProtocol.CLICK_SEND_ACTION_MSG, actionMsg); + } final Bundle response = cursor.respond(request); mMaxDisplayed = -1; mListItemToSelect = response.getInt( DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, SuggestionsAdapter.NONE); } + /** + * Tell the cursor that a search was started without using a suggestion. + * + * @param query The search query. + */ + void reportSearch(String query) { + if (!mGlobalSearchMode) return; + Cursor cursor = getCursor(); + if (cursor == null) return; + final Bundle request = new Bundle(3); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.SEARCH); + request.putString(DialogCursorProtocol.SEARCH_SEND_QUERY, query); + request.putInt(DialogCursorProtocol.SEARCH_SEND_MAX_DISPLAY_POS, mMaxDisplayed); + // the response is always empty + cursor.respond(request); + } + /** * Tags the view with cached child view look-ups. */ @@ -378,7 +401,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { Drawable.ConstantState cachedBg = mBackgroundsCache.get(backgroundColor); if (cachedBg != null) { if (DBG) Log.d(LOG_TAG, "Background cache hit for color " + backgroundColor); - return cachedBg.newDrawable(); + return cachedBg.newDrawable(mProviderContext.getResources()); } if (DBG) Log.d(LOG_TAG, "Creating new background for color " + backgroundColor); ColorDrawable transparent = new ColorDrawable(0); @@ -550,54 +573,91 @@ class SuggestionsAdapter extends ResourceCursorAdapter { if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) { return null; } - - // First, check the cache. - Drawable.ConstantState cached = mOutsideDrawablesCache.get(drawableId); - if (cached != null) { - if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + drawableId); - return cached.newDrawable(); - } - - Drawable drawable = null; try { - // Not cached, try using it as a plain resource ID in the provider's context. + // First, see if it's just an integer int resourceId = Integer.parseInt(drawableId); + // It's an int, look for it in the cache + String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + mProviderContext.getPackageName() + "/" + resourceId; + // Must use URI as cache key, since ints are app-specific + Drawable drawable = checkIconCache(drawableUri); + if (drawable != null) { + return drawable; + } + // Not cached, find it by resource ID drawable = mProviderContext.getResources().getDrawable(resourceId); - if (DBG) Log.d(LOG_TAG, "Found icon by resource ID: " + drawableId); + // Stick it in the cache, using the URI as key + storeInIconCache(drawableUri, drawable); + return drawable; } catch (NumberFormatException nfe) { - // The id was not an integer resource id. - // Let the ContentResolver handle content, android.resource and file URIs. - try { - Uri uri = Uri.parse(drawableId); + // It's not an integer, use it as a URI + Drawable drawable = checkIconCache(drawableId); + if (drawable != null) { + return drawable; + } + Uri uri = Uri.parse(drawableId); + drawable = getDrawable(uri); + storeInIconCache(drawableId, drawable); + return drawable; + } catch (Resources.NotFoundException nfe) { + // It was an integer, but it couldn't be found, bail out + Log.w(LOG_TAG, "Icon resource not found: " + drawableId); + return null; + } + } + + /** + * Gets a drawable by URI, without using the cache. + * + * @return A drawable, or {@code null} if the drawable could not be loaded. + */ + private Drawable getDrawable(Uri uri) { + try { + String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + // Load drawables through Resources, to get the source density information + OpenResourceIdResult r = + mProviderContext.getContentResolver().getResourceId(uri); + try { + return r.r.getDrawable(r.id); + } catch (Resources.NotFoundException ex) { + throw new FileNotFoundException("Resource does not exist: " + uri); + } + } else { + // Let the ContentResolver handle content and file URIs. InputStream stream = mProviderContext.getContentResolver().openInputStream(uri); - if (stream != null) { + if (stream == null) { + throw new FileNotFoundException("Failed to open " + uri); + } + try { + return Drawable.createFromStream(stream, null); + } finally { try { - drawable = Drawable.createFromStream(stream, null); - } finally { - try { - stream.close(); - } catch (IOException ex) { - Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); - } + stream.close(); + } catch (IOException ex) { + Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); } } - if (DBG) Log.d(LOG_TAG, "Opened icon input stream: " + drawableId); - } catch (FileNotFoundException fnfe) { - if (DBG) Log.d(LOG_TAG, "Icon stream not found: " + drawableId); - // drawable = null; } + } catch (FileNotFoundException fnfe) { + Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); + return null; + } + } - // If we got a drawable for this resource id, then stick it in the - // map so we don't do this lookup again. - if (drawable != null) { - mOutsideDrawablesCache.put(drawableId, drawable.getConstantState()); - } - } catch (Resources.NotFoundException nfe) { - if (DBG) Log.d(LOG_TAG, "Icon resource not found: " + drawableId); - // drawable = null; + private Drawable checkIconCache(String resourceUri) { + Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri); + if (cached == null) { + return null; } + if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri); + return cached.newDrawable(); + } - return drawable; + private void storeInIconCache(String resourceUri, Drawable drawable) { + if (drawable != null) { + mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState()); + } } /** @@ -646,7 +706,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { // Using containsKey() since we also store null values. if (mOutsideDrawablesCache.containsKey(componentIconKey)) { Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey); - return cached == null ? null : cached.newDrawable(); + return cached == null ? null : cached.newDrawable(mProviderContext.getResources()); } // Then try the activity or application icon Drawable drawable = getActivityIcon(component); diff --git a/core/java/android/app/WallpaperInfo.aidl b/core/java/android/app/WallpaperInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..7bbdcae71c56303a2c1b527d0d836de6b1f165e2 --- /dev/null +++ b/core/java/android/app/WallpaperInfo.aidl @@ -0,0 +1,19 @@ +/* +** 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 android.app; + +parcelable WallpaperInfo; diff --git a/core/java/android/app/WallpaperInfo.java b/core/java/android/app/WallpaperInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..34d3133bdf05e89eb15e2dafdec22040d2decbec --- /dev/null +++ b/core/java/android/app/WallpaperInfo.java @@ -0,0 +1,277 @@ +package android.app; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources.NotFoundException; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.wallpaper.WallpaperService; +import android.util.AttributeSet; +import android.util.Printer; +import android.util.Xml; + +import java.io.IOException; + +/** + * This class is used to specify meta information of a wallpaper service. + */ +public final class WallpaperInfo implements Parcelable { + static final String TAG = "WallpaperInfo"; + + /** + * The Service that implements this wallpaper component. + */ + final ResolveInfo mService; + + /** + * The wallpaper setting activity's name, to + * launch the setting activity of this wallpaper. + */ + final String mSettingsActivityName; + + /** + * Resource identifier for this wallpaper's thumbnail image. + */ + final int mThumbnailResource; + + /** + * Resource identifier for a string indicating the author of the wallpaper. + */ + final int mAuthorResource; + + /** + * Resource identifier for a string containing a short description of the wallpaper. + */ + final int mDescriptionResource; + + /** + * Constructor. + * + * @param context The Context in which we are parsing the wallpaper. + * @param service The ResolveInfo returned from the package manager about + * this wallpaper's component. + */ + public WallpaperInfo(Context context, ResolveInfo service) + throws XmlPullParserException, IOException { + mService = service; + ServiceInfo si = service.serviceInfo; + + PackageManager pm = context.getPackageManager(); + String settingsActivityComponent = null; + int thumbnailRes = -1; + int authorRes = -1; + int descriptionRes = -1; + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, WallpaperService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + + WallpaperService.SERVICE_META_DATA + " meta-data"); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!"wallpaper".equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with wallpaper tag"); + } + + TypedArray sa = context.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.Wallpaper); + settingsActivityComponent = sa.getString( + com.android.internal.R.styleable.Wallpaper_settingsActivity); + + thumbnailRes = sa.getResourceId( + com.android.internal.R.styleable.Wallpaper_thumbnail, + -1); + authorRes = sa.getResourceId( + com.android.internal.R.styleable.Wallpaper_wallpaperAuthor, + -1); + descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.Wallpaper_wallpaperDescription, + -1); + + sa.recycle(); + } finally { + if (parser != null) parser.close(); + } + + mSettingsActivityName = settingsActivityComponent; + mThumbnailResource = thumbnailRes; + mAuthorResource = authorRes; + mDescriptionResource = descriptionRes; + } + + WallpaperInfo(Parcel source) { + mSettingsActivityName = source.readString(); + mThumbnailResource = source.readInt(); + mAuthorResource = source.readInt(); + mDescriptionResource = source.readInt(); + mService = ResolveInfo.CREATOR.createFromParcel(source); + } + + /** + * Return the .apk package that implements this wallpaper. + */ + public String getPackageName() { + return mService.serviceInfo.packageName; + } + + /** + * Return the class name of the service component that implements + * this wallpaper. + */ + public String getServiceName() { + return mService.serviceInfo.name; + } + + /** + * Return the raw information about the Service implementing this + * wallpaper. Do not modify the returned object. + */ + public ServiceInfo getServiceInfo() { + return mService.serviceInfo; + } + + /** + * Return the component of the service that implements this wallpaper. + */ + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, + mService.serviceInfo.name); + } + + /** + * Load the user-displayed label for this wallpaper. + * + * @param pm Supply a PackageManager used to load the wallpaper's + * resources. + */ + public CharSequence loadLabel(PackageManager pm) { + return mService.loadLabel(pm); + } + + /** + * Load the user-displayed icon for this wallpaper. + * + * @param pm Supply a PackageManager used to load the wallpaper's + * resources. + */ + public Drawable loadIcon(PackageManager pm) { + return mService.loadIcon(pm); + } + + /** + * Load the thumbnail image for this wallpaper. + * + * @param pm Supply a PackageManager used to load the wallpaper's + * resources. + */ + public Drawable loadThumbnail(PackageManager pm) { + if (mThumbnailResource < 0) return null; + + return pm.getDrawable(mService.serviceInfo.packageName, + mThumbnailResource, + null); + } + + /** + * Return a string indicating the author(s) of this wallpaper. + */ + public CharSequence loadAuthor(PackageManager pm) throws NotFoundException { + if (mAuthorResource <= 0) throw new NotFoundException(); + return pm.getText( + (mService.resolvePackageName != null) + ? mService.resolvePackageName + : getPackageName(), + mAuthorResource, + null); + } + + /** + * Return a brief summary of this wallpaper's behavior. + */ + public CharSequence loadDescription(PackageManager pm) throws NotFoundException { + if (mDescriptionResource <= 0) throw new NotFoundException(); + return pm.getText( + (mService.resolvePackageName != null) + ? mService.resolvePackageName + : getPackageName(), + mDescriptionResource, + null); + } + + /** + * Return the class name of an activity that provides a settings UI for + * the wallpaper. You can launch this activity be starting it with + * an {@link android.content.Intent} whose action is MAIN and with an + * explicit {@link android.content.ComponentName} + * composed of {@link #getPackageName} and the class name returned here. + * + *

    A null will be returned if there is no settings activity associated + * with the wallpaper. + */ + public String getSettingsActivity() { + return mSettingsActivityName; + } + + public void dump(Printer pw, String prefix) { + pw.println(prefix + "Service:"); + mService.dump(pw, prefix + " "); + pw.println(prefix + "mSettingsActivityName=" + mSettingsActivityName); + } + + @Override + public String toString() { + return "WallpaperInfo{" + mService.serviceInfo.name + + ", settings: " + + mSettingsActivityName + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mSettingsActivityName); + dest.writeInt(mThumbnailResource); + dest.writeInt(mAuthorResource); + dest.writeInt(mDescriptionResource); + mService.writeToParcel(dest, flags); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public WallpaperInfo createFromParcel(Parcel source) { + return new WallpaperInfo(source); + } + + public WallpaperInfo[] newArray(int size) { + return new WallpaperInfo[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e98b286db6d470da5da8cca470332bb55f3d3f8f --- /dev/null +++ b/core/java/android/app/WallpaperManager.java @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ViewRoot; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Provides access to the system wallpaper. With WallpaperManager, you can + * get the current wallpaper, get the desired dimensions for the wallpaper, set + * the wallpaper, and more. Get an instance of WallpaperManager with + * {@link #getInstance(android.content.Context) getInstance()}. + */ +public class WallpaperManager { + private static String TAG = "WallpaperManager"; + private static boolean DEBUG = false; + private float mWallpaperXStep = -1; + private float mWallpaperYStep = -1; + + /** + * Launch an activity for the user to pick the current global live + * wallpaper. + */ + public static final String ACTION_LIVE_WALLPAPER_CHOOSER + = "android.service.wallpaper.LIVE_WALLPAPER_CHOOSER"; + + private final Context mContext; + + /** + * Special drawable that draws a wallpaper as fast as possible. Assumes + * no scaling or placement off (0,0) of the wallpaper (this should be done + * at the time the bitmap is loaded). + */ + static class FastBitmapDrawable extends Drawable { + private final Bitmap mBitmap; + private final int mWidth; + private final int mHeight; + private int mDrawLeft; + private int mDrawTop; + + private FastBitmapDrawable(Bitmap bitmap) { + mBitmap = bitmap; + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + setBounds(0, 0, mWidth, mHeight); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(mBitmap, mDrawLeft, mDrawTop, null); + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + mDrawLeft = left + (right-left - mWidth) / 2; + mDrawTop = top + (bottom-top - mHeight) / 2; + } + + @Override + public void setBounds(Rect bounds) { + // TODO Auto-generated method stub + super.setBounds(bounds); + } + + @Override + public void setAlpha(int alpha) { + throw new UnsupportedOperationException( + "Not supported with this drawable"); + } + + @Override + public void setColorFilter(ColorFilter cf) { + throw new UnsupportedOperationException( + "Not supported with this drawable"); + } + + @Override + public void setDither(boolean dither) { + throw new UnsupportedOperationException( + "Not supported with this drawable"); + } + + @Override + public void setFilterBitmap(boolean filter) { + throw new UnsupportedOperationException( + "Not supported with this drawable"); + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public int getMinimumWidth() { + return mWidth; + } + + @Override + public int getMinimumHeight() { + return mHeight; + } + } + + static class Globals extends IWallpaperManagerCallback.Stub { + private IWallpaperManager mService; + private Bitmap mWallpaper; + private Bitmap mDefaultWallpaper; + + private static final int MSG_CLEAR_WALLPAPER = 1; + + private final Handler mHandler; + + Globals(Looper looper) { + IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); + mService = IWallpaperManager.Stub.asInterface(b); + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_CLEAR_WALLPAPER: + synchronized (this) { + mWallpaper = null; + mDefaultWallpaper = null; + } + break; + } + } + }; + } + + public void onWallpaperChanged() { + /* The wallpaper has changed but we shouldn't eagerly load the + * wallpaper as that would be inefficient. Reset the cached wallpaper + * to null so if the user requests the wallpaper again then we'll + * fetch it. + */ + mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER); + } + + public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { + synchronized (this) { + if (mWallpaper != null) { + return mWallpaper; + } + if (mDefaultWallpaper != null) { + return mDefaultWallpaper; + } + mWallpaper = getCurrentWallpaperLocked(context); + if (mWallpaper == null && returnDefault) { + mDefaultWallpaper = getDefaultWallpaperLocked(context); + return mDefaultWallpaper; + } + return mWallpaper; + } + } + + private Bitmap getCurrentWallpaperLocked(Context context) { + try { + Bundle params = new Bundle(); + ParcelFileDescriptor fd = mService.getWallpaper(this, params); + if (fd != null) { + int width = params.getInt("width", 0); + int height = params.getInt("height", 0); + + if (width <= 0 || height <= 0) { + // Degenerate case: no size requested, just load + // bitmap as-is. + Bitmap bm = BitmapFactory.decodeFileDescriptor( + fd.getFileDescriptor(), null, null); + try { + fd.close(); + } catch (IOException e) { + } + if (bm != null) { + bm.setDensity(DisplayMetrics.DENSITY_DEVICE); + } + return bm; + } + + // Load the bitmap with full color depth, to preserve + // quality for later processing. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bm = BitmapFactory.decodeFileDescriptor( + fd.getFileDescriptor(), null, options); + try { + fd.close(); + } catch (IOException e) { + } + + return generateBitmap(context, bm, width, height); + } + } catch (RemoteException e) { + } + return null; + } + + private Bitmap getDefaultWallpaperLocked(Context context) { + try { + InputStream is = context.getResources().openRawResource( + com.android.internal.R.drawable.default_wallpaper); + if (is != null) { + int width = mService.getWidthHint(); + int height = mService.getHeightHint(); + + if (width <= 0 || height <= 0) { + // Degenerate case: no size requested, just load + // bitmap as-is. + Bitmap bm = BitmapFactory.decodeStream(is, null, null); + try { + is.close(); + } catch (IOException e) { + } + if (bm != null) { + bm.setDensity(DisplayMetrics.DENSITY_DEVICE); + } + return bm; + } + + // Load the bitmap with full color depth, to preserve + // quality for later processing. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap bm = BitmapFactory.decodeStream(is, null, options); + try { + is.close(); + } catch (IOException e) { + } + + return generateBitmap(context, bm, width, height); + } + } catch (RemoteException e) { + } + return null; + } + } + + private static Object mSync = new Object(); + private static Globals sGlobals; + + static void initGlobals(Looper looper) { + synchronized (mSync) { + if (sGlobals == null) { + sGlobals = new Globals(looper); + } + } + } + + /*package*/ WallpaperManager(Context context, Handler handler) { + mContext = context; + initGlobals(context.getMainLooper()); + } + + /** + * Retrieve a WallpaperManager associated with the given Context. + */ + public static WallpaperManager getInstance(Context context) { + return (WallpaperManager)context.getSystemService( + Context.WALLPAPER_SERVICE); + } + + /** @hide */ + public IWallpaperManager getIWallpaperManager() { + return sGlobals.mService; + } + + /** + * Retrieve the current system wallpaper; if + * no wallpaper is set, the system default wallpaper is returned. + * This is returned as an + * abstract Drawable that you can install in a View to display whatever + * wallpaper the user has currently set. + * + * @return Returns a Drawable object that will draw the wallpaper. + */ + public Drawable getDrawable() { + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true); + if (bm != null) { + Drawable dr = new BitmapDrawable(mContext.getResources(), bm); + dr.setDither(false); + return dr; + } + return null; + } + + /** + * Retrieve the current system wallpaper; if there is no wallpaper set, + * a null pointer is returned. This is returned as an + * abstract Drawable that you can install in a View to display whatever + * wallpaper the user has currently set. + * + * @return Returns a Drawable object that will draw the wallpaper or a + * null pointer if these is none. + */ + public Drawable peekDrawable() { + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false); + if (bm != null) { + Drawable dr = new BitmapDrawable(mContext.getResources(), bm); + dr.setDither(false); + return dr; + } + return null; + } + + /** + * Like {@link #getDrawable()}, but the returned Drawable has a number + * of limitations to reduce its overhead as much as possible. It will + * never scale the wallpaper (only centering it if the requested bounds + * do match the bitmap bounds, which should not be typical), doesn't + * allow setting an alpha, color filter, or other attributes, etc. The + * bounds of the returned drawable will be initialized to the same bounds + * as the wallpaper, so normally you will not need to touch it. The + * drawable also assumes that it will be used in a context running in + * the same density as the screen (not in density compatibility mode). + * + * @return Returns a Drawable object that will draw the wallpaper. + */ + public Drawable getFastDrawable() { + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true); + if (bm != null) { + Drawable dr = new FastBitmapDrawable(bm); + return dr; + } + return null; + } + + /** + * Like {@link #getFastDrawable()}, but if there is no wallpaper set, + * a null pointer is returned. + * + * @return Returns an optimized Drawable object that will draw the + * wallpaper or a null pointer if these is none. + */ + public Drawable peekFastDrawable() { + Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, false); + if (bm != null) { + Drawable dr = new FastBitmapDrawable(bm); + return dr; + } + return null; + } + + /** + * If the current wallpaper is a live wallpaper component, return the + * information about that wallpaper. Otherwise, if it is a static image, + * simply return null. + */ + public WallpaperInfo getWallpaperInfo() { + try { + return sGlobals.mService.getWallpaperInfo(); + } catch (RemoteException e) { + return null; + } + } + + /** + * Change the current system wallpaper to the bitmap in the given resource. + * The resource is opened as a raw data stream and copied into the + * wallpaper; it must be a valid PNG or JPEG image. On success, the intent + * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast. + * + * @param resid The bitmap to save. + * + * @throws IOException If an error occurs reverting to the default + * wallpaper. + */ + public void setResource(int resid) throws IOException { + try { + Resources resources = mContext.getResources(); + /* Set the wallpaper to the default values */ + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper( + "res:" + resources.getResourceName(resid)); + if (fd != null) { + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + setWallpaper(resources.openRawResource(resid), fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } + } catch (RemoteException e) { + } + } + + /** + * Change the current system wallpaper to a bitmap. The given bitmap is + * converted to a PNG and stored as the wallpaper. On success, the intent + * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast. + * + * @param bitmap The bitmap to save. + * + * @throws IOException If an error occurs reverting to the default + * wallpaper. + */ + public void setBitmap(Bitmap bitmap) throws IOException { + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + } + } + + /** + * Change the current system wallpaper to a specific byte stream. The + * give InputStream is copied into persistent storage and will now be + * used as the wallpaper. Currently it must be either a JPEG or PNG + * image. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} + * is broadcast. + * + * @param data A stream containing the raw data to install as a wallpaper. + * + * @throws IOException If an error occurs reverting to the default + * wallpaper. + */ + public void setStream(InputStream data) throws IOException { + try { + ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + setWallpaper(data, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + } + } + + private void setWallpaper(InputStream data, FileOutputStream fos) + throws IOException { + byte[] buffer = new byte[32768]; + int amt; + while ((amt=data.read(buffer)) > 0) { + fos.write(buffer, 0, amt); + } + } + + /** + * Returns the desired minimum width for the wallpaper. Callers of + * {@link #setBitmap(android.graphics.Bitmap)} or + * {@link #setStream(java.io.InputStream)} should check this value + * beforehand to make sure the supplied wallpaper respects the desired + * minimum width. + * + * If the returned value is <= 0, the caller should use the width of + * the default display instead. + * + * @return The desired minimum width for the wallpaper. This value should + * be honored by applications that set the wallpaper but it is not + * mandatory. + */ + public int getDesiredMinimumWidth() { + try { + return sGlobals.mService.getWidthHint(); + } catch (RemoteException e) { + // Shouldn't happen! + return 0; + } + } + + /** + * Returns the desired minimum height for the wallpaper. Callers of + * {@link #setBitmap(android.graphics.Bitmap)} or + * {@link #setStream(java.io.InputStream)} should check this value + * beforehand to make sure the supplied wallpaper respects the desired + * minimum height. + * + * If the returned value is <= 0, the caller should use the height of + * the default display instead. + * + * @return The desired minimum height for the wallpaper. This value should + * be honored by applications that set the wallpaper but it is not + * mandatory. + */ + public int getDesiredMinimumHeight() { + try { + return sGlobals.mService.getHeightHint(); + } catch (RemoteException e) { + // Shouldn't happen! + return 0; + } + } + + /** + * For use only by the current home application, to specify the size of + * wallpaper it would like to use. This allows such applications to have + * a virtual wallpaper that is larger than the physical screen, matching + * the size of their workspace. + * @param minimumWidth Desired minimum width + * @param minimumHeight Desired minimum height + */ + public void suggestDesiredDimensions(int minimumWidth, int minimumHeight) { + try { + sGlobals.mService.setDimensionHints(minimumWidth, minimumHeight); + } catch (RemoteException e) { + } + } + + /** + * Set the position of the current wallpaper within any larger space, when + * that wallpaper is visible behind the given window. The X and Y offsets + * are floating point numbers ranging from 0 to 1, representing where the + * wallpaper should be positioned within the screen space. These only + * make sense when the wallpaper is larger than the screen. + * + * @param windowToken The window who these offsets should be associated + * with, as returned by {@link android.view.View#getWindowToken() + * View.getWindowToken()}. + * @param xOffset The offset along the X dimension, from 0 to 1. + * @param yOffset The offset along the Y dimension, from 0 to 1. + */ + public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) { + try { + //Log.v(TAG, "Sending new wallpaper offsets from app..."); + ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep); + //Log.v(TAG, "...app returning after sending offsets!"); + } catch (RemoteException e) { + // Ignore. + } + } + + /** + * For applications that use multiple virtual screens showing a wallpaper, + * specify the step size between virtual screens. For example, if the + * launcher has 5 virtual screens, it would specify an xStep of 0.5, + * since the X offset for those screens are 0.0, 0.5 and 1.0 + * @param xStep The X offset delta from one screen to the next one + * @param yStep The Y offset delta from one screen to the next one + */ + public void setWallpaperOffsetSteps(float xStep, float yStep) { + mWallpaperXStep = xStep; + mWallpaperYStep = yStep; + } + + /** + * Send an arbitrary command to the current active wallpaper. + * + * @param windowToken The window who these offsets should be associated + * with, as returned by {@link android.view.View#getWindowToken() + * View.getWindowToken()}. + * @param action Name of the command to perform. This must be a scoped + * name to avoid collisions, such as "com.mycompany.wallpaper.DOIT". + * @param x Arbitrary integer argument based on command. + * @param y Arbitrary integer argument based on command. + * @param z Arbitrary integer argument based on command. + * @param extras Optional additional information for the command, or null. + */ + public void sendWallpaperCommand(IBinder windowToken, String action, + int x, int y, int z, Bundle extras) { + try { + //Log.v(TAG, "Sending new wallpaper offsets from app..."); + ViewRoot.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand( + windowToken, action, x, y, z, extras, false); + //Log.v(TAG, "...app returning after sending offsets!"); + } catch (RemoteException e) { + // Ignore. + } + } + + /** + * Clear the offsets previously associated with this window through + * {@link #setWallpaperOffsets(IBinder, float, float)}. This reverts + * the window to its default state, where it does not cause the wallpaper + * to scroll from whatever its last offsets were. + * + * @param windowToken The window who these offsets should be associated + * with, as returned by {@link android.view.View#getWindowToken() + * View.getWindowToken()}. + */ + public void clearWallpaperOffsets(IBinder windowToken) { + try { + ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + windowToken, -1, -1, -1, -1); + } catch (RemoteException e) { + // Ignore. + } + } + + /** + * Remove any currently set wallpaper, reverting to the system's default + * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} + * is broadcast. + * + * @throws IOException If an error occurs reverting to the default + * wallpaper. + */ + public void clear() throws IOException { + setResource(com.android.internal.R.drawable.default_wallpaper); + } + + static Bitmap generateBitmap(Context context, Bitmap bm, int width, int height) { + if (bm == null) { + return bm; + } + bm.setDensity(DisplayMetrics.DENSITY_DEVICE); + + // This is the final bitmap we want to return. + // XXX We should get the pixel depth from the system (to match the + // physical display depth), when there is a way. + Bitmap newbm = Bitmap.createBitmap(width, height, + Bitmap.Config.RGB_565); + newbm.setDensity(DisplayMetrics.DENSITY_DEVICE); + Canvas c = new Canvas(newbm); + c.setDensity(DisplayMetrics.DENSITY_DEVICE); + Rect targetRect = new Rect(); + targetRect.left = targetRect.top = 0; + targetRect.right = bm.getWidth(); + targetRect.bottom = bm.getHeight(); + + int deltaw = width - targetRect.right; + int deltah = height - targetRect.bottom; + + if (deltaw > 0 || deltah > 0) { + // We need to scale up so it covers the entire + // area. + float scale = 1.0f; + if (deltaw > deltah) { + scale = width / (float)targetRect.right; + } else { + scale = height / (float)targetRect.bottom; + } + targetRect.right = (int)(targetRect.right*scale); + targetRect.bottom = (int)(targetRect.bottom*scale); + deltaw = width - targetRect.right; + deltah = height - targetRect.bottom; + } + + targetRect.offset(deltaw/2, deltah/2); + Paint paint = new Paint(); + paint.setFilterBitmap(true); + paint.setDither(true); + c.drawBitmap(bm, null, targetRect, paint); + + bm.recycle(); + return newbm; + } +} diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 9799ac44059aae5e7372e9b68c4096739a400bca..2f719f3c5bd7e5070cda40da4384c067491966c6 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -24,16 +24,17 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.SystemClock; +import android.os.Parcelable; +import android.os.Parcel; import android.util.AttributeSet; import android.util.Log; +import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; -import android.widget.FrameLayout.LayoutParams; /** * Provides the glue to show AppWidget views. This class offers automatic animation @@ -108,6 +109,32 @@ public class AppWidgetHostView extends FrameLayout { return mInfo; } + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + final ParcelableSparseArray jail = new ParcelableSparseArray(); + super.dispatchSaveInstanceState(jail); + container.put(generateId(), jail); + } + + private int generateId() { + final int id = getId(); + return id == View.NO_ID ? mAppWidgetId : id; + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + final Parcelable parcelable = container.get(generateId()); + + ParcelableSparseArray jail = null; + if (parcelable != null && parcelable instanceof ParcelableSparseArray) { + jail = (ParcelableSparseArray) parcelable; + } + + if (jail == null) jail = new ParcelableSparseArray(); + + super.dispatchRestoreInstanceState(jail); + } + /** {@inheritDoc} */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { @@ -120,7 +147,7 @@ public class AppWidgetHostView extends FrameLayout { /** * Process a set of {@link RemoteViews} coming in as an update from the - * AppWidget provider. Will animate into these new views as needed. + * AppWidget provider. Will animate into these new views as needed */ public void updateAppWidget(RemoteViews remoteViews) { if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); @@ -303,6 +330,7 @@ public class AppWidgetHostView extends FrameLayout { if (mInfo != null) { Context theirContext = mContext.createPackageContext( mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); + mRemoteContext = theirContext; LayoutInflater inflater = (LayoutInflater) theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater = inflater.cloneInContext(theirContext); @@ -317,8 +345,8 @@ public class AppWidgetHostView extends FrameLayout { exception = e; } - if (exception != null && LOGD) { - Log.w(TAG, "Error inflating AppWidget " + mInfo, exception); + if (exception != null) { + Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString()); } if (defaultView == null) { @@ -339,4 +367,36 @@ public class AppWidgetHostView extends FrameLayout { tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); return tv; } + + private static class ParcelableSparseArray extends SparseArray implements Parcelable { + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + final int count = size(); + dest.writeInt(count); + for (int i = 0; i < count; i++) { + dest.writeInt(keyAt(i)); + dest.writeParcelable(valueAt(i), 0); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ParcelableSparseArray createFromParcel(Parcel source) { + final ParcelableSparseArray array = new ParcelableSparseArray(); + final ClassLoader loader = array.getClass().getClassLoader(); + final int count = source.readInt(); + for (int i = 0; i < count; i++) { + array.put(source.readInt(), source.readParcelable(loader)); + } + return array; + } + + public ParcelableSparseArray[] newArray(int size) { + return new ParcelableSparseArray[size]; + } + }; + } } diff --git a/core/java/android/backup/AbsoluteFileBackupHelper.java b/core/java/android/backup/AbsoluteFileBackupHelper.java index ab246754b5afa8fd350b4fc745a3feb28a73ec70..1dbccc9c47af802daff4e4df6285b420e0d2be01 100644 --- a/core/java/android/backup/AbsoluteFileBackupHelper.java +++ b/core/java/android/backup/AbsoluteFileBackupHelper.java @@ -31,6 +31,7 @@ import java.io.FileDescriptor; */ public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "AbsoluteFileBackupHelper"; + private static final boolean DEBUG = false; Context mContext; String[] mFiles; @@ -54,8 +55,7 @@ public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements Ba } public void restoreEntity(BackupDataInputStream data) { - // TODO: turn this off before ship - Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); + if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); String key = data.getKey(); if (isKeyInList(key, mFiles)) { File f = new File(key); diff --git a/core/java/android/backup/BackupDataInput.java b/core/java/android/backup/BackupDataInput.java index 69c206ce1c8155092d080133af95996260c9afdc..e67b0bee76773b76caa742953a51d9989a0b50f0 100644 --- a/core/java/android/backup/BackupDataInput.java +++ b/core/java/android/backup/BackupDataInput.java @@ -97,12 +97,7 @@ public class BackupDataInput { public void skipEntityData() throws IOException { if (mHeaderReady) { - int result = skipEntityData_native(mBackupReader); - if (result >= 0) { - return; - } else { - throw new IOException("result=0x" + Integer.toHexString(result)); - } + skipEntityData_native(mBackupReader); } else { throw new IllegalStateException("mHeaderReady=false"); } diff --git a/core/java/android/backup/BackupManager.java b/core/java/android/backup/BackupManager.java index c52fcd2d964ab514583a1bec3da3daa8b51466c3..da1647a5abcb589d13468f8a50fdfd5cabe306f5 100644 --- a/core/java/android/backup/BackupManager.java +++ b/core/java/android/backup/BackupManager.java @@ -43,7 +43,7 @@ public class BackupManager { private static final String TAG = "BackupManager"; /** @hide TODO: REMOVE THIS */ - public static final boolean EVEN_THINK_ABOUT_DOING_RESTORE = false; + public static final boolean EVEN_THINK_ABOUT_DOING_RESTORE = true; private Context mContext; private static IBackupManager sService; diff --git a/core/java/android/backup/FileBackupHelper.java b/core/java/android/backup/FileBackupHelper.java index 405849705ff769b14619786213df945bde9395cf..dacfc8f557dc9ac57aab288cf8f5a71962cfb151 100644 --- a/core/java/android/backup/FileBackupHelper.java +++ b/core/java/android/backup/FileBackupHelper.java @@ -26,6 +26,7 @@ import java.io.FileDescriptor; /** @hide */ public class FileBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "FileBackupHelper"; + private static final boolean DEBUG = false; Context mContext; File mFilesDir; @@ -60,8 +61,7 @@ public class FileBackupHelper extends FileBackupHelperBase implements BackupHelp } public void restoreEntity(BackupDataInputStream data) { - // TODO: turn this off before ship - Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); + if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); String key = data.getKey(); if (isKeyInList(key, mFiles)) { File f = new File(mFilesDir, key); diff --git a/core/java/android/backup/IRestoreSession.aidl b/core/java/android/backup/IRestoreSession.aidl index 2a1fbc179933c3114dfb22cd50489a23964a58b7..fd40d98216ab535ea811dc1d1c756d640daf40d5 100644 --- a/core/java/android/backup/IRestoreSession.aidl +++ b/core/java/android/backup/IRestoreSession.aidl @@ -40,6 +40,8 @@ interface IRestoreSession { * Restore the given set onto the device, replacing the current data of any app * contained in the restore set with the data previously backed up. * + * @return Zero on success; nonzero on error. The observer will only receive + * progress callbacks if this method returned zero. * @param token The token from {@link getAvailableRestoreSets()} corresponding to * the restore set that should be used. * @param observer If non-null, this binder points to an object that will receive @@ -50,6 +52,9 @@ interface IRestoreSession { /** * End this restore session. After this method is called, the IRestoreSession binder * is no longer valid. + * + *

    Note: The caller must invoke this method to end the restore session, + * even if {@link getAvailableRestoreSets} or {@link performRestore} failed. */ void endRestoreSession(); } diff --git a/core/java/android/backup/SharedPreferencesBackupHelper.java b/core/java/android/backup/SharedPreferencesBackupHelper.java index 4a7b399a5bde0aeeedffac378472f0c343e8013a..6a0bc965340923771fb2719b69a4d00cf3851d26 100644 --- a/core/java/android/backup/SharedPreferencesBackupHelper.java +++ b/core/java/android/backup/SharedPreferencesBackupHelper.java @@ -26,6 +26,7 @@ import java.io.FileDescriptor; /** @hide */ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "SharedPreferencesBackupHelper"; + private static final boolean DEBUG = false; private Context mContext; private String[] mPrefGroups; @@ -56,9 +57,9 @@ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implemen public void restoreEntity(BackupDataInputStream data) { Context context = mContext; - // TODO: turn this off before ship - Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); String key = data.getKey(); + if (DEBUG) Log.d(TAG, "got entity '" + key + "' size=" + data.size()); + if (isKeyInList(key, mPrefGroups)) { File f = context.getSharedPrefsFile(key).getAbsoluteFile(); writeFile(f, data); diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index 2ea45d567f6add5cb7d037756ebf8dc4d31d00eb..e8a69d8c1f766238475d51eca3478c4bee86dee7 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -25,7 +25,10 @@ import android.os.RemoteException; import android.os.IBinder; import android.util.Log; -import java.util.List; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.HashSet; /** * Public API for controlling the Bluetooth A2DP Profile Service. @@ -39,32 +42,30 @@ import java.util.List; * * Currently the BluetoothA2dp service runs in the system server and this * proxy object will be immediately bound to the service on construction. - * However this may change in future releases, and error codes such as - * BluetoothError.ERROR_IPC_NOT_READY will be returned from this API when the - * proxy object is not yet attached. * * Currently this class provides methods to connect to A2DP audio sinks. * * @hide */ -public class BluetoothA2dp { +public final class BluetoothA2dp { private static final String TAG = "BluetoothA2dp"; private static final boolean DBG = false; - /** int extra for SINK_STATE_CHANGED_ACTION */ - public static final String SINK_STATE = - "android.bluetooth.a2dp.intent.SINK_STATE"; - /** int extra for SINK_STATE_CHANGED_ACTION */ - public static final String SINK_PREVIOUS_STATE = - "android.bluetooth.a2dp.intent.SINK_PREVIOUS_STATE"; + /** int extra for ACTION_SINK_STATE_CHANGED */ + public static final String EXTRA_SINK_STATE = + "android.bluetooth.a2dp.extra.SINK_STATE"; + /** int extra for ACTION_SINK_STATE_CHANGED */ + public static final String EXTRA_PREVIOUS_SINK_STATE = + "android.bluetooth.a2dp.extra.PREVIOUS_SINK_STATE"; /** Indicates the state of an A2DP audio sink has changed. - * This intent will always contain SINK_STATE, SINK_PREVIOUS_STATE and - * BluetoothIntent.ADDRESS extras. + * This intent will always contain EXTRA_SINK_STATE, + * EXTRA_PREVIOUS_SINK_STATE and BluetoothDevice.EXTRA_DEVICE + * extras. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String SINK_STATE_CHANGED_ACTION = - "android.bluetooth.a2dp.intent.action.SINK_STATE_CHANGED"; + public static final String ACTION_SINK_STATE_CHANGED = + "android.bluetooth.a2dp.action.SINK_STATE_CHANGED"; public static final int STATE_DISCONNECTED = 0; public static final int STATE_CONNECTING = 1; @@ -79,6 +80,7 @@ public class BluetoothA2dp { /** Default priority for a2dp devices that should not allow incoming * connections */ public static final int PRIORITY_OFF = 0; + private final IBluetoothA2dp mService; private final Context mContext; @@ -89,84 +91,123 @@ public class BluetoothA2dp { */ public BluetoothA2dp(Context c) { mContext = c; + IBinder b = ServiceManager.getService(BluetoothA2dpService.BLUETOOTH_A2DP_SERVICE); - if (b == null) { - throw new RuntimeException("Bluetooth A2DP service not available!"); + if (b != null) { + mService = IBluetoothA2dp.Stub.asInterface(b); + } else { + Log.w(TAG, "Bluetooth A2DP service not available!"); + + // Instead of throwing an exception which prevents people from going + // into Wireless settings in the emulator. Let it crash later when it is actually used. + mService = null; } - mService = IBluetoothA2dp.Stub.asInterface(b); } /** Initiate a connection to an A2DP sink. * Listen for SINK_STATE_CHANGED_ACTION to find out when the * connection is completed. - * @param address Remote BT address. - * @return Result code, negative indicates an immediate error. + * @param device Remote BT device. + * @return false on immediate error, true otherwise * @hide */ - public int connectSink(String address) { - if (DBG) log("connectSink(" + address + ")"); + public boolean connectSink(BluetoothDevice device) { + if (DBG) log("connectSink(" + device + ")"); try { - return mService.connectSink(address); + return mService.connectSink(device); } catch (RemoteException e) { - Log.w(TAG, "", e); - return BluetoothError.ERROR_IPC; + Log.e(TAG, "", e); + return false; } } /** Initiate disconnect from an A2DP sink. * Listen for SINK_STATE_CHANGED_ACTION to find out when * disconnect is completed. - * @param address Remote BT address. - * @return Result code, negative indicates an immediate error. + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean disconnectSink(BluetoothDevice device) { + if (DBG) log("disconnectSink(" + device + ")"); + try { + return mService.disconnectSink(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Initiate suspend from an A2DP sink. + * Listen for SINK_STATE_CHANGED_ACTION to find out when + * suspend is completed. + * @param device Remote BT device. + * @return false on immediate error, true otherwise + * @hide + */ + public boolean suspendSink(BluetoothDevice device) { + try { + return mService.suspendSink(device); + } catch (RemoteException e) { + Log.e(TAG, "", e); + return false; + } + } + + /** Initiate resume from an suspended A2DP sink. + * Listen for SINK_STATE_CHANGED_ACTION to find out when + * resume is completed. + * @param device Remote BT device. + * @return false on immediate error, true otherwise * @hide */ - public int disconnectSink(String address) { - if (DBG) log("disconnectSink(" + address + ")"); + public boolean resumeSink(BluetoothDevice device) { try { - return mService.disconnectSink(address); + return mService.resumeSink(device); } catch (RemoteException e) { - Log.w(TAG, "", e); - return BluetoothError.ERROR_IPC; + Log.e(TAG, "", e); + return false; } } /** Check if a specified A2DP sink is connected. - * @param address Remote BT address. + * @param device Remote BT device. * @return True if connected (or playing), false otherwise and on error. * @hide */ - public boolean isSinkConnected(String address) { - if (DBG) log("isSinkConnected(" + address + ")"); - int state = getSinkState(address); + public boolean isSinkConnected(BluetoothDevice device) { + if (DBG) log("isSinkConnected(" + device + ")"); + int state = getSinkState(device); return state == STATE_CONNECTED || state == STATE_PLAYING; } /** Check if any A2DP sink is connected. - * @return a List of connected A2DP sinks, or null on error. + * @return a unmodifiable set of connected A2DP sinks, or null on error. * @hide */ - public List listConnectedSinks() { - if (DBG) log("listConnectedSinks()"); + public Set getConnectedSinks() { + if (DBG) log("getConnectedSinks()"); try { - return mService.listConnectedSinks(); + return Collections.unmodifiableSet( + new HashSet(Arrays.asList(mService.getConnectedSinks()))); } catch (RemoteException e) { - Log.w(TAG, "", e); + Log.e(TAG, "", e); return null; } } /** Get the state of an A2DP sink - * @param address Remote BT address. - * @return State code, or negative on error + * @param device Remote BT device. + * @return State code, one of STATE_ * @hide */ - public int getSinkState(String address) { - if (DBG) log("getSinkState(" + address + ")"); + public int getSinkState(BluetoothDevice device) { + if (DBG) log("getSinkState(" + device + ")"); try { - return mService.getSinkState(address); + return mService.getSinkState(device); } catch (RemoteException e) { - Log.w(TAG, "", e); - return BluetoothError.ERROR_IPC; + Log.e(TAG, "", e); + return BluetoothA2dp.STATE_DISCONNECTED; } } @@ -177,58 +218,33 @@ public class BluetoothA2dp { * Sinks with priority greater than zero will accept incoming connections * (if no sink is currently connected). * Priority for unpaired sink must be PRIORITY_NONE. - * @param address Paired sink + * @param device Paired sink * @param priority Integer priority, for example PRIORITY_AUTO or * PRIORITY_NONE - * @return Result code, negative indicates an error + * @return true if priority is set, false on error */ - public int setSinkPriority(String address, int priority) { - if (DBG) log("setSinkPriority(" + address + ", " + priority + ")"); + public boolean setSinkPriority(BluetoothDevice device, int priority) { + if (DBG) log("setSinkPriority(" + device + ", " + priority + ")"); try { - return mService.setSinkPriority(address, priority); + return mService.setSinkPriority(device, priority); } catch (RemoteException e) { - Log.w(TAG, "", e); - return BluetoothError.ERROR_IPC; + Log.e(TAG, "", e); + return false; } } /** * Get priority of a2dp sink. - * @param address Sink + * @param device Sink * @return non-negative priority, or negative error code on error. */ - public int getSinkPriority(String address) { - if (DBG) log("getSinkPriority(" + address + ")"); + public int getSinkPriority(BluetoothDevice device) { + if (DBG) log("getSinkPriority(" + device + ")"); try { - return mService.getSinkPriority(address); + return mService.getSinkPriority(device); } catch (RemoteException e) { - Log.w(TAG, "", e); - return BluetoothError.ERROR_IPC; - } - } - - /** - * Check class bits for possible A2DP Sink support. - * This is a simple heuristic that tries to guess if a device with the - * given class bits might be a A2DP Sink. It is not accurate for all - * devices. It tries to err on the side of false positives. - * @return True if this device might be a A2DP sink - */ - public static boolean doesClassMatchSink(int btClass) { - if (BluetoothClass.Service.hasService(btClass, BluetoothClass.Service.RENDER)) { - return true; - } - // By the A2DP spec, sinks must indicate the RENDER service. - // However we found some that do not (Chordette). So lets also - // match on some other class bits. - switch (BluetoothClass.Device.getDevice(btClass)) { - case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: - case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: - case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER: - case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: - return true; - default: - return false; + Log.e(TAG, "", e); + return PRIORITY_OFF; } } diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..bd5b07cff640e69b0c138e9d571e0274c58815c1 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -0,0 +1,849 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +/** + * Represents the local device Bluetooth adapter. The {@link BluetoothAdapter} + * lets you perform fundamental Bluetooth tasks, such as initiate + * device discovery, query a list of bonded (paired) devices, + * instantiate a {@link BluetoothDevice} using a known MAC address, and create + * a {@link BluetoothServerSocket} to listen for connection requests from other + * devices. + * + *

    To get a {@link BluetoothAdapter} representing the local Bluetooth + * adapter, call the static {@link #getDefaultAdapter} method. + * Fundamentally, this is your starting point for all + * Bluetooth actions. Once you have the local adapter, you can get a set of + * {@link BluetoothDevice} objects representing all paired devices with + * {@link #getBondedDevices()}; start device discovery with + * {@link #startDiscovery()}; or create a {@link BluetoothServerSocket} to + * listen for incoming connection requests with + * {@link #listenUsingRfcommWithServiceRecord(String,UUID)}. + * + *

    Note: + * Most methods require the {@link android.Manifest.permission#BLUETOOTH} + * permission and some also require the + * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * {@see BluetoothDevice} + * {@see BluetoothServerSocket} + */ +public final class BluetoothAdapter { + private static final String TAG = "BluetoothAdapter"; + private static final boolean DBG = false; + + /** + * Sentinel error value for this class. Guaranteed to not equal any other + * integer constant in this class. Provided as a convenience for functions + * that require a sentinel error value, for example: + *

    Intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + * BluetoothAdapter.ERROR) + */ + public static final int ERROR = Integer.MIN_VALUE; + + /** + * Broadcast Action: The state of the local Bluetooth adapter has been + * changed. + *

    For example, Bluetooth has been turned on or off. + *

    Always contains the extra fields {@link #EXTRA_STATE} and {@link + * #EXTRA_PREVIOUS_STATE} containing the new and old states + * respectively. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_STATE_CHANGED = + "android.bluetooth.adapter.action.STATE_CHANGED"; + + /** + * Used as an int extra field in {@link #ACTION_STATE_CHANGED} + * intents to request the current power state. Possible values are: + * {@link #STATE_OFF}, + * {@link #STATE_TURNING_ON}, + * {@link #STATE_ON}, + * {@link #STATE_TURNING_OFF}, + */ + public static final String EXTRA_STATE = + "android.bluetooth.adapter.extra.STATE"; + /** + * Used as an int extra field in {@link #ACTION_STATE_CHANGED} + * intents to request the previous power state. Possible values are: + * {@link #STATE_OFF}, + * {@link #STATE_TURNING_ON}, + * {@link #STATE_ON}, + * {@link #STATE_TURNING_OFF}, + */ + public static final String EXTRA_PREVIOUS_STATE = + "android.bluetooth.adapter.extra.PREVIOUS_STATE"; + + /** + * Indicates the local Bluetooth adapter is off. + */ + public static final int STATE_OFF = 10; + /** + * Indicates the local Bluetooth adapter is turning on. However local + * clients should wait for {@link #STATE_ON} before attempting to + * use the adapter. + */ + public static final int STATE_TURNING_ON = 11; + /** + * Indicates the local Bluetooth adapter is on, and ready for use. + */ + public static final int STATE_ON = 12; + /** + * Indicates the local Bluetooth adapter is turning off. Local clients + * should immediately attempt graceful disconnection of any remote links. + */ + public static final int STATE_TURNING_OFF = 13; + + /** + * Activity Action: Show a system activity that requests discoverable mode. + *

    This activity will also request the user to turn on Bluetooth if it + * is not currently enabled. + *

    Discoverable mode is equivalent to {@link + * #SCAN_MODE_CONNECTABLE_DISCOVERABLE}. It allows remote devices to see + * this Bluetooth adapter when they perform a discovery. + *

    For privacy, Android is not by default discoverable. + *

    The sender can optionally use extra field {@link + * #EXTRA_DISCOVERABLE_DURATION} to request the duration of + * discoverability. Currently the default duration is 120 seconds, and + * maximum duration is capped at 300 seconds for each request. + *

    Notification of the result of this activity is posted using the + * {@link android.app.Activity#onActivityResult} callback. The + * resultCode + * will be the duration (in seconds) of discoverability or + * {@link android.app.Activity#RESULT_CANCELED} if the user rejected + * discoverability or an error has occurred. + *

    Applications can also listen for {@link #ACTION_SCAN_MODE_CHANGED} + * for global notification whenever the scan mode changes. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_REQUEST_DISCOVERABLE = + "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE"; + + /** + * Used as an optional int extra field in {@link + * #ACTION_REQUEST_DISCOVERABLE} intents to request a specific duration + * for discoverability in seconds. The current default is 120 seconds, and + * requests over 300 seconds will be capped. These values could change. + */ + public static final String EXTRA_DISCOVERABLE_DURATION = + "android.bluetooth.adapter.extra.DISCOVERABLE_DURATION"; + + /** + * Activity Action: Show a system activity that allows the user to turn on + * Bluetooth. + *

    This system activity will return once Bluetooth has completed turning + * on, or the user has decided not to turn Bluetooth on. + *

    Notification of the result of this activity is posted using the + * {@link android.app.Activity#onActivityResult} callback. The + * resultCode + * will be {@link android.app.Activity#RESULT_OK} if Bluetooth has been + * turned on or {@link android.app.Activity#RESULT_CANCELED} if the user + * has rejected the request or an error has occurred. + *

    Applications can also listen for {@link #ACTION_STATE_CHANGED} + * for global notification whenever Bluetooth is turned on or off. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_REQUEST_ENABLE = + "android.bluetooth.adapter.action.REQUEST_ENABLE"; + + /** + * Broadcast Action: Indicates the Bluetooth scan mode of the local Adapter + * has changed. + *

    Always contains the extra fields {@link #EXTRA_SCAN_MODE} and {@link + * #EXTRA_PREVIOUS_SCAN_MODE} containing the new and old scan modes + * respectively. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SCAN_MODE_CHANGED = + "android.bluetooth.adapter.action.SCAN_MODE_CHANGED"; + + /** + * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED} + * intents to request the current scan mode. Possible values are: + * {@link #SCAN_MODE_NONE}, + * {@link #SCAN_MODE_CONNECTABLE}, + * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}, + */ + public static final String EXTRA_SCAN_MODE = "android.bluetooth.adapter.extra.SCAN_MODE"; + /** + * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED} + * intents to request the previous scan mode. Possible values are: + * {@link #SCAN_MODE_NONE}, + * {@link #SCAN_MODE_CONNECTABLE}, + * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}, + */ + public static final String EXTRA_PREVIOUS_SCAN_MODE = + "android.bluetooth.adapter.extra.PREVIOUS_SCAN_MODE"; + + /** + * Indicates that both inquiry scan and page scan are disabled on the local + * Bluetooth adapter. Therefore this device is neither discoverable + * nor connectable from remote Bluetooth devices. + */ + public static final int SCAN_MODE_NONE = 20; + /** + * Indicates that inquiry scan is disabled, but page scan is enabled on the + * local Bluetooth adapter. Therefore this device is not discoverable from + * remote Bluetooth devices, but is connectable from remote devices that + * have previously discovered this device. + */ + public static final int SCAN_MODE_CONNECTABLE = 21; + /** + * Indicates that both inquiry scan and page scan are enabled on the local + * Bluetooth adapter. Therefore this device is both discoverable and + * connectable from remote Bluetooth devices. + */ + public static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE = 23; + + + /** + * Broadcast Action: The local Bluetooth adapter has started the remote + * device discovery process. + *

    This usually involves an inquiry scan of about 12 seconds, followed + * by a page scan of each new device to retrieve its Bluetooth name. + *

    Register for {@link BluetoothDevice#ACTION_FOUND} to be notified as + * remote Bluetooth devices are found. + *

    Device discovery is a heavyweight procedure. New connections to + * remote Bluetooth devices should not be attempted while discovery is in + * progress, and existing connections will experience limited bandwidth + * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing + * discovery. + *

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

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

    This name is visible to remote Bluetooth devices. + *

    Always contains the extra field {@link #EXTRA_LOCAL_NAME} containing + * the name. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_LOCAL_NAME_CHANGED = + "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED"; + /** + * Used as a String extra field in {@link #ACTION_LOCAL_NAME_CHANGED} + * intents to request the local Bluetooth name. + */ + public static final String EXTRA_LOCAL_NAME = "android.bluetooth.adapter.extra.LOCAL_NAME"; + + /** @hide */ + public static final String BLUETOOTH_SERVICE = "bluetooth"; + + private static final int ADDRESS_LENGTH = 17; + + /** + * Lazyily initialized singleton. Guaranteed final after first object + * constructed. + */ + private static BluetoothAdapter sAdapter; + + private final IBluetooth mService; + + /** + * Get a handle to the default local Bluetooth adapter. + *

    Currently Android only supports one Bluetooth adapter, but the API + * could be extended to support more. This will always return the default + * adapter. + * @return the default local adapter, or null if Bluetooth is not supported + * on this hardware platform + */ + public static synchronized BluetoothAdapter getDefaultAdapter() { + if (sAdapter == null) { + IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE); + if (b != null) { + IBluetooth service = IBluetooth.Stub.asInterface(b); + sAdapter = new BluetoothAdapter(service); + } + } + return sAdapter; + } + + /** + * Use {@link #getDefaultAdapter} to get the BluetoothAdapter instance. + * @hide + */ + public BluetoothAdapter(IBluetooth service) { + if (service == null) { + throw new IllegalArgumentException("service is null"); + } + mService = service; + } + + /** + * Get a {@link BluetoothDevice} object for the given Bluetooth hardware + * address. + *

    Valid Bluetooth hardware addresses must be upper case, in a format + * such as "00:11:22:33:AA:BB". The helper {@link #checkBluetoothAddress} is + * available to validate a Bluetooth address. + *

    A {@link BluetoothDevice} will always be returned for a valid + * hardware address, even if this adapter has never seen that device. + * + * @param address valid Bluetooth MAC address + * @throws IllegalArgumentException if address is invalid + */ + public BluetoothDevice getRemoteDevice(String address) { + return new BluetoothDevice(address); + } + + /** + * Return true if Bluetooth is currently enabled and ready for use. + *

    Equivalent to: + * getBluetoothState() == STATE_ON + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @return true if the local adapter is turned on + */ + public boolean isEnabled() { + try { + return mService.isEnabled(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Get the current state of the local Bluetooth adapter. + *

    Possible return values are + * {@link #STATE_OFF}, + * {@link #STATE_TURNING_ON}, + * {@link #STATE_ON}, + * {@link #STATE_TURNING_OFF}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @return current state of Bluetooth adapter + */ + public int getState() { + try { + return mService.getBluetoothState(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return STATE_OFF; + } + + /** + * Turn on the local Bluetooth adapter. + *

    This powers on the underlying Bluetooth hardware, and starts all + * Bluetooth system services. + *

    This is an asynchronous call: it will return immediately, and + * clients should listen for {@link #ACTION_STATE_CHANGED} + * to be notified of subsequent adapter state changes. If this call returns + * true, then the adapter state will immediately transition from {@link + * #STATE_OFF} to {@link #STATE_TURNING_ON}, and some time + * later transition to either {@link #STATE_OFF} or {@link + * #STATE_ON}. If this call returns false then there was an + * immediate problem that will prevent the adapter from being turned on - + * such as Airplane mode, or the adapter is already turned on. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @return true to indicate adapter startup has begun, or false on + * immediate error + */ + public boolean enable() { + try { + return mService.enable(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Turn off the local Bluetooth adapter. + *

    This gracefully shuts down all Bluetooth connections, stops Bluetooth + * system services, and powers down the underlying Bluetooth hardware. + *

    This is an asynchronous call: it will return immediately, and + * clients should listen for {@link #ACTION_STATE_CHANGED} + * to be notified of subsequent adapter state changes. If this call returns + * true, then the adapter state will immediately transition from {@link + * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time + * later transition to either {@link #STATE_OFF} or {@link + * #STATE_ON}. If this call returns false then there was an + * immediate problem that will prevent the adapter from being turned off - + * such as the adapter already being turned off. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @return true to indicate adapter shutdown has begun, or false on + * immediate error + */ + public boolean disable() { + try { + return mService.disable(true); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Returns the hardware address of the local Bluetooth adapter. + *

    For example, "00:11:22:AA:BB:CC". + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @return Bluetooth hardware address as string + */ + public String getAddress() { + try { + return mService.getAddress(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Get the friendly Bluetooth name of the local Bluetooth adapter. + *

    This name is visible to remote Bluetooth devices. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @return the Bluetooth name, or null on error + */ + public String getName() { + try { + return mService.getName(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Set the friendly Bluetooth name of the local Bluetoth adapter. + *

    This name is visible to remote Bluetooth devices. + *

    Valid Bluetooth names are a maximum of 248 UTF-8 characters, however + * many remote devices can only display the first 40 characters, and some + * may be limited to just 20. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @param name a valid Bluetooth name + * @return true if the name was set, false otherwise + */ + public boolean setName(String name) { + try { + return mService.setName(name); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Get the current Bluetooth scan mode of the local Bluetooth adaper. + *

    The Bluetooth scan mode determines if the local adapter is + * connectable and/or discoverable from remote Bluetooth devices. + *

    Possible values are: + * {@link #SCAN_MODE_NONE}, + * {@link #SCAN_MODE_CONNECTABLE}, + * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @return scan mode + */ + public int getScanMode() { + try { + return mService.getScanMode(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return SCAN_MODE_NONE; + } + + /** + * Set the Bluetooth scan mode of the local Bluetooth adapter. + *

    The Bluetooth scan mode determines if the local adapter is + * connectable and/or discoverable from remote Bluetooth devices. + *

    For privacy reasons, discoverable mode is automatically turned off + * after duration seconds. For example, 120 seconds should be + * enough for a remote device to initiate and complete its discovery + * process. + *

    Valid scan mode values are: + * {@link #SCAN_MODE_NONE}, + * {@link #SCAN_MODE_CONNECTABLE}, + * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}. + *

    Requires {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} + *

    Applications cannot set the scan mode. They should use + * startActivityForResult( + * BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE}) + * instead. + * + * @param mode valid scan mode + * @param duration time in seconds to apply scan mode, only used for + * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE} + * @return true if the scan mode was set, false otherwise + * @hide + */ + public boolean setScanMode(int mode, int duration) { + try { + return mService.setScanMode(mode, duration); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** @hide */ + public boolean setScanMode(int mode) { + return setScanMode(mode, 120); + } + + /** @hide */ + public int getDiscoverableTimeout() { + try { + return mService.getDiscoverableTimeout(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return -1; + } + + /** @hide */ + public void setDiscoverableTimeout(int timeout) { + try { + mService.setDiscoverableTimeout(timeout); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + /** + * Start the remote device discovery process. + *

    The discovery process usually involves an inquiry scan of about 12 + * seconds, followed by a page scan of each new device to retrieve its + * Bluetooth name. + *

    This is an asynchronous call, it will return immediately. Register + * for {@link #ACTION_DISCOVERY_STARTED} and {@link + * #ACTION_DISCOVERY_FINISHED} intents to determine exactly when the + * discovery starts and completes. Register for {@link + * BluetoothDevice#ACTION_FOUND} to be notified as remote Bluetooth devices + * are found. + *

    Device discovery is a heavyweight procedure. New connections to + * remote Bluetooth devices should not be attempted while discovery is in + * progress, and existing connections will experience limited bandwidth + * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing + * discovery. + *

    Device discovery will only find remote devices that are currently + * discoverable (inquiry scan enabled). Many Bluetooth devices are + * not discoverable by default, and need to be entered into a special mode. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. + * + * @return true on success, false on error + */ + public boolean startDiscovery() { + try { + return mService.startDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Cancel the current device discovery process. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. + * + * @return true on success, false on error + */ + public boolean cancelDiscovery() { + try { + mService.cancelDiscovery(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Return true if the local Bluetooth adapter is currently in the device + * discovery process. + *

    Device discovery is a heavyweight procedure. New connections to + * remote Bluetooth devices should not be attempted while discovery is in + * progress, and existing connections will experience limited bandwidth + * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing + * discovery. + *

    Applications can also register for {@link #ACTION_DISCOVERY_STARTED} + * or {@link #ACTION_DISCOVERY_FINISHED} to be notified when discovery + * starts or completes. + *

    Requires {@link android.Manifest.permission#BLUETOOTH}. + * + * @return true if discovering + */ + public boolean isDiscovering() { + try { + return mService.isDiscovering(); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return false; + } + + /** + * Return the set of {@link BluetoothDevice} objects that are bonded + * (paired) to the local adapter. + *

    Requires {@link android.Manifest.permission#BLUETOOTH}. + * + * @return unmodifiable set of {@link BluetoothDevice}, or null on error + */ + public Set getBondedDevices() { + try { + return toDeviceSet(mService.listBonds()); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return null; + } + + /** + * Picks RFCOMM channels until none are left. + * Avoids reserved channels. + */ + private static class RfcommChannelPicker { + private static final int[] RESERVED_RFCOMM_CHANNELS = new int[] { + 10, // HFAG + 11, // HSAG + 12, // OPUSH + 19, // PBAP + }; + private static LinkedList sChannels; // master list of non-reserved channels + private static Random sRandom; + + private final LinkedList mChannels; // local list of channels left to try + + private final UUID mUuid; + + public RfcommChannelPicker(UUID uuid) { + synchronized (RfcommChannelPicker.class) { + if (sChannels == null) { + // lazy initialization of non-reserved rfcomm channels + sChannels = new LinkedList(); + for (int i = 1; i <= BluetoothSocket.MAX_RFCOMM_CHANNEL; i++) { + sChannels.addLast(new Integer(i)); + } + for (int reserved : RESERVED_RFCOMM_CHANNELS) { + sChannels.remove(new Integer(reserved)); + } + sRandom = new Random(); + } + mChannels = (LinkedList)sChannels.clone(); + } + mUuid = uuid; + } + /* Returns next random channel, or -1 if we're out */ + public int nextChannel() { + if (mChannels.size() == 0) { + return -1; + } + return mChannels.remove(sRandom.nextInt(mChannels.size())); + } + } + + /** + * Create a listening, secure RFCOMM Bluetooth socket. + *

    A remote device connecting to this socket will be authenticated and + * communication on this socket will be encrypted. + *

    Use {@link BluetoothServerSocket#accept} to retrieve incoming + * connections from a listening {@link BluetoothServerSocket}. + *

    Valid RFCOMM channels are in range 1 to 30. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * @param channel RFCOMM channel to listen on + * @return a listening RFCOMM BluetoothServerSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions, or channel in use. + * @hide + */ + public BluetoothServerSocket listenUsingRfcommOn(int channel) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_RFCOMM, true, true, channel); + int errno = socket.mSocket.bindListen(); + if (errno != 0) { + try { + socket.close(); + } catch (IOException e) {} + socket.mSocket.throwErrnoNative(errno); + } + return socket; + } + + /** + * Create a listening, secure RFCOMM Bluetooth socket with Service Record. + *

    A remote device connecting to this socket will be authenticated and + * communication on this socket will be encrypted. + *

    Use {@link BluetoothServerSocket#accept} to retrieve incoming + * connections from a listening {@link BluetoothServerSocket}. + *

    The system will assign an unused RFCOMM channel to listen on. + *

    The system will also register a Service Discovery + * Protocol (SDP) record with the local SDP server containing the specified + * UUID, service name, and auto-assigned channel. Remote Bluetooth devices + * can use the same UUID to query our SDP server and discover which channel + * to connect to. This SDP record will be removed when this socket is + * closed, or if this application closes unexpectedly. + *

    Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to + * connect to this socket from another device using the same {@link UUID}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * @param name service name for SDP record + * @param uuid uuid for SDP record + * @return a listening RFCOMM BluetoothServerSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions, or channel in use. + */ + public BluetoothServerSocket listenUsingRfcommWithServiceRecord(String name, UUID uuid) + throws IOException { + RfcommChannelPicker picker = new RfcommChannelPicker(uuid); + + BluetoothServerSocket socket; + int channel; + int errno; + while (true) { + channel = picker.nextChannel(); + + if (channel == -1) { + throw new IOException("No available channels"); + } + + socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_RFCOMM, true, true, channel); + errno = socket.mSocket.bindListen(); + if (errno == 0) { + if (DBG) Log.d(TAG, "listening on RFCOMM channel " + channel); + break; // success + } else if (errno == BluetoothSocket.EADDRINUSE) { + if (DBG) Log.d(TAG, "RFCOMM channel " + channel + " in use"); + try { + socket.close(); + } catch (IOException e) {} + continue; // try another channel + } else { + try { + socket.close(); + } catch (IOException e) {} + socket.mSocket.throwErrnoNative(errno); // Exception as a result of bindListen() + } + } + + int handle = -1; + try { + handle = mService.addRfcommServiceRecord(name, new ParcelUuid(uuid), channel, + new Binder()); + } catch (RemoteException e) {Log.e(TAG, "", e);} + if (handle == -1) { + try { + socket.close(); + } catch (IOException e) {} + throw new IOException("Not able to register SDP record for " + name); + } + socket.setCloseHandler(mHandler, handle); + return socket; + } + + /** + * Construct an unencrypted, unauthenticated, RFCOMM server socket. + * Call #accept to retrieve connections to this socket. + * @return An RFCOMM BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_RFCOMM, false, false, port); + int errno = socket.mSocket.bindListen(); + if (errno != 0) { + try { + socket.close(); + } catch (IOException e) {} + socket.mSocket.throwErrnoNative(errno); + } + return socket; + } + + /** + * Construct a SCO server socket. + * Call #accept to retrieve connections to this socket. + * @return A SCO BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public static BluetoothServerSocket listenUsingScoOn() throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_SCO, false, false, -1); + int errno = socket.mSocket.bindListen(); + if (errno != 0) { + try { + socket.close(); + } catch (IOException e) {} + socket.mSocket.throwErrnoNative(errno); + } + return socket; + } + + private Set toDeviceSet(String[] addresses) { + Set devices = new HashSet(addresses.length); + for (int i = 0; i < addresses.length; i++) { + devices.add(getRemoteDevice(addresses[i])); + } + return Collections.unmodifiableSet(devices); + } + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + /* handle socket closing */ + int handle = msg.what; + try { + if (DBG) Log.d(TAG, "Removing service record " + Integer.toHexString(handle)); + mService.removeServiceRecord(handle); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + }; + + /** + * Validate a Bluetooth address, such as "00:43:A8:23:10:F0" + *

    Alphabetic characters must be uppercase to be valid. + * + * @param address Bluetooth address as string + * @return true if the address is valid, false otherwise + */ + public static boolean checkBluetoothAddress(String address) { + if (address == null || address.length() != ADDRESS_LENGTH) { + return false; + } + for (int i = 0; i < ADDRESS_LENGTH; i++) { + char c = address.charAt(i); + switch (i % 3) { + case 0: + case 1: + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) { + // hex character, OK + break; + } + return false; + case 2: + if (c == ':') { + break; // OK + } + return false; + } + } + return true; + } +} diff --git a/core/java/android/bluetooth/BluetoothAudioGateway.java b/core/java/android/bluetooth/BluetoothAudioGateway.java index f3afd2a4a0bb58d0835ef7331a6fff6aa59b5835..abd7723c57c79a7ef8d2806edf9dc0a84642dde6 100644 --- a/core/java/android/bluetooth/BluetoothAudioGateway.java +++ b/core/java/android/bluetooth/BluetoothAudioGateway.java @@ -9,24 +9,22 @@ import android.util.Log; /** * Listen's for incoming RFCOMM connection for the headset / handsfree service. * - * This class is planned for deletion, in favor of a generic Rfcomm class. + * TODO: Use the new generic BluetoothSocket class instead of this legacy code * * @hide */ -public class BluetoothAudioGateway { +public final class BluetoothAudioGateway { private static final String TAG = "BT Audio Gateway"; private static final boolean DBG = false; private int mNativeData; static { classInitNative(); } - private BluetoothDevice mBluetooth; - /* in */ private int mHandsfreeAgRfcommChannel = -1; private int mHeadsetAgRfcommChannel = -1; - /* out */ + /* out - written by native code */ private String mConnectingHeadsetAddress; private int mConnectingHeadsetRfcommChannel; /* -1 when not connected */ private int mConnectingHeadsetSocketFd; @@ -35,17 +33,18 @@ public class BluetoothAudioGateway { private int mConnectingHandsfreeSocketFd; private int mTimeoutRemainingMs; /* in/out */ + private final BluetoothAdapter mAdapter; + public static final int DEFAULT_HF_AG_CHANNEL = 10; public static final int DEFAULT_HS_AG_CHANNEL = 11; - public BluetoothAudioGateway(BluetoothDevice bluetooth) { - this(bluetooth, DEFAULT_HF_AG_CHANNEL, DEFAULT_HS_AG_CHANNEL); + public BluetoothAudioGateway(BluetoothAdapter adapter) { + this(adapter, DEFAULT_HF_AG_CHANNEL, DEFAULT_HS_AG_CHANNEL); } - public BluetoothAudioGateway(BluetoothDevice bluetooth, - int handsfreeAgRfcommChannel, - int headsetAgRfcommChannel) { - mBluetooth = bluetooth; + public BluetoothAudioGateway(BluetoothAdapter adapter, int handsfreeAgRfcommChannel, + int headsetAgRfcommChannel) { + mAdapter = adapter; mHandsfreeAgRfcommChannel = handsfreeAgRfcommChannel; mHeadsetAgRfcommChannel = headsetAgRfcommChannel; initializeNativeDataNative(); @@ -58,18 +57,17 @@ public class BluetoothAudioGateway { private Handler mCallback; public class IncomingConnectionInfo { - IncomingConnectionInfo(BluetoothDevice bluetooth, String address, int socketFd, - int rfcommChan) { - mBluetooth = bluetooth; - mAddress = address; + public BluetoothAdapter mAdapter; + public BluetoothDevice mRemoteDevice; + public int mSocketFd; + public int mRfcommChan; + IncomingConnectionInfo(BluetoothAdapter adapter, BluetoothDevice remoteDevice, + int socketFd, int rfcommChan) { + mAdapter = adapter; + mRemoteDevice = remoteDevice; mSocketFd = socketFd; mRfcommChan = rfcommChan; } - - public BluetoothDevice mBluetooth; - public String mAddress; - public int mSocketFd; - public int mRfcommChan; } public static final int MSG_INCOMING_HEADSET_CONNECTION = 100; @@ -111,12 +109,11 @@ public class BluetoothAudioGateway { mConnectingHeadsetRfcommChannel); Message msg = Message.obtain(mCallback); msg.what = MSG_INCOMING_HEADSET_CONNECTION; - msg.obj = - new IncomingConnectionInfo( - mBluetooth, - mConnectingHeadsetAddress, - mConnectingHeadsetSocketFd, - mConnectingHeadsetRfcommChannel); + msg.obj = new IncomingConnectionInfo( + mAdapter, + mAdapter.getRemoteDevice(mConnectingHeadsetAddress), + mConnectingHeadsetSocketFd, + mConnectingHeadsetRfcommChannel); msg.sendToTarget(); } if (mConnectingHandsfreeRfcommChannel >= 0) { @@ -126,12 +123,11 @@ public class BluetoothAudioGateway { Message msg = Message.obtain(); msg.setTarget(mCallback); msg.what = MSG_INCOMING_HANDSFREE_CONNECTION; - msg.obj = - new IncomingConnectionInfo( - mBluetooth, - mConnectingHandsfreeAddress, - mConnectingHandsfreeSocketFd, - mConnectingHandsfreeRfcommChannel); + msg.obj = new IncomingConnectionInfo( + mAdapter, + mAdapter.getRemoteDevice(mConnectingHandsfreeAddress), + mConnectingHandsfreeSocketFd, + mConnectingHandsfreeRfcommChannel); msg.sendToTarget(); } } diff --git a/core/java/android/bluetooth/BluetoothClass.java b/core/java/android/bluetooth/BluetoothClass.java index 88ce18b45773e10a42db71b5441b24df8b8d2640..bc067130584ef6aa575ad2d225184aca8336e13d 100644 --- a/core/java/android/bluetooth/BluetoothClass.java +++ b/core/java/android/bluetooth/BluetoothClass.java @@ -16,39 +16,98 @@ package android.bluetooth; +import android.os.Parcel; +import android.os.Parcelable; + /** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * Static helper methods and constants to decode the device class bit vector - * returned by the Bluetooth API. + * Represents a Bluetooth class, which describes general characteristics + * and capabilities of a device. For example, a Bluetooth class will + * specify the general device type such as a phone, a computer, or + * headset, and whether it's capable of services such as audio or telephony. * - * The Android Bluetooth API returns a 32-bit integer to represent the class. - * The format of these bits is defined at - * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm - * (login required). This class provides static helper methods and constants to - * determine what Service Class(es) and Device Class are encoded in the 32-bit - * class. + *

    The Bluetooth class is useful as a hint to roughly describe a device (for example to + * show an icon in the UI), but does not reliably describe which Bluetooth + * profiles or services are actually supported by a device. * - * Devices typically have zero or more service classes, and exactly one device - * class. The device class is encoded as a major and minor device class, the - * minor being a subset of the major. + *

    Every Bluetooth class is composed of zero or more service classes, and + * exactly one device class. The device class is further broken down into major + * and minor device class components. * - * Class is useful to describe a device (for example to show an icon), - * but does not reliably describe what profiles a device supports. To determine - * profile support you usually need to perform SDP queries. + *

    {@link BluetoothClass} is useful as a hint to roughly describe a device + * (for example to show an icon in the UI), but does not reliably describe which + * Bluetooth profiles or services are actually supported by a device. Accurate + * service discovery is done through SDP requests, which are automatically + * performed when creating an RFCOMM socket with {@link + * BluetoothDevice#createRfcommSocketToServiceRecord(UUID)} and {@link + * BluetoothAdapter#listenUsingRfcommWithServiceRecord(String,UUID)}

    * - * Each of these helper methods takes the 32-bit integer class as an argument. + *

    Use {@link BluetoothDevice#getBluetoothClass} to retrieve the class for + * a remote device. * - * @hide + * */ -public class BluetoothClass { - /** Indicates the Bluetooth API could not retrieve the class */ +public final class BluetoothClass implements Parcelable { + /** + * Legacy error value. Applications should use null instead. + * @hide + */ public static final int ERROR = 0xFF000000; - /** Every Bluetooth device has zero or more service classes */ - public static class Service { - public static final int BITMASK = 0xFFE000; + private final int mClass; + + /** @hide */ + public BluetoothClass(int classInt) { + mClass = classInt; + } + + @Override + public boolean equals(Object o) { + if (o instanceof BluetoothClass) { + return mClass == ((BluetoothClass)o).mClass; + } + return false; + } + + @Override + public int hashCode() { + return mClass; + } + + @Override + public String toString() { + return Integer.toHexString(mClass); + } + + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public BluetoothClass createFromParcel(Parcel in) { + return new BluetoothClass(in.readInt()); + } + public BluetoothClass[] newArray(int size) { + return new BluetoothClass[size]; + } + }; + + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mClass); + } + + /** + * Defines all service class constants. + *

    Each {@link BluetoothClass} encodes zero or more service classes. + */ + public static final class Service { + private static final int BITMASK = 0xFFE000; public static final int LIMITED_DISCOVERABILITY = 0x002000; public static final int POSITIONING = 0x010000; @@ -59,32 +118,41 @@ public class BluetoothClass { public static final int AUDIO = 0x200000; public static final int TELEPHONY = 0x400000; public static final int INFORMATION = 0x800000; + } - /** Returns true if the given class supports the given Service Class. - * A bluetooth device can claim to support zero or more service classes. - * @param btClass The bluetooth class. - * @param serviceClass The service class constant to test for. For - * example, Service.AUDIO. Must be one of the - * Service.FOO constants. - * @return True if the service class is supported. - */ - public static boolean hasService(int btClass, int serviceClass) { - if (btClass == ERROR) { - return false; - } - return ((btClass & Service.BITMASK & serviceClass) != 0); - } + /** + * Return true if the specified service class is supported by this + * {@link BluetoothClass}. + *

    Valid service classes are the public constants in + * {@link BluetoothClass.Service}. For example, {@link + * BluetoothClass.Service#AUDIO}. + * + * @param service valid service class + * @return true if the service class is supported + */ + public boolean hasService(int service) { + return ((mClass & Service.BITMASK & service) != 0); } - /** Every Bluetooth device has exactly one device class, comprimised of - * major and minor components. We have not included the minor classes for - * major classes: NETWORKING, PERIPHERAL and IMAGING yet because they work - * a little differently. */ + /** + * Defines all device class constants. + *

    Each {@link BluetoothClass} encodes exactly one device class, with + * major and minor components. + *

    The constants in {@link + * BluetoothClass.Device} represent a combination of major and minor + * device components (the complete device class). The constants in {@link + * BluetoothClass.Device.Major} represent only major device classes. + *

    See {@link BluetoothClass.Service} for service class constants. + */ public static class Device { - public static final int BITMASK = 0x1FFC; + private static final int BITMASK = 0x1FFC; + /** + * Defines all major device class constants. + *

    See {@link BluetoothClass.Device} for minor classes. + */ public static class Major { - public static final int BITMASK = 0x1F00; + private static final int BITMASK = 0x1F00; public static final int MISC = 0x0000; public static final int COMPUTER = 0x0100; @@ -97,18 +165,6 @@ public class BluetoothClass { public static final int TOY = 0x0800; public static final int HEALTH = 0x0900; public static final int UNCATEGORIZED = 0x1F00; - - /** Returns the Major Device Class component of a bluetooth class. - * Values returned from this function can be compared with the constants - * Device.Major.FOO. A bluetooth device can only be associated - * with one major class. - */ - public static int getDeviceMajor(int btClass) { - if (btClass == ERROR) { - return ERROR; - } - return (btClass & Device.Major.BITMASK); - } } // Devices in the COMPUTER major class @@ -174,18 +230,106 @@ public class BluetoothClass { public static final int HEALTH_PULSE_OXIMETER = 0x0914; public static final int HEALTH_PULSE_RATE = 0x0918; public static final int HEALTH_DATA_DISPLAY = 0x091C; + } - /** Returns the Device Class component of a bluetooth class. This includes - * both the major and minor device components. Values returned from this - * function can be compared with the constants Device.FOO. A bluetooth - * device can only be associated with one device class. - */ - public static int getDevice(int btClass) { - if (btClass == ERROR) { - return ERROR; + /** + * Return the major device class component of this {@link BluetoothClass}. + *

    Values returned from this function can be compared with the + * public constants in {@link BluetoothClass.Device.Major} to determine + * which major class is encoded in this Bluetooth class. + * + * @return major device class component + */ + public int getMajorDeviceClass() { + return (mClass & Device.Major.BITMASK); + } + + /** + * Return the (major and minor) device class component of this + * {@link BluetoothClass}. + *

    Values returned from this function can be compared with the + * public constants in {@link BluetoothClass.Device} to determine which + * device class is encoded in this Bluetooth class. + * + * @return device class component + */ + public int getDeviceClass() { + return (mClass & Device.BITMASK); + } + + /** @hide */ + public static final int PROFILE_HEADSET = 0; + /** @hide */ + public static final int PROFILE_A2DP = 1; + /** @hide */ + public static final int PROFILE_OPP = 2; + + /** + * Check class bits for possible bluetooth profile support. + * This is a simple heuristic that tries to guess if a device with the + * given class bits might support specified profile. It is not accurate for all + * devices. It tries to err on the side of false positives. + * @param profile The profile to be checked + * @return True if this device might support specified profile. + * @hide + */ + public boolean doesClassMatch(int profile) { + if (profile == PROFILE_A2DP) { + if (hasService(Service.RENDER)) { + return true; + } + // By the A2DP spec, sinks must indicate the RENDER service. + // However we found some that do not (Chordette). So lets also + // match on some other class bits. + switch (getDeviceClass()) { + case Device.AUDIO_VIDEO_HIFI_AUDIO: + case Device.AUDIO_VIDEO_HEADPHONES: + case Device.AUDIO_VIDEO_LOUDSPEAKER: + case Device.AUDIO_VIDEO_CAR_AUDIO: + return true; + default: + return false; + } + } else if (profile == PROFILE_HEADSET) { + // The render service class is required by the spec for HFP, so is a + // pretty good signal + if (hasService(Service.RENDER)) { + return true; } - return (btClass & Device.BITMASK); + // Just in case they forgot the render service class + switch (getDeviceClass()) { + case Device.AUDIO_VIDEO_HANDSFREE: + case Device.AUDIO_VIDEO_WEARABLE_HEADSET: + case Device.AUDIO_VIDEO_CAR_AUDIO: + return true; + default: + return false; + } + } else if (profile == PROFILE_OPP) { + if (hasService(Service.OBJECT_TRANSFER)) { + return true; + } + + switch (getDeviceClass()) { + case Device.COMPUTER_UNCATEGORIZED: + case Device.COMPUTER_DESKTOP: + case Device.COMPUTER_SERVER: + case Device.COMPUTER_LAPTOP: + case Device.COMPUTER_HANDHELD_PC_PDA: + case Device.COMPUTER_PALM_SIZE_PC_PDA: + case Device.COMPUTER_WEARABLE: + case Device.PHONE_UNCATEGORIZED: + case Device.PHONE_CELLULAR: + case Device.PHONE_CORDLESS: + case Device.PHONE_SMART: + case Device.PHONE_MODEM_OR_GATEWAY: + case Device.PHONE_ISDN: + return true; + default: + return false; + } + } else { + return false; } } } - diff --git a/core/java/android/bluetooth/BluetoothDevice.aidl b/core/java/android/bluetooth/BluetoothDevice.aidl new file mode 100644 index 0000000000000000000000000000000000000000..daae74d52c43e6915d21c4af341ca611ea1a96a6 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothDevice.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +parcelable BluetoothDevice; diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index 951b4b0ab471c9a7f57da6ec282a7892b4830c5b..6cb9770a61005f25cd17b4af8881df164bd2bf97 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,515 +16,689 @@ package android.bluetooth; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ParcelUuid; import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Log; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.UUID; /** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. + * Represents a remote Bluetooth device. A {@link BluetoothDevice} lets you + * create a connection with the repective device or query information about + * it, such as the name, address, class, and bonding state. * - * Manages the local Bluetooth device. Scan for devices, create bondings, - * power up and down the adapter. + *

    This class is really just a thin wrapper for a Bluetooth hardware + * address. Objects of this class are immutable. Operations on this class + * are performed on the remote Bluetooth hardware address, using the + * {@link BluetoothAdapter} that was used to create this {@link + * BluetoothDevice}. * - * @hide + *

    To get a {@link BluetoothDevice}, use + * {@link BluetoothAdapter#getRemoteDevice(String) + * BluetoothAdapter.getRemoteDevice(String)} to create one representing a device + * of a known MAC address (which you can get through device discovery with + * {@link BluetoothAdapter}) or get one from the set of bonded devices + * returned by {@link BluetoothAdapter#getBondedDevices() + * BluetoothAdapter.getBondedDevices()}. You can then open a + * {@link BluetoothSocket} for communciation with the remote device, using + * {@link #createRfcommSocketToServiceRecord(UUID)}. + * + *

    Note: + * Requires the {@link android.Manifest.permission#BLUETOOTH} permission. + * + * {@see BluetoothAdapter} + * {@see BluetoothSocket} */ -public class BluetoothDevice { - - public static final int BLUETOOTH_STATE_OFF = 0; - public static final int BLUETOOTH_STATE_TURNING_ON = 1; - public static final int BLUETOOTH_STATE_ON = 2; - public static final int BLUETOOTH_STATE_TURNING_OFF = 3; - - /** Inquiry scan and page scan are both off. - * Device is neither discoverable nor connectable */ - public static final int SCAN_MODE_NONE = 0; - /** Page scan is on, inquiry scan is off. - * Device is connectable, but not discoverable */ - public static final int SCAN_MODE_CONNECTABLE = 1; - /** Page scan and inquiry scan are on. - * Device is connectable and discoverable */ - public static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE = 3; - - public static final int RESULT_FAILURE = -1; - public static final int RESULT_SUCCESS = 0; - - /** We do not have a link key for the remote device, and are therefore not - * bonded */ - public static final int BOND_NOT_BONDED = 0; - /** We have a link key for the remote device, and are probably bonded. */ - public static final int BOND_BONDED = 1; - /** We are currently attempting bonding */ - public static final int BOND_BONDING = 2; - - //TODO: Unify these result codes in BluetoothResult or BluetoothError - /** A bond attempt failed because pins did not match, or remote device did - * not respond to pin request in time */ - public static final int UNBOND_REASON_AUTH_FAILED = 1; - /** A bond attempt failed because the other side explicilty rejected - * bonding */ - public static final int UNBOND_REASON_AUTH_REJECTED = 2; - /** A bond attempt failed because we canceled the bonding process */ - public static final int UNBOND_REASON_AUTH_CANCELED = 3; - /** A bond attempt failed because we could not contact the remote device */ - public static final int UNBOND_REASON_REMOTE_DEVICE_DOWN = 4; - /** A bond attempt failed because a discovery is in progress */ - public static final int UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5; - /** An existing bond was explicitly revoked */ - public static final int UNBOND_REASON_REMOVED = 6; - +public final class BluetoothDevice implements Parcelable { private static final String TAG = "BluetoothDevice"; - - private final IBluetoothDevice mService; + /** - * @hide - hide this because it takes a parameter of type - * IBluetoothDevice, which is a System private class. - * Also note that Context.getSystemService is a factory that - * returns a BlueToothDevice. That is the right way to get - * a BluetoothDevice. + * Sentinel error value for this class. Guaranteed to not equal any other + * integer constant in this class. Provided as a convenience for functions + * that require a sentinel error value, for example: + *

    Intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, + * BluetoothDevice.ERROR) */ - public BluetoothDevice(IBluetoothDevice service) { - mService = service; - } + public static final int ERROR = Integer.MIN_VALUE; /** - * Is Bluetooth currently turned on. - * - * @return true if Bluetooth enabled, false otherwise. + * Broadcast Action: Remote device discovered. + *

    Sent when a remote device is found during discovery. + *

    Always contains the extra fields {@link #EXTRA_DEVICE} and {@link + * #EXTRA_CLASS}. Can contain the extra fields {@link #EXTRA_NAME} and/or + * {@link #EXTRA_RSSI} if they are available. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - public boolean isEnabled() { - try { - return mService.isEnabled(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; - } + // TODO: Change API to not broadcast RSSI if not available (incoming connection) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_FOUND = + "android.bluetooth.device.action.FOUND"; /** - * Get the current state of Bluetooth. - * - * @return One of BLUETOOTH_STATE_ or BluetoothError.ERROR. + * Broadcast Action: Remote device disappeared. + *

    Sent when a remote device that was found in the last discovery is not + * found in the current discovery. + *

    Always contains the extra field {@link #EXTRA_DEVICE}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + * @hide */ - public int getBluetoothState() { - try { - return mService.getBluetoothState(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return BluetoothError.ERROR; - } + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DISAPPEARED = + "android.bluetooth.device.action.DISAPPEARED"; /** - * Enable the Bluetooth device. - * Turn on the underlying hardware. - * This is an asynchronous call, - * BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION can be used to check if - * and when the device is sucessfully enabled. - * @return false if we cannot enable the Bluetooth device. True does not - * imply the device was enabled, it only implies that so far there were no - * problems. + * Broadcast Action: Bluetooth class of a remote device has changed. + *

    Always contains the extra fields {@link #EXTRA_DEVICE} and {@link + * #EXTRA_CLASS}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + * @see {@link BluetoothClass} */ - public boolean enable() { - try { - return mService.enable(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; - } + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CLASS_CHANGED = + "android.bluetooth.device.action.CLASS_CHANGED"; /** - * Disable the Bluetooth device. - * This turns off the underlying hardware. - * - * @return true if successful, false otherwise. + * Broadcast Action: Indicates a low level (ACL) connection has been + * established with a remote device. + *

    Always contains the extra field {@link #EXTRA_DEVICE}. + *

    ACL connections are managed automatically by the Android Bluetooth + * stack. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - public boolean disable() { - try { - return mService.disable(true); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; - } + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ACL_CONNECTED = + "android.bluetooth.device.action.ACL_CONNECTED"; - public String getAddress() { - try { - return mService.getAddress(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } + /** + * Broadcast Action: Indicates that a low level (ACL) disconnection has + * been requested for a remote device, and it will soon be disconnected. + *

    This is useful for graceful disconnection. Applications should use + * this intent as a hint to immediately terminate higher level connections + * (RFCOMM, L2CAP, or profile connections) to the remote device. + *

    Always contains the extra field {@link #EXTRA_DEVICE}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ACL_DISCONNECT_REQUESTED = + "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED"; /** - * Get the friendly Bluetooth name of this device. - * - * This name is visible to remote Bluetooth devices. Currently it is only - * possible to retrieve the Bluetooth name when Bluetooth is enabled. - * - * @return the Bluetooth name, or null if there was a problem. + * Broadcast Action: Indicates a low level (ACL) disconnection from a + * remote device. + *

    Always contains the extra field {@link #EXTRA_DEVICE}. + *

    ACL connections are managed automatically by the Android Bluetooth + * stack. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - public String getName() { - try { - return mService.getName(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ACL_DISCONNECTED = + "android.bluetooth.device.action.ACL_DISCONNECTED"; /** - * Set the friendly Bluetooth name of this device. - * - * This name is visible to remote Bluetooth devices. The Bluetooth Service - * is responsible for persisting this name. - * - * @param name the name to set - * @return true, if the name was successfully set. False otherwise. + * Broadcast Action: Indicates the friendly name of a remote device has + * been retrieved for the first time, or changed since the last retrieval. + *

    Always contains the extra fields {@link #EXTRA_DEVICE} and {@link + * #EXTRA_NAME}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. */ - public boolean setName(String name) { - try { - return mService.setName(name); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; - } + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NAME_CHANGED = + "android.bluetooth.device.action.NAME_CHANGED"; - public String getVersion() { - try { - return mService.getVersion(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } - public String getRevision() { - try { - return mService.getRevision(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } - public String getManufacturer() { - try { - return mService.getManufacturer(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } - public String getCompany() { - try { - return mService.getCompany(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } + /** + * Broadcast Action: Indicates a change in the bond state of a remote + * device. For example, if a device is bonded (paired). + *

    Always contains the extra fields {@link #EXTRA_DEVICE}, {@link + * #EXTRA_BOND_STATE} and {@link #EXTRA_PREVIOUS_BOND_STATE}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + */ + // Note: When EXTRA_BOND_STATE is BOND_NONE then this will also + // contain a hidden extra field EXTRA_REASON with the result code. + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_BOND_STATE_CHANGED = + "android.bluetooth.device.action.BOND_STATE_CHANGED"; /** - * Get the current scan mode. - * Used to determine if the local device is connectable and/or discoverable - * @return Scan mode, one of SCAN_MODE_* or an error code + * Used as a Parcelable {@link BluetoothDevice} extra field in every intent + * broadcast by this class. It contains the {@link BluetoothDevice} that + * the intent applies to. */ - public int getScanMode() { - try { - return mService.getScanMode(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return BluetoothError.ERROR_IPC; - } + public static final String EXTRA_DEVICE = "android.bluetooth.device.extra.DEVICE"; /** - * Set the current scan mode. - * Used to make the local device connectable and/or discoverable - * @param scanMode One of SCAN_MODE_* + * Used as a String extra field in {@link #ACTION_NAME_CHANGED} and {@link + * #ACTION_FOUND} intents. It contains the friendly Bluetooth name. */ - public void setScanMode(int scanMode) { - try { - mService.setScanMode(scanMode); - } catch (RemoteException e) {Log.e(TAG, "", e);} - } + public static final String EXTRA_NAME = "android.bluetooth.device.extra.NAME"; - public int getDiscoverableTimeout() { - try { - return mService.getDiscoverableTimeout(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return -1; - } - public void setDiscoverableTimeout(int timeout) { - try { - mService.setDiscoverableTimeout(timeout); - } catch (RemoteException e) {Log.e(TAG, "", e);} + /** + * Used as an optional short extra field in {@link #ACTION_FOUND} intents. + * Contains the RSSI value of the remote device as reported by the + * Bluetooth hardware. + */ + public static final String EXTRA_RSSI = "android.bluetooth.device.extra.RSSI"; + + /** + * Used as an Parcelable {@link BluetoothClass} extra field in {@link + * #ACTION_FOUND} and {@link #ACTION_CLASS_CHANGED} intents. + */ + public static final String EXTRA_CLASS = "android.bluetooth.device.extra.CLASS"; + + /** + * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents. + * Contains the bond state of the remote device. + *

    Possible values are: + * {@link #BOND_NONE}, + * {@link #BOND_BONDING}, + * {@link #BOND_BONDED}. + */ + public static final String EXTRA_BOND_STATE = "android.bluetooth.device.extra.BOND_STATE"; + /** + * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents. + * Contains the previous bond state of the remote device. + *

    Possible values are: + * {@link #BOND_NONE}, + * {@link #BOND_BONDING}, + * {@link #BOND_BONDED}. + */ + public static final String EXTRA_PREVIOUS_BOND_STATE = + "android.bluetooth.device.extra.PREVIOUS_BOND_STATE"; + /** + * Indicates the remote device is not bonded (paired). + *

    There is no shared link key with the remote device, so communication + * (if it is allowed at all) will be unauthenticated and unencrypted. + */ + public static final int BOND_NONE = 10; + /** + * Indicates bonding (pairing) is in progress with the remote device. + */ + public static final int BOND_BONDING = 11; + /** + * Indicates the remote device is bonded (paired). + *

    A shared link keys exists locally for the remote device, so + * communication can be authenticated and encrypted. + *

    Being bonded (paired) with a remote device does not necessarily + * mean the device is currently connected. It just means that the ponding + * procedure was compeleted at some earlier time, and the link key is still + * stored locally, ready to use on the next connection. + * + */ + public static final int BOND_BONDED = 12; + + /** @hide */ + public static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON"; + /** @hide */ + public static final String EXTRA_PAIRING_VARIANT = + "android.bluetooth.device.extra.PAIRING_VARIANT"; + /** @hide */ + public static final String EXTRA_PASSKEY = "android.bluetooth.device.extra.PASSKEY"; + + /** + * Broadcast Action: This intent is used to broadcast the {@link UUID} + * wrapped as a {@link android.os.ParcelUuid} of the remote device after it + * has been fetched. This intent is sent only when the UUIDs of the remote + * device are requested to be fetched using Service Discovery Protocol + *

    Always contains the extra field {@link #EXTRA_DEVICE} + *

    Always contains the extra filed {@link #EXTRA_UUID} + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_UUID = + "android.bleutooth.device.action.UUID"; + + /** + * Broadcast Action: Indicates a failure to retrieve the name of a remote + * device. + *

    Always contains the extra field {@link #EXTRA_DEVICE}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} to receive. + * @hide + */ + //TODO: is this actually useful? + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NAME_FAILED = + "android.bluetooth.device.action.NAME_FAILED"; + + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PAIRING_REQUEST = + "android.bluetooth.device.action.PAIRING_REQUEST"; + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PAIRING_CANCEL = + "android.bluetooth.device.action.PAIRING_CANCEL"; + + /** A bond attempt succeeded + * @hide */ + public static final int BOND_SUCCESS = 0; + /** A bond attempt failed because pins did not match, or remote device did + * not respond to pin request in time + * @hide */ + public static final int UNBOND_REASON_AUTH_FAILED = 1; + /** A bond attempt failed because the other side explicilty rejected + * bonding + * @hide */ + public static final int UNBOND_REASON_AUTH_REJECTED = 2; + /** A bond attempt failed because we canceled the bonding process + * @hide */ + public static final int UNBOND_REASON_AUTH_CANCELED = 3; + /** A bond attempt failed because we could not contact the remote device + * @hide */ + public static final int UNBOND_REASON_REMOTE_DEVICE_DOWN = 4; + /** A bond attempt failed because a discovery is in progress + * @hide */ + public static final int UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5; + /** A bond attempt failed because of authentication timeout + * @hide */ + public static final int UNBOND_REASON_AUTH_TIMEOUT = 6; + /** A bond attempt failed because of repeated attempts + * @hide */ + public static final int UNBOND_REASON_REPEATED_ATTEMPTS = 7; + /** A bond attempt failed because we received an Authentication Cancel + * by remote end + * @hide */ + public static final int UNBOND_REASON_REMOTE_AUTH_CANCELED = 8; + /** An existing bond was explicitly revoked + * @hide */ + public static final int UNBOND_REASON_REMOVED = 9; + + /** The user will be prompted to enter a pin + * @hide */ + public static final int PAIRING_VARIANT_PIN = 0; + /** The user will be prompted to enter a passkey + * @hide */ + public static final int PAIRING_VARIANT_PASSKEY = 1; + /** The user will be prompted to confirm the passkey displayed on the screen + * @hide */ + public static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2; + /** The user will be prompted to accept or deny the incoming pairing request + * @hide */ + public static final int PAIRING_VARIANT_CONSENT = 3; + /** The user will be prompted to enter the passkey displayed on remote device + * @hide */ + public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4; + + /** + * Used as an extra field in {@link #ACTION_UUID} intents, + * Contains the {@link android.os.ParcelUuid}s of the remote device which + * is a parcelable version of {@link UUID}. + * @hide + */ + public static final String EXTRA_UUID = "android.bluetooth.device.extra.UUID"; + + /** + * Lazy initialization. Guaranteed final after first object constructed, or + * getService() called. + * TODO: Unify implementation of sService amongst BluetoothFoo API's + */ + private static IBluetooth sService; + + private final String mAddress; + + /*package*/ static IBluetooth getService() { + synchronized (BluetoothDevice.class) { + if (sService == null) { + IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE); + if (b == null) { + throw new RuntimeException("Bluetooth service not available"); + } + sService = IBluetooth.Stub.asInterface(b); + } + } + return sService; } - public boolean startDiscovery() { - return startDiscovery(true); + /** + * Create a new BluetoothDevice + * Bluetooth MAC address must be upper case, such as "00:11:22:33:AA:BB", + * and is validated in this constructor. + * @param address valid Bluetooth MAC address + * @throws RuntimeException Bluetooth is not available on this platform + * @throws IllegalArgumentException address is invalid + * @hide + */ + /*package*/ BluetoothDevice(String address) { + getService(); // ensures sService is initialized + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + throw new IllegalArgumentException(address + " is not a valid Bluetooth address"); + } + + mAddress = address; } - public boolean startDiscovery(boolean resolveNames) { - try { - return mService.startDiscovery(resolveNames); - } catch (RemoteException e) {Log.e(TAG, "", e);} + + @Override + public boolean equals(Object o) { + if (o instanceof BluetoothDevice) { + return mAddress.equals(((BluetoothDevice)o).getAddress()); + } return false; } - public void cancelDiscovery() { - try { - mService.cancelDiscovery(); - } catch (RemoteException e) {Log.e(TAG, "", e);} + @Override + public int hashCode() { + return mAddress.hashCode(); } - public boolean isDiscovering() { - try { - return mService.isDiscovering(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + /** + * Returns a string representation of this BluetoothDevice. + *

    Currently this is the Bluetooth hardware address, for example + * "00:11:22:AA:BB:CC". However, you should always use {@link #getAddress} + * if you explicitly require the Bluetooth hardware address in case the + * {@link #toString} representation changes in the future. + * @return string representation of this BluetoothDevice + */ + @Override + public String toString() { + return mAddress; } - public boolean startPeriodicDiscovery() { - try { - return mService.startPeriodicDiscovery(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + public int describeContents() { + return 0; } - public boolean stopPeriodicDiscovery() { - try { - return mService.stopPeriodicDiscovery(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; - } - public boolean isPeriodicDiscovery() { - try { - return mService.isPeriodicDiscovery(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public BluetoothDevice createFromParcel(Parcel in) { + return new BluetoothDevice(in.readString()); + } + public BluetoothDevice[] newArray(int size) { + return new BluetoothDevice[size]; + } + }; + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mAddress); } - public String[] listRemoteDevices() { - try { - return mService.listRemoteDevices(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + /** + * Returns the hardware address of this BluetoothDevice. + *

    For example, "00:11:22:AA:BB:CC". + * @return Bluetooth hardware address as string + */ + public String getAddress() { + return mAddress; } /** - * List remote devices that have a low level (ACL) connection. - * - * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have - * an ACL connection even when not paired - this is common for SDP queries - * or for in-progress pairing requests. + * Get the friendly Bluetooth name of the remote device. * - * In most cases you probably want to test if a higher level protocol is - * connected, rather than testing ACL connections. + *

    The local adapter will automatically retrieve remote names when + * performing a device scan, and will cache them. This method just returns + * the name for this device from the cache. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} * - * @return bluetooth hardware addresses of remote devices with a current - * ACL connection. Array size is 0 if no devices have a - * connection. Null on error. + * @return the Bluetooth name, or null if there was a problem. */ - public String[] listAclConnections() { + public String getName() { try { - return mService.listAclConnections(); + return sService.getRemoteName(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return null; } /** - * Check if a specified remote device has a low level (ACL) connection. - * - * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have - * an ACL connection even when not paired - this is common for SDP queries - * or for in-progress pairing requests. - * - * In most cases you probably want to test if a higher level protocol is - * connected, rather than testing ACL connections. + * Start the bonding (pairing) process with the remote device. + *

    This is an asynchronous call, it will return immediately. Register + * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when + * the bonding process completes, and its result. + *

    Android system services will handle the necessary user interactions + * to confirm and complete the bonding process. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. * - * @param address the Bluetooth hardware address you want to check. - * @return true if there is an ACL connection, false otherwise and on - * error. + * @return false on immediate error, true if bonding will begin + * @hide */ - public boolean isAclConnected(String address) { + public boolean createBond() { try { - return mService.isAclConnected(address); + return sService.createBond(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } /** - * Perform a low level (ACL) disconnection of a remote device. + * Cancel an in-progress bonding request started with {@link #createBond}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. * - * This forcably disconnects the ACL layer connection to a remote device, - * which will cause all RFCOMM, SDP and L2CAP connections to this remote - * device to close. - * - * @param address the Bluetooth hardware address you want to disconnect. - * @return true if the device was disconnected, false otherwise and on - * error. + * @return true on sucess, false on error + * @hide */ - public boolean disconnectRemoteDeviceAcl(String address) { + public boolean cancelBondProcess() { try { - return mService.disconnectRemoteDeviceAcl(address); + return sService.cancelBondProcess(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } /** - * Create a bonding with a remote bluetooth device. - * - * This is an asynchronous call. The result of this bonding attempt can be - * observed through BluetoothIntent.BOND_STATE_CHANGED_ACTION intents. + * Remove bond (pairing) with the remote device. + *

    Delete the link key associated with the remote device, and + * immediately terminate connections to that device that require + * authentication and encryption. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. * - * @param address the remote device Bluetooth address. - * @return false If there was an immediate problem creating the bonding, - * true otherwise. + * @return true on sucess, false on error + * @hide */ - public boolean createBond(String address) { + public boolean removeBond() { try { - return mService.createBond(address); + return sService.removeBond(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } /** - * Cancel an in-progress bonding request started with createBond. + * Get the bond state of the remote device. + *

    Possible values for the bond state are: + * {@link #BOND_NONE}, + * {@link #BOND_BONDING}, + * {@link #BOND_BONDED}. + *

    Requires {@link android.Manifest.permission#BLUETOOTH}. + * + * @return the bond state */ - public boolean cancelBondProcess(String address) { + public int getBondState() { try { - return mService.cancelBondProcess(address); + return sService.getBondState(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + return BOND_NONE; } /** - * Remove an already exisiting bonding (delete the link key). + * Get the Bluetooth class of the remote device. + *

    Requires {@link android.Manifest.permission#BLUETOOTH}. + * + * @return Bluetooth class object, or null on error */ - public boolean removeBond(String address) { + public BluetoothClass getBluetoothClass() { try { - return mService.removeBond(address); + int classInt = sService.getRemoteClass(mAddress); + if (classInt == BluetoothClass.ERROR) return null; + return new BluetoothClass(classInt); } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + return null; } /** - * List remote devices that are bonded (paired) to the local device. - * - * Bonding (pairing) is the process by which the user enters a pin code for - * the device, which generates a shared link key, allowing for - * authentication and encryption of future connections. In Android we - * require bonding before RFCOMM or SCO connections can be made to a remote - * device. - * - * This function lists which remote devices we have a link key for. It does - * not cause any RF transmission, and does not check if the remote device - * still has it's link key with us. If the other side no longer has its - * link key then the RFCOMM or SCO connection attempt will result in an - * error. - * - * This function does not check if the remote device is in range. - * - * Remote devices that have an in-progress bonding attempt are not - * returned. - * - * @return bluetooth hardware addresses of remote devices that are - * bonded. Array size is 0 if no devices are bonded. Null on error. + * Get trust state of a remote device. + *

    Requires {@link android.Manifest.permission#BLUETOOTH}. + * @hide */ - public String[] listBonds() { + public boolean getTrustState() { try { - return mService.listBonds(); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + return sService.getTrustState(mAddress); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + return false; } /** - * Get the bonding state of a remote device. - * - * Result is one of: - * BluetoothError.* - * BOND_* - * - * @param address Bluetooth hardware address of the remote device to check. - * @return Result code + * Set trust state for a remote device. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}. + * @param value the trust state value (true or false) + * @hide */ - public int getBondState(String address) { + public boolean setTrust(boolean value) { try { - return mService.getBondState(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return BluetoothError.ERROR_IPC; + return sService.setTrust(mAddress, value); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + return false; } - public String getRemoteName(String address) { + /** @hide */ + public ParcelUuid[] getUuids() { try { - return mService.getRemoteName(address); + return sService.getRemoteUuids(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return null; } - public String getRemoteVersion(String address) { - try { - return mService.getRemoteVersion(address); + /** + * Perform a SDP query on the remote device to get the UUIDs + * supported. This API is asynchronous and an Intent is sent, + * with the UUIDs supported by the remote end. If there is an error + * in getting the SDP records or if the process takes a long time, + * an Intent is sent with the UUIDs that is currently present in the + * cache. Clients should use the {@link getUuids} to get UUIDs + * is SDP is not to be performed. + * + * @return False if the sanity check fails, True if the process + * of initiating an ACL connection to the remote device + * was started. + * @hide + */ + public boolean fetchUuidsWithSdp() { + try { + return sService.fetchRemoteUuids(mAddress, null, null); } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + return false; } - public String getRemoteRevision(String address) { + + /** @hide */ + public int getServiceChannel(ParcelUuid uuid) { + try { + return sService.getRemoteServiceChannel(mAddress, uuid); + } catch (RemoteException e) {Log.e(TAG, "", e);} + return BluetoothDevice.ERROR; + } + + /** @hide */ + public boolean setPin(byte[] pin) { try { - return mService.getRemoteRevision(address); + return sService.setPin(mAddress, pin); } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + return false; } - public String getRemoteManufacturer(String address) { + + /** @hide */ + public boolean setPasskey(int passkey) { try { - return mService.getRemoteManufacturer(address); + return sService.setPasskey(mAddress, passkey); } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + return false; } - public String getRemoteCompany(String address) { + + /** @hide */ + public boolean setPairingConfirmation(boolean confirm) { try { - return mService.getRemoteCompany(address); + return sService.setPairingConfirmation(mAddress, confirm); } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + return false; } - /** - * Returns the RFCOMM channel associated with the 16-byte UUID on - * the remote Bluetooth address. - * - * Performs a SDP ServiceSearchAttributeRequest transaction. The provided - * uuid is verified in the returned record. If there was a problem, or the - * specified uuid does not exist, -1 is returned. - */ - public boolean getRemoteServiceChannel(String address, short uuid16, - IBluetoothDeviceCallback callback) { + /** @hide */ + public boolean cancelPairingUserInput() { try { - return mService.getRemoteServiceChannel(address, uuid16, callback); + return sService.cancelPairingUserInput(mAddress); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } /** - * Get the major, minor and servics classes of a remote device. - * These classes are encoded as a 32-bit integer. See BluetoothClass. - * @param address remote device - * @return 32-bit class suitable for use with BluetoothClass, or - * BluetoothClass.ERROR on error + * Create an RFCOMM {@link BluetoothSocket} ready to start a secure + * outgoing connection to this remote device on given channel. + *

    The remote device will be authenticated and communication on this + * socket will be encrypted. + *

    Use {@link BluetoothSocket#connect} to intiate the outgoing + * connection. + *

    Valid RFCOMM channels are in range 1 to 30. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param channel RFCOMM channel to connect to + * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + * @hide */ - public int getRemoteClass(String address) { - try { - return mService.getRemoteClass(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return BluetoothClass.ERROR; + public BluetoothSocket createRfcommSocket(int channel) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, channel, + null); } - public byte[] getRemoteFeatures(String address) { - try { - return mService.getRemoteFeatures(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } - public String lastSeen(String address) { - try { - return mService.lastSeen(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; - } - public String lastUsed(String address) { - try { - return mService.lastUsed(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return null; + /** + * Create an RFCOMM {@link BluetoothSocket} ready to start a secure + * outgoing connection to this remote device using SDP lookup of uuid. + *

    This is designed to be used with {@link + * BluetoothAdapter#listenUsingRfcommWithServiceRecord} for peer-peer + * Bluetooth applications. + *

    Use {@link BluetoothSocket#connect} to intiate the outgoing + * connection. This will also perform an SDP lookup of the given uuid to + * determine which channel to connect to. + *

    The remote device will be authenticated and communication on this + * socket will be encrypted. + *

    Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param uuid service record uuid to lookup RFCOMM channel + * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + */ + public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, -1, + new ParcelUuid(uuid)); } - public boolean setPin(String address, byte[] pin) { - try { - return mService.setPin(address, pin); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + /** + * Construct an insecure RFCOMM socket ready to start an outgoing + * connection. + * Call #connect on the returned #BluetoothSocket to begin the connection. + * The remote device will not be authenticated and communication on this + * socket will not be encrypted. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @param port remote port + * @return An RFCOMM BluetoothSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public BluetoothSocket createInsecureRfcommSocket(int port) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, port, + null); } - public boolean cancelPin(String address) { - try { - return mService.cancelPin(address); - } catch (RemoteException e) {Log.e(TAG, "", e);} - return false; + + /** + * Construct a SCO socket ready to start an outgoing connection. + * Call #connect on the returned #BluetoothSocket to begin the connection. + *

    Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @return a SCO BluetoothSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public BluetoothSocket createScoSocket() throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_SCO, -1, true, true, this, -1, null); } /** @@ -534,6 +708,7 @@ public class BluetoothDevice { * @param pin pin as java String * @return the pin code as a UTF8 byte array, or null if it is an invalid * Bluetooth pin. + * @hide */ public static byte[] convertPinToBytes(String pin) { if (pin == null) { @@ -552,28 +727,4 @@ public class BluetoothDevice { return pinBytes; } - private static final int ADDRESS_LENGTH = 17; - /** Sanity check a bluetooth address, such as "00:43:A8:23:10:F0" */ - public static boolean checkBluetoothAddress(String address) { - if (address == null || address.length() != ADDRESS_LENGTH) { - return false; - } - for (int i = 0; i < ADDRESS_LENGTH; i++) { - char c = address.charAt(i); - switch (i % 3) { - case 0: - case 1: - if (Character.digit(c, 16) != -1) { - break; // hex character, OK - } - return false; - case 2: - if (c == ':') { - break; // OK - } - return false; - } - } - return true; - } } diff --git a/core/java/android/bluetooth/BluetoothDevicePicker.java b/core/java/android/bluetooth/BluetoothDevicePicker.java new file mode 100644 index 0000000000000000000000000000000000000000..05eed0e6de633c6f5db7c4990a11f206d991cc54 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothDevicePicker.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; + +/** + * A helper to show a system "Device Picker" activity to the user. + * + * @hide + */ +public interface BluetoothDevicePicker { + public static final String EXTRA_NEED_AUTH = + "android.bluetooth.devicepicker.extra.NEED_AUTH"; + public static final String EXTRA_FILTER_TYPE = + "android.bluetooth.devicepicker.extra.FILTER_TYPE"; + public static final String EXTRA_LAUNCH_PACKAGE = + "android.bluetooth.devicepicker.extra.LAUNCH_PACKAGE"; + public static final String EXTRA_LAUNCH_CLASS = + "android.bluetooth.devicepicker.extra.DEVICE_PICKER_LAUNCH_CLASS"; + + /** + * Broadcast when one BT device is selected from BT device picker screen. + * Selected BT device address is contained in extra string {@link BluetoothIntent} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEVICE_SELECTED = + "android.bluetooth.devicepicker.action.DEVICE_SELECTED"; + + /** + * Broadcast when someone want to select one BT device from devices list. + * This intent contains below extra data: + * - {@link #EXTRA_NEED_AUTH} (boolean): if need authentication + * - {@link #EXTRA_FILTER_TYPE} (int): what kinds of device should be + * listed + * - {@link #EXTRA_LAUNCH_PACKAGE} (string): where(which package) this + * intent come from + * - {@link #EXTRA_LAUNCH_CLASS} (string): where(which class) this intent + * come from + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_LAUNCH = + "android.bluetooth.devicepicker.action.LAUNCH"; + + /** Ask device picker to show all kinds of BT devices */ + public static final int FILTER_TYPE_ALL = 0; + /** Ask device picker to show BT devices that support AUDIO profiles */ + public static final int FILTER_TYPE_AUDIO = 1; + /** Ask device picker to show BT devices that support Object Transfer */ + public static final int FILTER_TYPE_TRANSFER = 2; +} diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index fe1e09af984a3984d20283fc055bca1e25a4426c..90cff6b8c90269b0be448180f81ff800499de6da 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -16,6 +16,8 @@ package android.bluetooth; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -49,11 +51,32 @@ import android.util.Log; * * @hide */ -public class BluetoothHeadset { +public final class BluetoothHeadset { private static final String TAG = "BluetoothHeadset"; private static final boolean DBG = false; + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_STATE_CHANGED = + "android.bluetooth.headset.action.STATE_CHANGED"; + /** + * TODO(API release): Consider incorporating as new state in + * HEADSET_STATE_CHANGED + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_AUDIO_STATE_CHANGED = + "android.bluetooth.headset.action.AUDIO_STATE_CHANGED"; + public static final String EXTRA_STATE = + "android.bluetooth.headset.extra.STATE"; + public static final String EXTRA_PREVIOUS_STATE = + "android.bluetooth.headset.extra.PREVIOUS_STATE"; + public static final String EXTRA_AUDIO_STATE = + "android.bluetooth.headset.extra.AUDIO_STATE"; + + /** + * TODO(API release): Consider incorporating as new state in + * HEADSET_STATE_CHANGED + */ private IBluetoothHeadset mService; private final Context mContext; private final ServiceListener mServiceListener; @@ -163,16 +186,16 @@ public class BluetoothHeadset { } /** - * Get the Bluetooth address of the current headset. - * @return The Bluetooth address, or null if not in connected or connecting + * Get the BluetoothDevice for the current headset. + * @return current headset, or null if not in connected or connecting * state, or if this proxy object is not connected to the Headset * service. */ - public String getHeadsetAddress() { - if (DBG) log("getHeadsetAddress()"); + public BluetoothDevice getCurrentHeadset() { + if (DBG) log("getCurrentHeadset()"); if (mService != null) { try { - return mService.getHeadsetAddress(); + return mService.getCurrentHeadset(); } catch (RemoteException e) {Log.e(TAG, e.toString());} } else { Log.w(TAG, "Proxy not attached to service"); @@ -185,19 +208,19 @@ public class BluetoothHeadset { * Request to initiate a connection to a headset. * This call does not block. Fails if a headset is already connecting * or connected. - * Initiates auto-connection if address is null. Tries to connect to all + * Initiates auto-connection if device is null. Tries to connect to all * devices with priority greater than PRIORITY_AUTO in descending order. - * @param address The Bluetooth Address to connect to, or null to - * auto-connect to the last connected headset. - * @return False if there was a problem initiating the connection - * procedure, and no further HEADSET_STATE_CHANGED intents - * will be expected. + * @param device device to connect to, or null to auto-connect last connected + * headset + * @return false if there was a problem initiating the connection + * procedure, and no further HEADSET_STATE_CHANGED intents + * will be expected. */ - public boolean connectHeadset(String address) { - if (DBG) log("connectHeadset(" + address + ")"); + public boolean connectHeadset(BluetoothDevice device) { + if (DBG) log("connectHeadset(" + device + ")"); if (mService != null) { try { - if (mService.connectHeadset(address)) { + if (mService.connectHeadset(device)) { return true; } } catch (RemoteException e) {Log.e(TAG, e.toString());} @@ -213,11 +236,11 @@ public class BluetoothHeadset { * connecting). Returns false if not connected, or if this proxy object * if not currently connected to the headset service. */ - public boolean isConnected(String address) { - if (DBG) log("isConnected(" + address + ")"); + public boolean isConnected(BluetoothDevice device) { + if (DBG) log("isConnected(" + device + ")"); if (mService != null) { try { - return mService.isConnected(address); + return mService.isConnected(device); } catch (RemoteException e) {Log.e(TAG, e.toString());} } else { Log.w(TAG, "Proxy not attached to service"); @@ -295,16 +318,16 @@ public class BluetoothHeadset { * auto-connected. * Incoming connections are ignored regardless of priority if there is * already a headset connected. - * @param address Paired headset + * @param device paired headset * @param priority Integer priority, for example PRIORITY_AUTO or * PRIORITY_NONE - * @return True if successful, false if there was some error. + * @return true if successful, false if there was some error */ - public boolean setPriority(String address, int priority) { - if (DBG) log("setPriority(" + address + ", " + priority + ")"); + public boolean setPriority(BluetoothDevice device, int priority) { + if (DBG) log("setPriority(" + device + ", " + priority + ")"); if (mService != null) { try { - return mService.setPriority(address, priority); + return mService.setPriority(device, priority); } catch (RemoteException e) {Log.e(TAG, e.toString());} } else { Log.w(TAG, "Proxy not attached to service"); @@ -315,14 +338,14 @@ public class BluetoothHeadset { /** * Get priority of headset. - * @param address Headset - * @return non-negative priority, or negative error code on error. + * @param device headset + * @return non-negative priority, or negative error code on error */ - public int getPriority(String address) { - if (DBG) log("getPriority(" + address + ")"); + public int getPriority(BluetoothDevice device) { + if (DBG) log("getPriority(" + device + ")"); if (mService != null) { try { - return mService.getPriority(address); + return mService.getPriority(device); } catch (RemoteException e) {Log.e(TAG, e.toString());} } else { Log.w(TAG, "Proxy not attached to service"); @@ -356,30 +379,6 @@ public class BluetoothHeadset { return -1; } - /** - * Check class bits for possible HSP or HFP support. - * This is a simple heuristic that tries to guess if a device with the - * given class bits might support HSP or HFP. It is not accurate for all - * devices. It tries to err on the side of false positives. - * @return True if this device might support HSP or HFP. - */ - public static boolean doesClassMatch(int btClass) { - // The render service class is required by the spec for HFP, so is a - // pretty good signal - if (BluetoothClass.Service.hasService(btClass, BluetoothClass.Service.RENDER)) { - return true; - } - // Just in case they forgot the render service class - switch (BluetoothClass.Device.getDevice(btClass)) { - case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: - case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: - case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: - return true; - default: - return false; - } - } - private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if (DBG) Log.d(TAG, "Proxy object connected"); diff --git a/core/java/android/bluetooth/BluetoothInputStream.java b/core/java/android/bluetooth/BluetoothInputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..03af95337c509cc7dafd369e6f909066e2c4a24a --- /dev/null +++ b/core/java/android/bluetooth/BluetoothInputStream.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import java.io.IOException; +import java.io.InputStream; + +/** + * BluetoothInputStream. + * + * Used to write to a Bluetooth socket. + * + * @hide + */ +/*package*/ final class BluetoothInputStream extends InputStream { + private BluetoothSocket mSocket; + + /*package*/ BluetoothInputStream(BluetoothSocket s) { + mSocket = s; + } + + /** + * Return number of bytes available before this stream will block. + */ + public int available() throws IOException { + return mSocket.available(); + } + + public void close() throws IOException { + mSocket.close(); + } + + /** + * Reads a single byte from this stream and returns it as an integer in the + * range from 0 to 255. Returns -1 if the end of the stream has been + * reached. Blocks until one byte has been read, the end of the source + * stream is detected or an exception is thrown. + * + * @return the byte read or -1 if the end of stream has been reached. + * @throws IOException + * if the stream is closed or another IOException occurs. + * @since Android 1.5 + */ + public int read() throws IOException { + byte b[] = new byte[1]; + int ret = mSocket.read(b, 0, 1); + if (ret == 1) { + return (int)b[0] & 0xff; + } else { + return -1; + } + } + + /** + * Reads at most {@code length} bytes from this stream and stores them in + * the byte array {@code b} starting at {@code offset}. + * + * @param b + * the byte array in which to store the bytes read. + * @param offset + * the initial position in {@code buffer} to store the bytes + * read from this stream. + * @param length + * the maximum number of bytes to store in {@code b}. + * @return the number of bytes actually read or -1 if the end of the stream + * has been reached. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code length < 0}, or if + * {@code offset + length} is greater than the length of + * {@code b}. + * @throws IOException + * if the stream is closed or another IOException occurs. + * @since Android 1.5 + */ + public int read(byte[] b, int offset, int length) throws IOException { + if (b == null) { + throw new NullPointerException("byte array is null"); + } + if ((offset | length) < 0 || length > b.length - offset) { + throw new ArrayIndexOutOfBoundsException("invalid offset or length"); + } + return mSocket.read(b, offset, length); + } +} diff --git a/core/java/android/bluetooth/BluetoothIntent.java b/core/java/android/bluetooth/BluetoothIntent.java deleted file mode 100644 index 344601b0b89a869873e1d6c7da46a13df8ef87a8..0000000000000000000000000000000000000000 --- a/core/java/android/bluetooth/BluetoothIntent.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 android.bluetooth; - -import android.annotation.SdkConstant; -import android.annotation.SdkConstant.SdkConstantType; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * Manages the local Bluetooth device. Scan for devices, create bondings, - * power up and down the adapter. - * - * @hide - */ -public interface BluetoothIntent { - public static final String SCAN_MODE = - "android.bluetooth.intent.SCAN_MODE"; - public static final String ADDRESS = - "android.bluetooth.intent.ADDRESS"; - public static final String NAME = - "android.bluetooth.intent.NAME"; - public static final String ALIAS = - "android.bluetooth.intent.ALIAS"; - public static final String RSSI = - "android.bluetooth.intent.RSSI"; - public static final String CLASS = - "android.bluetooth.intent.CLASS"; - public static final String BLUETOOTH_STATE = - "android.bluetooth.intent.BLUETOOTH_STATE"; - public static final String BLUETOOTH_PREVIOUS_STATE = - "android.bluetooth.intent.BLUETOOTH_PREVIOUS_STATE"; - public static final String HEADSET_STATE = - "android.bluetooth.intent.HEADSET_STATE"; - public static final String HEADSET_PREVIOUS_STATE = - "android.bluetooth.intent.HEADSET_PREVIOUS_STATE"; - public static final String HEADSET_AUDIO_STATE = - "android.bluetooth.intent.HEADSET_AUDIO_STATE"; - public static final String BOND_STATE = - "android.bluetooth.intent.BOND_STATE"; - public static final String BOND_PREVIOUS_STATE = - "android.bluetooth.intent.BOND_PREVIOUS_STATE"; - public static final String REASON = - "android.bluetooth.intent.REASON"; - - /** Broadcast when the local Bluetooth device state changes, for example - * when Bluetooth is enabled. Will contain int extra's BLUETOOTH_STATE and - * BLUETOOTH_PREVIOUS_STATE. */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String BLUETOOTH_STATE_CHANGED_ACTION = - "android.bluetooth.intent.action.BLUETOOTH_STATE_CHANGED"; - - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String NAME_CHANGED_ACTION = - "android.bluetooth.intent.action.NAME_CHANGED"; - - /** - * Broadcast when the scan mode changes. Always contains an int extra - * named SCAN_MODE that contains the new scan mode. - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String SCAN_MODE_CHANGED_ACTION = - "android.bluetooth.intent.action.SCAN_MODE_CHANGED"; - - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String DISCOVERY_STARTED_ACTION = - "android.bluetooth.intent.action.DISCOVERY_STARTED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String DISCOVERY_COMPLETED_ACTION = - "android.bluetooth.intent.action.DISCOVERY_COMPLETED"; - - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String PAIRING_REQUEST_ACTION = - "android.bluetooth.intent.action.PAIRING_REQUEST"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String PAIRING_CANCEL_ACTION = - "android.bluetooth.intent.action.PAIRING_CANCEL"; - - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_FOUND_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_FOUND"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_DISAPPEARED_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_CLASS_UPDATED_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_CONNECTED_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_CONNECTED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECT_REQUESTED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_DEVICE_DISCONNECTED_ACTION = - "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECTED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_NAME_UPDATED_ACTION = - "android.bluetooth.intent.action.REMOTE_NAME_UPDATED"; - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String REMOTE_NAME_FAILED_ACTION = - "android.bluetooth.intent.action.REMOTE_NAME_FAILED"; - - /** - * Broadcast when the bond state of a remote device changes. - * Has string extra ADDRESS and int extras BOND_STATE and - * BOND_PREVIOUS_STATE. - * If BOND_STATE is BluetoothDevice.BOND_NOT_BONDED then will - * also have an int extra REASON with a value of: - * BluetoothDevice.BOND_RESULT_* - * */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String BOND_STATE_CHANGED_ACTION = - "android.bluetooth.intent.action.BOND_STATE_CHANGED_ACTION"; - - /** - * TODO(API release): Move into BluetoothHeadset - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String HEADSET_STATE_CHANGED_ACTION = - "android.bluetooth.intent.action.HEADSET_STATE_CHANGED"; - - /** - * TODO(API release): Consider incorporating as new state in - * HEADSET_STATE_CHANGED - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String HEADSET_AUDIO_STATE_CHANGED_ACTION = - "android.bluetooth.intent.action.HEADSET_ADUIO_STATE_CHANGED"; -} diff --git a/core/java/android/bluetooth/BluetoothOutputStream.java b/core/java/android/bluetooth/BluetoothOutputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..62242a2672ffc766847c7ea50324290d3dccfab2 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothOutputStream.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * BluetoothOutputStream. + * + * Used to read from a Bluetooth socket. + * + * @hide + */ +/*package*/ final class BluetoothOutputStream extends OutputStream { + private BluetoothSocket mSocket; + + /*package*/ BluetoothOutputStream(BluetoothSocket s) { + mSocket = s; + } + + /** + * Close this output stream and the socket associated with it. + */ + public void close() throws IOException { + mSocket.close(); + } + + /** + * Writes a single byte to this stream. Only the least significant byte of + * the integer {@code oneByte} is written to the stream. + * + * @param oneByte + * the byte to be written. + * @throws IOException + * if an error occurs while writing to this stream. + * @since Android 1.0 + */ + public void write(int oneByte) throws IOException { + byte b[] = new byte[1]; + b[0] = (byte)oneByte; + mSocket.write(b, 0, 1); + } + + /** + * Writes {@code count} bytes from the byte array {@code buffer} starting + * at position {@code offset} to this stream. + * + * @param b + * the buffer to be written. + * @param offset + * the start position in {@code buffer} from where to get bytes. + * @param count + * the number of bytes from {@code buffer} to write to this + * stream. + * @throws IOException + * if an error occurs while writing to this stream. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code count < 0}, or if + * {@code offset + count} is bigger than the length of + * {@code buffer}. + * @since Android 1.0 + */ + public void write(byte[] b, int offset, int count) throws IOException { + if (b == null) { + throw new NullPointerException("buffer is null"); + } + if ((offset | count) < 0 || count > b.length - offset) { + throw new IndexOutOfBoundsException("invalid offset or length"); + } + mSocket.write(b, offset, count); + } +} diff --git a/core/java/android/bluetooth/BluetoothPbap.java b/core/java/android/bluetooth/BluetoothPbap.java new file mode 100644 index 0000000000000000000000000000000000000000..b48f48e9f1977b8259d16862143ae746d1ed8935 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothPbap.java @@ -0,0 +1,257 @@ +/* + * 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 android.bluetooth; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.ServiceManager; +import android.util.Log; + +/** + * The Android Bluetooth API is not finalized, and *will* change. Use at your + * own risk. + * + * Public API for controlling the Bluetooth Pbap Service. This includes + * Bluetooth Phone book Access profile. + * BluetoothPbap is a proxy object for controlling the Bluetooth Pbap + * Service via IPC. + * + * Creating a BluetoothPbap object will create a binding with the + * BluetoothPbap service. Users of this object should call close() when they + * are finished with the BluetoothPbap, so that this proxy object can unbind + * from the service. + * + * This BluetoothPbap object is not immediately bound to the + * BluetoothPbap service. Use the ServiceListener interface to obtain a + * notification when it is bound, this is especially important if you wish to + * immediately call methods on BluetoothPbap after construction. + * + * Android only supports one connected Bluetooth Pce at a time. + * + * @hide + */ +public class BluetoothPbap { + + private static final String TAG = "BluetoothPbap"; + private static final boolean DBG = false; + + /** int extra for PBAP_STATE_CHANGED_ACTION */ + public static final String PBAP_STATE = + "android.bluetooth.pbap.intent.PBAP_STATE"; + /** int extra for PBAP_STATE_CHANGED_ACTION */ + public static final String PBAP_PREVIOUS_STATE = + "android.bluetooth.pbap.intent.PBAP_PREVIOUS_STATE"; + + /** Indicates the state of an pbap connection state has changed. + * This intent will always contain PBAP_STATE, PBAP_PREVIOUS_STATE and + * BluetoothIntent.ADDRESS extras. + */ + public static final String PBAP_STATE_CHANGED_ACTION = + "android.bluetooth.pbap.intent.action.PBAP_STATE_CHANGED"; + + private IBluetoothPbap mService; + private final Context mContext; + private final ServiceListener mServiceListener; + + /** There was an error trying to obtain the state */ + public static final int STATE_ERROR = -1; + /** No client currently connected */ + public static final int STATE_DISCONNECTED = 0; + /** Connection attempt in progress */ + public static final int STATE_CONNECTING = 1; + /** Client is currently connected */ + public static final int STATE_CONNECTED = 2; + + public static final int RESULT_FAILURE = 0; + public static final int RESULT_SUCCESS = 1; + /** Connection canceled before completion. */ + public static final int RESULT_CANCELED = 2; + + /** + * An interface for notifying Bluetooth PCE IPC clients when they have + * been connected to the BluetoothPbap service. + */ + public interface ServiceListener { + /** + * Called to notify the client when this proxy object has been + * connected to the BluetoothPbap service. Clients must wait for + * this callback before making IPC calls on the BluetoothPbap + * service. + */ + public void onServiceConnected(); + + /** + * Called to notify the client that this proxy object has been + * disconnected from the BluetoothPbap service. Clients must not + * make IPC calls on the BluetoothPbap service after this callback. + * This callback will currently only occur if the application hosting + * the BluetoothPbap service, but may be called more often in future. + */ + public void onServiceDisconnected(); + } + + /** + * Create a BluetoothPbap proxy object. + */ + public BluetoothPbap(Context context, ServiceListener l) { + mContext = context; + mServiceListener = l; + if (!context.bindService(new Intent(IBluetoothPbap.class.getName()), mConnection, 0)) { + Log.e(TAG, "Could not bind to Bluetooth Pbap Service"); + } + } + + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Close the connection to the backing service. + * Other public functions of BluetoothPbap will return default error + * results once close() has been called. Multiple invocations of close() + * are ok. + */ + public synchronized void close() { + if (mConnection != null) { + mContext.unbindService(mConnection); + mConnection = null; + } + } + + /** + * Get the current state of the BluetoothPbap service. + * @return One of the STATE_ return codes, or STATE_ERROR if this proxy + * object is currently not connected to the Pbap service. + */ + public int getState() { + if (DBG) log("getState()"); + if (mService != null) { + try { + return mService.getState(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } + return BluetoothPbap.STATE_ERROR; + } + + /** + * Get the currently connected remote Bluetooth device (PCE). + * @return The remote Bluetooth device, or null if not in connected or + * connecting state, or if this proxy object is not connected to + * the Pbap service. + */ + public BluetoothDevice getClient() { + if (DBG) log("getClient()"); + if (mService != null) { + try { + return mService.getClient(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } + return null; + } + + /** + * Returns true if the specified Bluetooth device is connected (does not + * include connecting). Returns false if not connected, or if this proxy + * object is not currently connected to the Pbap service. + */ + public boolean isConnected(BluetoothDevice device) { + if (DBG) log("isConnected(" + device + ")"); + if (mService != null) { + try { + return mService.isConnected(device); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } + return false; + } + + /** + * Disconnects the current Pbap client (PCE). Currently this call blocks, + * it may soon be made asynchornous. Returns false if this proxy object is + * not currently connected to the Pbap service. + */ + public boolean disconnect() { + if (DBG) log("disconnect()"); + if (mService != null) { + try { + mService.disconnect(); + return true; + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } + return false; + } + + /** + * Check class bits for possible PBAP support. + * This is a simple heuristic that tries to guess if a device with the + * given class bits might support PBAP. It is not accurate for all + * devices. It tries to err on the side of false positives. + * @return True if this device might support PBAP. + */ + public static boolean doesClassMatchSink(BluetoothClass btClass) { + // TODO optimize the rule + switch (btClass.getDeviceClass()) { + case BluetoothClass.Device.COMPUTER_DESKTOP: + case BluetoothClass.Device.COMPUTER_LAPTOP: + case BluetoothClass.Device.COMPUTER_SERVER: + case BluetoothClass.Device.COMPUTER_UNCATEGORIZED: + return true; + default: + return false; + } + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + if (DBG) log("Proxy object connected"); + mService = IBluetoothPbap.Stub.asInterface(service); + if (mServiceListener != null) { + mServiceListener.onServiceConnected(); + } + } + public void onServiceDisconnected(ComponentName className) { + if (DBG) log("Proxy object disconnected"); + mService = null; + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(); + } + } + }; + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java new file mode 100644 index 0000000000000000000000000000000000000000..1b23f6c048d2537040e03471f8e3a1c09b7d88ab --- /dev/null +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.os.Handler; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A listening Bluetooth socket. + * + *

    The interface for Bluetooth Sockets is similar to that of TCP sockets: + * {@link java.net.Socket} and {@link java.net.ServerSocket}. On the server + * side, use a {@link BluetoothServerSocket} to create a listening server + * socket. When a connection is accepted by the {@link BluetoothServerSocket}, + * it will return a new {@link BluetoothSocket} to manage the connection. + * On the client side, use a single {@link BluetoothSocket} to both intiate + * an outgoing connection and to manage the connection. + * + *

    The most common type of Bluetooth socket is RFCOMM, which is the type + * supported by the Android APIs. RFCOMM is a connection-oriented, streaming + * transport over Bluetooth. It is also known as the Serial Port Profile (SPP). + * + *

    To create a listenting {@link BluetoothServerSocket} that's ready for + * incoming connections, use + * {@link BluetoothAdapter#listenUsingRfcommWithServiceRecord + * BluetoothAdapter.listenUsingRfcommWithServiceRecord()}. Then call + * {@link #accept()} to listen for incoming connection requests. This call + * will block until a connection is established, at which point, it will return + * a {@link BluetoothSocket} to manage the connection. + * + *

    {@link BluetoothServerSocket} is thread + * safe. In particular, {@link #close} will always immediately abort ongoing + * operations and close the server socket. + * + *

    Note: + * Requires the {@link android.Manifest.permission#BLUETOOTH} permission. + * + * {@see BluetoothSocket} + */ +public final class BluetoothServerSocket implements Closeable { + + /*package*/ final BluetoothSocket mSocket; + private Handler mHandler; + private int mMessage; + + /** + * Construct a socket for incoming connections. + * @param type type of socket + * @param auth require the remote device to be authenticated + * @param encrypt require the connection to be encrypted + * @param port remote port + * @throws IOException On error, for example Bluetooth not available, or + * insufficient priveleges + */ + /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, int port) + throws IOException { + mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null); + } + + /** + * Block until a connection is established. + *

    Returns a connected {@link BluetoothSocket} on successful connection. + *

    Once this call returns, it can be called again to accept subsequent + * incoming connections. + *

    {@link #close} can be used to abort this call from another thread. + * @return a connected {@link BluetoothSocket} + * @throws IOException on error, for example this call was aborted, or + * timeout + */ + public BluetoothSocket accept() throws IOException { + return accept(-1); + } + + /** + * Block until a connection is established, with timeout. + *

    Returns a connected {@link BluetoothSocket} on successful connection. + *

    Once this call returns, it can be called again to accept subsequent + * incoming connections. + *

    {@link #close} can be used to abort this call from another thread. + * @return a connected {@link BluetoothSocket} + * @throws IOException on error, for example this call was aborted, or + * timeout + */ + public BluetoothSocket accept(int timeout) throws IOException { + return mSocket.accept(timeout); + } + + /** + * Immediately close this socket, and release all associated resources. + *

    Causes blocked calls on this socket in other threads to immediately + * throw an IOException. + */ + public void close() throws IOException { + synchronized (this) { + if (mHandler != null) { + mHandler.obtainMessage(mMessage).sendToTarget(); + } + } + mSocket.close(); + } + + /*package*/ synchronized void setCloseHandler(Handler handler, int message) { + mHandler = handler; + mMessage = message; + } +} diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java new file mode 100644 index 0000000000000000000000000000000000000000..dbcc758574b65a0339822e18f8c4941f9fb26c91 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.IBluetoothCallback; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A connected or connecting Bluetooth socket. + * + *

    The interface for Bluetooth Sockets is similar to that of TCP sockets: + * {@link java.net.Socket} and {@link java.net.ServerSocket}. On the server + * side, use a {@link BluetoothServerSocket} to create a listening server + * socket. When a connection is accepted by the {@link BluetoothServerSocket}, + * it will return a new {@link BluetoothSocket} to manage the connection. + * On the client side, use a single {@link BluetoothSocket} to both intiate + * an outgoing connection and to manage the connection. + * + *

    The most common type of Bluetooth socket is RFCOMM, which is the type + * supported by the Android APIs. RFCOMM is a connection-oriented, streaming + * transport over Bluetooth. It is also known as the Serial Port Profile (SPP). + * + *

    To create a {@link BluetoothSocket} for connecting to a known device, use + * {@link BluetoothDevice#createRfcommSocketToServiceRecord + * BluetoothDevice.createRfcommSocketToServiceRecord()}. + * Then call {@link #connect()} to attempt a connection to the remote device. + * This call will block until a connection is established or the connection + * fails. + * + *

    To create a {@link BluetoothSocket} as a server (or "host"), see the + * {@link BluetoothServerSocket} documentation. + * + *

    Once the socket is connected, whether initiated as a client or accepted + * as a server, open the IO streams by calling {@link #getInputStream} and + * {@link #getOutputStream} in order to retrieve {@link java.io.InputStream} + * and {@link java.io.OutputStream} objects, respectively, which are + * automatically connected to the socket. + * + *

    {@link BluetoothSocket} is thread + * safe. In particular, {@link #close} will always immediately abort ongoing + * operations and close the socket. + * + *

    Note: + * Requires the {@link android.Manifest.permission#BLUETOOTH} permission. + * + * {@see BluetoothServerSocket} + * {@see java.io.InputStream} + * {@see java.io.OutputStream} + */ +public final class BluetoothSocket implements Closeable { + private static final String TAG = "BluetoothSocket"; + + /** @hide */ + public static final int MAX_RFCOMM_CHANNEL = 30; + + /** Keep TYPE_ fields in sync with BluetoothSocket.cpp */ + /*package*/ static final int TYPE_RFCOMM = 1; + /*package*/ static final int TYPE_SCO = 2; + /*package*/ static final int TYPE_L2CAP = 3; + + /*package*/ static final int EBADFD = 77; + /*package*/ static final int EADDRINUSE = 98; + + private final int mType; /* one of TYPE_RFCOMM etc */ + private final BluetoothDevice mDevice; /* remote device */ + private final String mAddress; /* remote address */ + private final boolean mAuth; + private final boolean mEncrypt; + private final BluetoothInputStream mInputStream; + private final BluetoothOutputStream mOutputStream; + private final SdpHelper mSdp; + + private int mPort; /* RFCOMM channel or L2CAP psm */ + + /** prevents all native calls after destroyNative() */ + private boolean mClosed; + + /** protects mClosed */ + private final ReentrantReadWriteLock mLock; + + /** used by native code only */ + private int mSocketData; + + /** + * Construct a BluetoothSocket. + * @param type type of socket + * @param fd fd to use for connected socket, or -1 for a new socket + * @param auth require the remote device to be authenticated + * @param encrypt require the connection to be encrypted + * @param device remote device that this socket can connect to + * @param port remote port + * @param uuid SDP uuid + * @throws IOException On error, for example Bluetooth not available, or + * insufficient priveleges + */ + /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, + BluetoothDevice device, int port, ParcelUuid uuid) throws IOException { + if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1) { + if (port < 1 || port > MAX_RFCOMM_CHANNEL) { + throw new IOException("Invalid RFCOMM channel: " + port); + } + } + if (uuid == null) { + mPort = port; + mSdp = null; + } else { + mSdp = new SdpHelper(device, uuid); + mPort = -1; + } + mType = type; + mAuth = auth; + mEncrypt = encrypt; + mDevice = device; + if (device == null) { + mAddress = null; + } else { + mAddress = device.getAddress(); + } + if (fd == -1) { + initSocketNative(); + } else { + initSocketFromFdNative(fd); + } + mInputStream = new BluetoothInputStream(this); + mOutputStream = new BluetoothOutputStream(this); + mClosed = false; + mLock = new ReentrantReadWriteLock(); + } + + /** + * Construct a BluetoothSocket from address. Used by native code. + * @param type type of socket + * @param fd fd to use for connected socket, or -1 for a new socket + * @param auth require the remote device to be authenticated + * @param encrypt require the connection to be encrypted + * @param address remote device that this socket can connect to + * @param port remote port + * @throws IOException On error, for example Bluetooth not available, or + * insufficient priveleges + */ + private BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address, + int port) throws IOException { + this(type, fd, auth, encrypt, new BluetoothDevice(address), port, null); + } + + /** @hide */ + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Attempt to connect to a remote device. + *

    This method will block until a connection is made or the connection + * fails. If this method returns without an exception then this socket + * is now connected. + *

    {@link #close} can be used to abort this call from another thread. + * @throws IOException on error, for example connection failure + */ + public void connect() throws IOException { + mLock.readLock().lock(); + try { + if (mClosed) throw new IOException("socket closed"); + + if (mSdp != null) { + mPort = mSdp.doSdp(); // blocks + } + + connectNative(); // blocks + } finally { + mLock.readLock().unlock(); + } + } + + /** + * Immediately close this socket, and release all associated resources. + *

    Causes blocked calls on this socket in other threads to immediately + * throw an IOException. + */ + public void close() throws IOException { + // abort blocking operations on the socket + mLock.readLock().lock(); + try { + if (mClosed) return; + if (mSdp != null) { + mSdp.cancel(); + } + abortNative(); + } finally { + mLock.readLock().unlock(); + } + + // all native calls are guaranteed to immediately return after + // abortNative(), so this lock should immediatley acquire + mLock.writeLock().lock(); + try { + mClosed = true; + destroyNative(); + } finally { + mLock.writeLock().unlock(); + } + } + + /** + * Get the remote device this socket is connecting, or connected, to. + * @return remote device + */ + public BluetoothDevice getRemoteDevice() { + return mDevice; + } + + /** + * Get the input stream associated with this socket. + *

    The input stream will be returned even if the socket is not yet + * connected, but operations on that stream will throw IOException until + * the associated socket is connected. + * @return InputStream + */ + public InputStream getInputStream() throws IOException { + return mInputStream; + } + + /** + * Get the output stream associated with this socket. + *

    The output stream will be returned even if the socket is not yet + * connected, but operations on that stream will throw IOException until + * the associated socket is connected. + * @return OutputStream + */ + public OutputStream getOutputStream() throws IOException { + return mOutputStream; + } + + /** + * Currently returns unix errno instead of throwing IOException, + * so that BluetoothAdapter can check the error code for EADDRINUSE + */ + /*package*/ int bindListen() { + mLock.readLock().lock(); + try { + if (mClosed) return EBADFD; + return bindListenNative(); + } finally { + mLock.readLock().unlock(); + } + } + + /*package*/ BluetoothSocket accept(int timeout) throws IOException { + mLock.readLock().lock(); + try { + if (mClosed) throw new IOException("socket closed"); + return acceptNative(timeout); + } finally { + mLock.readLock().unlock(); + } + } + + /*package*/ int available() throws IOException { + mLock.readLock().lock(); + try { + if (mClosed) throw new IOException("socket closed"); + return availableNative(); + } finally { + mLock.readLock().unlock(); + } + } + + /*package*/ int read(byte[] b, int offset, int length) throws IOException { + mLock.readLock().lock(); + try { + if (mClosed) throw new IOException("socket closed"); + return readNative(b, offset, length); + } finally { + mLock.readLock().unlock(); + } + } + + /*package*/ int write(byte[] b, int offset, int length) throws IOException { + mLock.readLock().lock(); + try { + if (mClosed) throw new IOException("socket closed"); + return writeNative(b, offset, length); + } finally { + mLock.readLock().unlock(); + } + } + + private native void initSocketNative() throws IOException; + private native void initSocketFromFdNative(int fd) throws IOException; + private native void connectNative() throws IOException; + private native int bindListenNative(); + private native BluetoothSocket acceptNative(int timeout) throws IOException; + private native int availableNative() throws IOException; + private native int readNative(byte[] b, int offset, int length) throws IOException; + private native int writeNative(byte[] b, int offset, int length) throws IOException; + private native void abortNative() throws IOException; + private native void destroyNative() throws IOException; + /** + * Throws an IOException for given posix errno. Done natively so we can + * use strerr to convert to string error. + */ + /*package*/ native void throwErrnoNative(int errno) throws IOException; + + /** + * Helper to perform blocking SDP lookup. + */ + private static class SdpHelper extends IBluetoothCallback.Stub { + private final IBluetooth service; + private final ParcelUuid uuid; + private final BluetoothDevice device; + private int channel; + private boolean canceled; + public SdpHelper(BluetoothDevice device, ParcelUuid uuid) { + service = BluetoothDevice.getService(); + this.device = device; + this.uuid = uuid; + canceled = false; + } + /** + * Returns the RFCOMM channel for the UUID, or throws IOException + * on failure. + */ + public synchronized int doSdp() throws IOException { + if (canceled) throw new IOException("Service discovery canceled"); + channel = -1; + + boolean inProgress = false; + try { + inProgress = service.fetchRemoteUuids(device.getAddress(), uuid, this); + } catch (RemoteException e) {Log.e(TAG, "", e);} + + if (!inProgress) throw new IOException("Unable to start Service Discovery"); + + try { + /* 12 second timeout as a precaution - onRfcommChannelFound + * should always occur before the timeout */ + wait(12000); // block + + } catch (InterruptedException e) {} + + if (canceled) throw new IOException("Service discovery canceled"); + if (channel < 1) throw new IOException("Service discovery failed"); + + return channel; + } + /** Object cannot be re-used after calling cancel() */ + public synchronized void cancel() { + if (!canceled) { + canceled = true; + channel = -1; + notifyAll(); // unblock + } + } + public synchronized void onRfcommChannelFound(int channel) { + if (!canceled) { + this.channel = channel; + notifyAll(); // unblock + } + } + } +} diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java new file mode 100644 index 0000000000000000000000000000000000000000..4164a3d6e66587f83c59b1311697383804947811 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothUuid.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.os.ParcelUuid; + +import java.util.Arrays; +import java.util.HashSet; + +/** +* Static helper methods and constants to decode the ParcelUuid of remote devices. +* @hide +*/ +public final class BluetoothUuid { + + /* See Bluetooth Assigned Numbers document - SDP section, to get the values of UUIDs + * for the various services. + * + * The following 128 bit values are calculated as: + * uuid * 2^96 + BASE_UUID + */ + public static final ParcelUuid AudioSink = + ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid AudioSource = + ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid AdvAudioDist = + ParcelUuid.fromString("0000110D-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid HSP = + ParcelUuid.fromString("00001108-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid Handsfree = + ParcelUuid.fromString("0000111E-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid AvrcpController = + ParcelUuid.fromString("0000110E-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid AvrcpTarget = + ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"); + public static final ParcelUuid ObexObjectPush = + ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb"); + + public static final ParcelUuid[] RESERVED_UUIDS = { + AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget, + ObexObjectPush}; + + public static boolean isAudioSource(ParcelUuid uuid) { + return uuid.equals(AudioSource); + } + + public static boolean isAudioSink(ParcelUuid uuid) { + return uuid.equals(AudioSink); + } + + public static boolean isAdvAudioDist(ParcelUuid uuid) { + return uuid.equals(AdvAudioDist); + } + + public static boolean isHandsfree(ParcelUuid uuid) { + return uuid.equals(Handsfree); + } + + public static boolean isHeadset(ParcelUuid uuid) { + return uuid.equals(HSP); + } + + public static boolean isAvrcpController(ParcelUuid uuid) { + return uuid.equals(AvrcpController); + } + + public static boolean isAvrcpTarget(ParcelUuid uuid) { + return uuid.equals(AvrcpTarget); + } + + /** + * Returns true if ParcelUuid is present in uuidArray + * + * @param uuidArray - Array of ParcelUuids + * @param uuid + */ + public static boolean isUuidPresent(ParcelUuid[] uuidArray, ParcelUuid uuid) { + if ((uuidArray == null || uuidArray.length == 0) && uuid == null) + return true; + + if (uuidArray == null) + return false; + + for (ParcelUuid element: uuidArray) { + if (element.equals(uuid)) return true; + } + return false; + } + + /** + * Returns true if there any common ParcelUuids in uuidA and uuidB. + * + * @param uuidA - List of ParcelUuids + * @param uuidB - List of ParcelUuids + * + */ + public static boolean containsAnyUuid(ParcelUuid[] uuidA, ParcelUuid[] uuidB) { + if (uuidA == null && uuidB == null) return true; + + if (uuidA == null) { + return uuidB.length == 0 ? true : false; + } + + if (uuidB == null) { + return uuidA.length == 0 ? true : false; + } + + HashSet uuidSet = new HashSet (Arrays.asList(uuidA)); + for (ParcelUuid uuid: uuidB) { + if (uuidSet.contains(uuid)) return true; + } + return false; + } + + /** + * Returns true if all the ParcelUuids in ParcelUuidB are present in + * ParcelUuidA + * + * @param uuidA - Array of ParcelUuidsA + * @param uuidB - Array of ParcelUuidsB + * + */ + public static boolean containsAllUuids(ParcelUuid[] uuidA, ParcelUuid[] uuidB) { + if (uuidA == null && uuidB == null) return true; + + if (uuidA == null) { + return uuidB.length == 0 ? true : false; + } + + if (uuidB == null) return true; + + HashSet uuidSet = new HashSet (Arrays.asList(uuidA)); + for (ParcelUuid uuid: uuidB) { + if (!uuidSet.contains(uuid)) return false; + } + return true; + } + +} diff --git a/core/java/android/bluetooth/Database.java b/core/java/android/bluetooth/Database.java deleted file mode 100644 index fef641a5f7ca0776e81d9ac152b42de1acc7bc20..0000000000000000000000000000000000000000 --- a/core/java/android/bluetooth/Database.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -import android.bluetooth.RfcommSocket; - -import android.util.Log; - -import java.io.*; -import java.util.*; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * A low-level API to the Service Discovery Protocol (SDP) Database. - * - * Allows service records to be added to the local SDP database. Once added, - * these services will be advertised to remote devices when they make SDP - * queries on this device. - * - * Currently this API is a thin wrapper to the bluez SDP Database API. See: - * http://wiki.bluez.org/wiki/Database - * http://wiki.bluez.org/wiki/HOWTO/ManagingServiceRecords - * @hide - */ -public final class Database { - private static Database mInstance; - - private static final String sLogName = "android.bluetooth.Database"; - - /** - * Class load time initialization - */ - static { - classInitNative(); - } - private native static void classInitNative(); - - /** - * Private to enforce singleton property - */ - private Database() { - initializeNativeDataNative(); - } - private native void initializeNativeDataNative(); - - protected void finalize() throws Throwable { - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - private native void cleanupNativeDataNative(); - - /** - * Singelton accessor - * @return The singleton instance of Database - */ - public static synchronized Database getInstance() { - if (mInstance == null) { - mInstance = new Database(); - } - return mInstance; - } - - /** - * Advertise a service with an RfcommSocket. - * - * This adds the service the SDP Database with the following attributes - * set: Service Name, Protocol Descriptor List, Service Class ID List - * TODO: Construct a byte[] record directly, rather than via XML. - * @param socket The rfcomm socket to advertise (by channel). - * @param serviceName A short name for this service - * @param uuid - * Unique identifier for this service, by which clients - * can search for your service - * @return Handle to the new service record - */ - public int advertiseRfcommService(RfcommSocket socket, - String serviceName, - UUID uuid) throws IOException { - String xmlRecord = - "\n" + - "\n" + - " \n" + // ServiceClassIDList - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + // ProtocolDescriptorList - " \n" + - " \n" + - " \n" + // L2CAP - " \n" + - " \n" + - " \n" + // RFCOMM - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + // ServiceName - " \n" + - " \n" + - "\n"; - Log.i(sLogName, xmlRecord); - return addServiceRecordFromXml(xmlRecord); - } - - - /** - * Add a new service record. - * @param record The byte[] record - * @return A handle to the new record - */ - public synchronized int addServiceRecord(byte[] record) throws IOException { - int handle = addServiceRecordNative(record); - Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); - return handle; - } - private native int addServiceRecordNative(byte[] record) - throws IOException; - - /** - * Add a new service record, using XML. - * @param record The record as an XML string - * @return A handle to the new record - */ - public synchronized int addServiceRecordFromXml(String record) throws IOException { - int handle = addServiceRecordFromXmlNative(record); - Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); - return handle; - } - private native int addServiceRecordFromXmlNative(String record) - throws IOException; - - /** - * Update an exisiting service record. - * @param handle Handle to exisiting record - * @param record The updated byte[] record - */ - public synchronized void updateServiceRecord(int handle, byte[] record) { - try { - updateServiceRecordNative(handle, record); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void updateServiceRecordNative(int handle, byte[] record) - throws IOException; - - /** - * Update an exisiting record, using XML. - * @param handle Handle to exisiting record - * @param record The record as an XML string. - */ - public synchronized void updateServiceRecordFromXml(int handle, String record) { - try { - updateServiceRecordFromXmlNative(handle, record); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void updateServiceRecordFromXmlNative(int handle, String record) - throws IOException; - - /** - * Remove a service record. - * It is only possible to remove service records that were added by the - * current connection. - * @param handle Handle to exisiting record to be removed - */ - public synchronized void removeServiceRecord(int handle) { - try { - removeServiceRecordNative(handle); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void removeServiceRecordNative(int handle) throws IOException; -} diff --git a/core/java/android/bluetooth/HeadsetBase.java b/core/java/android/bluetooth/HeadsetBase.java index f987ffdd440ac8480c873bad4f66e4d006585704..e2935c95d324a423c2550c519b1e53912c4f5880 100644 --- a/core/java/android/bluetooth/HeadsetBase.java +++ b/core/java/android/bluetooth/HeadsetBase.java @@ -31,7 +31,7 @@ import android.util.Log; * * @hide */ -public class HeadsetBase { +public final class HeadsetBase { private static final String TAG = "Bluetooth HeadsetBase"; private static final boolean DBG = false; @@ -42,8 +42,9 @@ public class HeadsetBase { private static int sAtInputCount = 0; /* TODO: Consider not using a static variable */ - private final BluetoothDevice mBluetooth; - private final String mAddress; + private final BluetoothAdapter mAdapter; + private final BluetoothDevice mRemoteDevice; + private final String mAddress; // for native code private final int mRfcommChannel; private int mNativeData; private Thread mEventThread; @@ -73,12 +74,13 @@ public class HeadsetBase { private native void cleanupNativeDataNative(); - public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address, - int rfcommChannel) { + public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, BluetoothDevice device, + int rfcommChannel) { mDirection = DIRECTION_OUTGOING; mConnectTimestamp = System.currentTimeMillis(); - mBluetooth = bluetooth; - mAddress = address; + mAdapter = adapter; + mRemoteDevice = device; + mAddress = device.getAddress(); mRfcommChannel = rfcommChannel; mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase"); mWakeLock.setReferenceCounted(false); @@ -88,12 +90,13 @@ public class HeadsetBase { } /* Create from an already exisiting rfcomm connection */ - public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address, int socketFd, - int rfcommChannel, Handler handler) { + public HeadsetBase(PowerManager pm, BluetoothAdapter adapter, BluetoothDevice device, + int socketFd, int rfcommChannel, Handler handler) { mDirection = DIRECTION_INCOMING; mConnectTimestamp = System.currentTimeMillis(); - mBluetooth = bluetooth; - mAddress = address; + mAdapter = adapter; + mRemoteDevice = device; + mAddress = device.getAddress(); mRfcommChannel = rfcommChannel; mEventThreadHandler = handler; mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase"); @@ -208,9 +211,10 @@ public class HeadsetBase { */ public boolean connectAsync() { - return connectAsyncNative(); + int ret = connectAsyncNative(); + return (ret == 0) ? true : false; } - private native boolean connectAsyncNative(); + private native int connectAsyncNative(); public int getRemainingAsyncConnectWaitingTimeMs() { return mTimeoutRemainingMs; @@ -252,12 +256,8 @@ public class HeadsetBase { return mEventThread != null; } - public String getAddress() { - return mAddress; - } - - public String getName() { - return mBluetooth.getRemoteName(mAddress); + public BluetoothDevice getRemoteDevice() { + return mRemoteDevice; } public int getDirection() { diff --git a/core/java/android/bluetooth/IBluetoothDevice.aidl b/core/java/android/bluetooth/IBluetooth.aidl similarity index 58% rename from core/java/android/bluetooth/IBluetoothDevice.aidl rename to core/java/android/bluetooth/IBluetooth.aidl index 6cd792e26a52a14c187593d4d3034c7bf155cf5b..7e752af428f6263ca509578b2b7b427e51ec836f 100644 --- a/core/java/android/bluetooth/IBluetoothDevice.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -16,14 +16,15 @@ package android.bluetooth; -import android.bluetooth.IBluetoothDeviceCallback; +import android.bluetooth.IBluetoothCallback; +import android.os.ParcelUuid; /** * System private API for talking with the Bluetooth service. * * {@hide} */ -interface IBluetoothDevice +interface IBluetooth { boolean isEnabled(); int getBluetoothState(); @@ -33,28 +34,16 @@ interface IBluetoothDevice String getAddress(); String getName(); boolean setName(in String name); - String getVersion(); - String getRevision(); - String getManufacturer(); - String getCompany(); int getScanMode(); - boolean setScanMode(int mode); + boolean setScanMode(int mode, int duration); int getDiscoverableTimeout(); boolean setDiscoverableTimeout(int timeout); - boolean startDiscovery(boolean resolveNames); + boolean startDiscovery(); boolean cancelDiscovery(); boolean isDiscovering(); - boolean startPeriodicDiscovery(); - boolean stopPeriodicDiscovery(); - boolean isPeriodicDiscovery(); - String[] listRemoteDevices(); - - String[] listAclConnections(); - boolean isAclConnected(in String address); - boolean disconnectRemoteDeviceAcl(in String address); boolean createBond(in String address); boolean cancelBondProcess(in String address); @@ -63,16 +52,19 @@ interface IBluetoothDevice int getBondState(in String address); String getRemoteName(in String address); - String getRemoteVersion(in String address); - String getRemoteRevision(in String address); int getRemoteClass(in String address); - String getRemoteManufacturer(in String address); - String getRemoteCompany(in String address); - boolean getRemoteServiceChannel(in String address, int uuid16, in IBluetoothDeviceCallback callback); - byte[] getRemoteFeatures(in String adddress); - String lastSeen(in String address); - String lastUsed(in String address); + ParcelUuid[] getRemoteUuids(in String address); + boolean fetchRemoteUuids(in String address, in ParcelUuid uuid, in IBluetoothCallback callback); + int getRemoteServiceChannel(in String address, in ParcelUuid uuid); boolean setPin(in String address, in byte[] pin); - boolean cancelPin(in String address); + boolean setPasskey(in String address, int passkey); + boolean setPairingConfirmation(in String address, boolean confirm); + boolean cancelPairingUserInput(in String address); + + boolean setTrust(in String address, in boolean value); + boolean getTrustState(in String address); + + int addRfcommServiceRecord(in String serviceName, in ParcelUuid uuid, int channel, IBinder b); + void removeServiceRecord(int handle); } diff --git a/core/java/android/bluetooth/IBluetoothA2dp.aidl b/core/java/android/bluetooth/IBluetoothA2dp.aidl index 55ff27f97ec036a550722e55d7fbe42d86d19002..002cf4efff61bcc16d690573bfdf991c3cc96182 100644 --- a/core/java/android/bluetooth/IBluetoothA2dp.aidl +++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl @@ -16,16 +16,20 @@ package android.bluetooth; +import android.bluetooth.BluetoothDevice; + /** * System private API for Bluetooth A2DP service * * {@hide} */ interface IBluetoothA2dp { - int connectSink(in String address); - int disconnectSink(in String address); - List listConnectedSinks(); - int getSinkState(in String address); - int setSinkPriority(in String address, int priority); - int getSinkPriority(in String address); + boolean connectSink(in BluetoothDevice device); + boolean disconnectSink(in BluetoothDevice device); + boolean suspendSink(in BluetoothDevice device); + boolean resumeSink(in BluetoothDevice device); + BluetoothDevice[] getConnectedSinks(); // change to Set<> once AIDL supports + int getSinkState(in BluetoothDevice device); + boolean setSinkPriority(in BluetoothDevice device, int priority); + int getSinkPriority(in BluetoothDevice device); } diff --git a/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl b/core/java/android/bluetooth/IBluetoothCallback.aidl similarity index 76% rename from core/java/android/bluetooth/IBluetoothDeviceCallback.aidl rename to core/java/android/bluetooth/IBluetoothCallback.aidl index d05709330b761ff696fa587e196e41e08084c643..8edb3f4c4139b1c124ac6651de652de80f7b4820 100644 --- a/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl +++ b/core/java/android/bluetooth/IBluetoothCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, The Android Open Source Project + * Copyright (C) 2009, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,11 @@ package android.bluetooth; /** + * System private API for Bluetooth service callbacks. + * * {@hide} */ -oneway interface IBluetoothDeviceCallback +interface IBluetoothCallback { - void onGetRemoteServiceChannelResult(in String address, int channel); + void onRfcommChannelFound(int channel); } diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl index 5f42fd6521b7277657423de0cd30f03ebfd77964..6cccd506e2692189d7b9518b5bc32025e7c07139 100644 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -16,6 +16,8 @@ package android.bluetooth; +import android.bluetooth.BluetoothDevice; + /** * System private API for Bluetooth Headset service * @@ -23,13 +25,13 @@ package android.bluetooth; */ interface IBluetoothHeadset { int getState(); - String getHeadsetAddress(); - boolean connectHeadset(in String address); + BluetoothDevice getCurrentHeadset(); + boolean connectHeadset(in BluetoothDevice device); void disconnectHeadset(); - boolean isConnected(in String address); + boolean isConnected(in BluetoothDevice device); boolean startVoiceRecognition(); boolean stopVoiceRecognition(); - boolean setPriority(in String address, int priority); - int getPriority(in String address); + boolean setPriority(in BluetoothDevice device, int priority); + int getPriority(in BluetoothDevice device); int getBatteryUsageHint(); } diff --git a/libs/utils/executablepath_linux.cpp b/core/java/android/bluetooth/IBluetoothPbap.aidl similarity index 64% rename from libs/utils/executablepath_linux.cpp rename to core/java/android/bluetooth/IBluetoothPbap.aidl index b8d2a3d6fbc9733a65212c3a56765dbb25c783ce..7cc77d110e969c25727fa6253b59506b62db98fe 100644 --- a/libs/utils/executablepath_linux.cpp +++ b/core/java/android/bluetooth/IBluetoothPbap.aidl @@ -14,17 +14,19 @@ * limitations under the License. */ -#include -#include -#include -#include -#include +package android.bluetooth; -void executablepath(char exe[PATH_MAX]) -{ - char proc[100]; - sprintf(proc, "/proc/%d/exe", getpid()); - - int err = readlink(proc, exe, PATH_MAX); -} +import android.bluetooth.BluetoothDevice; +/** + * System private API for Bluetooth pbap service + * + * {@hide} + */ +interface IBluetoothPbap { + int getState(); + BluetoothDevice getClient(); + boolean connect(in BluetoothDevice device); + void disconnect(); + boolean isConnected(in BluetoothDevice device); +} diff --git a/core/java/android/bluetooth/RfcommSocket.java b/core/java/android/bluetooth/RfcommSocket.java deleted file mode 100644 index a33263f526121ffacf2a8a3199e8af63d8dfa863..0000000000000000000000000000000000000000 --- a/core/java/android/bluetooth/RfcommSocket.java +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -import java.io.IOException; -import java.io.FileOutputStream; -import java.io.FileInputStream; -import java.io.OutputStream; -import java.io.InputStream; -import java.io.FileDescriptor; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * This class implements an API to the Bluetooth RFCOMM layer. An RFCOMM socket - * is similar to a normal socket in that it takes an address and a port number. - * The difference is of course that the address is a Bluetooth-device address, - * and the port number is an RFCOMM channel. The API allows for the - * establishment of listening sockets via methods - * {@link #bind(String, int) bind}, {@link #listen(int) listen}, and - * {@link #accept(RfcommSocket, int) accept}, as well as for the making of - * outgoing connections with {@link #connect(String, int) connect}, - * {@link #connectAsync(String, int) connectAsync}, and - * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. - * - * After constructing a socket, you need to {@link #create() create} it and then - * {@link #destroy() destroy} it when you are done using it. Both - * {@link #create() create} and {@link #accept(RfcommSocket, int) accept} return - * a {@link java.io.FileDescriptor FileDescriptor} for the actual data. - * Alternatively, you may call {@link #getInputStream() getInputStream} and - * {@link #getOutputStream() getOutputStream} to retrieve the respective streams - * without going through the FileDescriptor. - * - * @hide - */ -public class RfcommSocket { - - /** - * Used by the native implementation of the class. - */ - private int mNativeData; - - /** - * Used by the native implementation of the class. - */ - private int mPort; - - /** - * Used by the native implementation of the class. - */ - private String mAddress; - - /** - * We save the return value of {@link #create() create} and - * {@link #accept(RfcommSocket,int) accept} in this variable, and use it to - * retrieve the I/O streams. - */ - private FileDescriptor mFd; - - /** - * After a call to {@link #waitForAsyncConnect(int) waitForAsyncConnect}, - * if the return value is zero, then, the the remaining time left to wait is - * written into this variable (by the native implementation). It is possible - * that {@link #waitForAsyncConnect(int) waitForAsyncConnect} returns before - * the user-specified timeout expires, which is why we save the remaining - * time in this member variable for the user to retrieve by calling method - * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs}. - */ - private int mTimeoutRemainingMs; - - /** - * Set to true when an asynchronous (nonblocking) connect is in progress. - * {@see #connectAsync(String,int)}. - */ - private boolean mIsConnecting; - - /** - * Set to true after a successful call to {@link #bind(String,int) bind} and - * used for error checking in {@link #listen(int) listen}. Reset to false - * on {@link #destroy() destroy}. - */ - private boolean mIsBound = false; - - /** - * Set to true after a successful call to {@link #listen(int) listen} and - * used for error checking in {@link #accept(RfcommSocket,int) accept}. - * Reset to false on {@link #destroy() destroy}. - */ - private boolean mIsListening = false; - - /** - * Used to store the remaining time after an accept with a non-negative - * timeout returns unsuccessfully. It is possible that a blocking - * {@link #accept(int) accept} may wait for less than the time specified by - * the user, which is why we store the remainder in this member variable for - * it to be retrieved with method - * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs}. - */ - private int mAcceptTimeoutRemainingMs; - - /** - * Maintained by {@link #getInputStream() getInputStream}. - */ - protected FileInputStream mInputStream; - - /** - * Maintained by {@link #getOutputStream() getOutputStream}. - */ - protected FileOutputStream mOutputStream; - - private native void initializeNativeDataNative(); - - /** - * Constructor. - */ - public RfcommSocket() { - initializeNativeDataNative(); - } - - private native void cleanupNativeDataNative(); - - /** - * Called by the GC to clean up the native data that we set up when we - * construct the object. - */ - protected void finalize() throws Throwable { - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - - private native static void classInitNative(); - - static { - classInitNative(); - } - - /** - * Creates a socket. You need to call this method before performing any - * other operation on a socket. - * - * @return FileDescriptor for the data stream. - * @throws IOException - * @see #destroy() - */ - public FileDescriptor create() throws IOException { - if (mFd == null) { - mFd = createNative(); - } - if (mFd == null) { - throw new IOException("socket not created"); - } - return mFd; - } - - private native FileDescriptor createNative(); - - /** - * Destroys a socket created by {@link #create() create}. Call this - * function when you no longer use the socket in order to release the - * underlying OS resources. - * - * @see #create() - */ - public void destroy() { - synchronized (this) { - destroyNative(); - mFd = null; - mIsBound = false; - mIsListening = false; - } - } - - private native void destroyNative(); - - /** - * Returns the {@link java.io.FileDescriptor FileDescriptor} of the socket. - * - * @return the FileDescriptor - * @throws IOException - * when the socket has not been {@link #create() created}. - */ - public FileDescriptor getFileDescriptor() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - return mFd; - } - - /** - * Retrieves the input stream from the socket. Alternatively, you can do - * that from the FileDescriptor returned by {@link #create() create} or - * {@link #accept(RfcommSocket, int) accept}. - * - * @return InputStream - * @throws IOException - * if you have not called {@link #create() create} on the - * socket. - */ - public InputStream getInputStream() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - - synchronized (this) { - if (mInputStream == null) { - mInputStream = new FileInputStream(mFd); - } - - return mInputStream; - } - } - - /** - * Retrieves the output stream from the socket. Alternatively, you can do - * that from the FileDescriptor returned by {@link #create() create} or - * {@link #accept(RfcommSocket, int) accept}. - * - * @return OutputStream - * @throws IOException - * if you have not called {@link #create() create} on the - * socket. - */ - public OutputStream getOutputStream() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - - synchronized (this) { - if (mOutputStream == null) { - mOutputStream = new FileOutputStream(mFd); - } - - return mOutputStream; - } - } - - /** - * Starts a blocking connect to a remote RFCOMM socket. It takes the address - * of a device and the RFCOMM channel (port) to which to connect. - * - * @param address - * is the Bluetooth address of the remote device. - * @param port - * is the RFCOMM channel - * @return true on success, false on failure - * @throws IOException - * if {@link #create() create} has not been called. - * @see #connectAsync(String, int) - */ - public boolean connect(String address, int port) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return connectNative(address, port); - } - } - - private native boolean connectNative(String address, int port); - - /** - * Starts an asynchronous (nonblocking) connect to a remote RFCOMM socket. - * It takes the address of the device to connect to, as well as the RFCOMM - * channel (port). On successful return (return value is true), you need to - * call method {@link #waitForAsyncConnect(int) waitForAsyncConnect} to - * block for up to a specified number of milliseconds while waiting for the - * asyncronous connect to complete. - * - * @param address - * of remote device - * @param port - * the RFCOMM channel - * @return true when the asynchronous connect has successfully started, - * false if there was an error. - * @throws IOException - * is you have not called {@link #create() create} - * @see #waitForAsyncConnect(int) - * @see #getRemainingAsyncConnectWaitingTimeMs() - * @see #connect(String, int) - */ - public boolean connectAsync(String address, int port) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - mIsConnecting = connectAsyncNative(address, port); - return mIsConnecting; - } - } - - private native boolean connectAsyncNative(String address, int port); - - /** - * Interrupts an asynchronous connect in progress. This method does nothing - * when there is no asynchronous connect in progress. - * - * @throws IOException - * if you have not called {@link #create() create}. - * @see #connectAsync(String, int) - */ - public void interruptAsyncConnect() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (mIsConnecting) { - mIsConnecting = !interruptAsyncConnectNative(); - } - } - } - - private native boolean interruptAsyncConnectNative(); - - /** - * Tells you whether there is an asynchronous connect in progress. This - * method returns an undefined value when there is a synchronous connect in - * progress. - * - * @return true if there is an asyc connect in progress, false otherwise - * @see #connectAsync(String, int) - */ - public boolean isConnecting() { - return mIsConnecting; - } - - /** - * Blocks for a specified amount of milliseconds while waiting for an - * asynchronous connect to complete. Returns an integer value to indicate - * one of the following: the connect succeeded, the connect is still in - * progress, or the connect failed. It is possible for this method to block - * for less than the time specified by the user, and still return zero - * (i.e., async connect is still in progress.) For this reason, if the - * return value is zero, you need to call method - * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs} - * to retrieve the remaining time. - * - * @param timeoutMs - * the time to block while waiting for the async connect to - * complete. - * @return a positive value if the connect succeeds; zero, if the connect is - * still in progress, and a negative value if the connect failed. - * - * @throws IOException - * @see #getRemainingAsyncConnectWaitingTimeMs() - * @see #connectAsync(String, int) - */ - public int waitForAsyncConnect(int timeoutMs) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - int ret = waitForAsyncConnectNative(timeoutMs); - if (ret != 0) { - mIsConnecting = false; - } - return ret; - } - } - - private native int waitForAsyncConnectNative(int timeoutMs); - - /** - * Returns the number of milliseconds left to wait after the last call to - * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. - * - * It is possible that waitForAsyncConnect() waits for less than the time - * specified by the user, and still returns zero (i.e., async connect is - * still in progress.) For this reason, if the return value is zero, you - * need to call this method to retrieve the remaining time before you call - * waitForAsyncConnect again. - * - * @return the remaining timeout in milliseconds. - * @see #waitForAsyncConnect(int) - * @see #connectAsync(String, int) - */ - public int getRemainingAsyncConnectWaitingTimeMs() { - return mTimeoutRemainingMs; - } - - /** - * Shuts down both directions on a socket. - * - * @return true on success, false on failure; if the return value is false, - * the socket might be left in a patially shut-down state (i.e. one - * direction is shut down, but the other is still open.) In this - * case, you should {@link #destroy() destroy} and then - * {@link #create() create} the socket again. - * @throws IOException - * is you have not caled {@link #create() create}. - * @see #shutdownInput() - * @see #shutdownOutput() - */ - public boolean shutdown() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (shutdownNative(true)) { - return shutdownNative(false); - } - - return false; - } - } - - /** - * Shuts down the input stream of the socket, but leaves the output stream - * in its current state. - * - * @return true on success, false on failure - * @throws IOException - * is you have not called {@link #create() create} - * @see #shutdown() - * @see #shutdownOutput() - */ - public boolean shutdownInput() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return shutdownNative(true); - } - } - - /** - * Shut down the output stream of the socket, but leaves the input stream in - * its current state. - * - * @return true on success, false on failure - * @throws IOException - * is you have not called {@link #create() create} - * @see #shutdown() - * @see #shutdownInput() - */ - public boolean shutdownOutput() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return shutdownNative(false); - } - } - - private native boolean shutdownNative(boolean shutdownInput); - - /** - * Tells you whether a socket is connected to another socket. This could be - * for input or output or both. - * - * @return true if connected, false otherwise. - * @see #isInputConnected() - * @see #isOutputConnected() - */ - public boolean isConnected() { - return isConnectedNative() > 0; - } - - /** - * Determines whether input is connected (i.e., whether you can receive data - * on this socket.) - * - * @return true if input is connected, false otherwise. - * @see #isConnected() - * @see #isOutputConnected() - */ - public boolean isInputConnected() { - return (isConnectedNative() & 1) != 0; - } - - /** - * Determines whether output is connected (i.e., whether you can send data - * on this socket.) - * - * @return true if output is connected, false otherwise. - * @see #isConnected() - * @see #isInputConnected() - */ - public boolean isOutputConnected() { - return (isConnectedNative() & 2) != 0; - } - - private native int isConnectedNative(); - - /** - * Binds a listening socket to the local device, or a non-listening socket - * to a remote device. The port is automatically selected as the first - * available port in the range 12 to 30. - * - * NOTE: Currently we ignore the device parameter and always bind the socket - * to the local device, assuming that it is a listening socket. - * - * TODO: Use bind(0) in native code to have the kernel select an unused - * port. - * - * @param device - * Bluetooth address of device to bind to (currently ignored). - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} - * @see #listen(int) - * @see #accept(RfcommSocket,int) - */ - public boolean bind(String device) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - for (int port = 12; port <= 30; port++) { - if (bindNative(device, port)) { - mIsBound = true; - return true; - } - } - mIsBound = false; - return false; - } - - /** - * Binds a listening socket to the local device, or a non-listening socket - * to a remote device. - * - * NOTE: Currently we ignore the device parameter and always bind the socket - * to the local device, assuming that it is a listening socket. - * - * @param device - * Bluetooth address of device to bind to (currently ignored). - * @param port - * RFCOMM channel to bind socket to. - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} - * @see #listen(int) - * @see #accept(RfcommSocket,int) - */ - public boolean bind(String device, int port) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - mIsBound = bindNative(device, port); - return mIsBound; - } - - private native boolean bindNative(String device, int port); - - /** - * Starts listening for incoming connections on this socket, after it has - * been bound to an address and RFCOMM channel with - * {@link #bind(String,int) bind}. - * - * @param backlog - * the number of pending incoming connections to queue for - * {@link #accept(RfcommSocket, int) accept}. - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} or if the - * socket has not been bound to a device and RFCOMM channel. - */ - public boolean listen(int backlog) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (!mIsBound) { - throw new IOException("socket not bound"); - } - mIsListening = listenNative(backlog); - return mIsListening; - } - - private native boolean listenNative(int backlog); - - /** - * Accepts incoming-connection requests for a listening socket bound to an - * RFCOMM channel. The user may provide a time to wait for an incoming - * connection. - * - * Note that this method may return null (i.e., no incoming connection) - * before the user-specified timeout expires. For this reason, on a null - * return value, you need to call - * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs} - * in order to see how much time is left to wait, before you call this - * method again. - * - * @param newSock - * is set to the new socket that is created as a result of a - * successful accept. - * @param timeoutMs - * time (in milliseconds) to block while waiting to an - * incoming-connection request. A negative value is an infinite - * wait. - * @return FileDescriptor of newSock on success, null on failure. Failure - * occurs if the timeout expires without a successful connect. - * @throws IOException - * if the socket has not been {@link #create() create}ed, is - * not bound, or is not a listening socket. - * @see #bind(String, int) - * @see #listen(int) - * @see #getRemainingAcceptWaitingTimeMs() - */ - public FileDescriptor accept(RfcommSocket newSock, int timeoutMs) - throws IOException { - synchronized (newSock) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (mIsListening == false) { - throw new IOException("not listening on socket"); - } - newSock.mFd = acceptNative(newSock, timeoutMs); - return newSock.mFd; - } - } - - /** - * Returns the number of milliseconds left to wait after the last call to - * {@link #accept(RfcommSocket, int) accept}. - * - * Since accept() may return null (i.e., no incoming connection) before the - * user-specified timeout expires, you need to call this method in order to - * see how much time is left to wait, and wait for that amount of time - * before you call accept again. - * - * @return the remaining time, in milliseconds. - */ - public int getRemainingAcceptWaitingTimeMs() { - return mAcceptTimeoutRemainingMs; - } - - private native FileDescriptor acceptNative(RfcommSocket newSock, - int timeoutMs); - - /** - * Get the port (rfcomm channel) associated with this socket. - * - * This is only valid if the port has been set via a successful call to - * {@link #bind(String, int)}, {@link #connect(String, int)} - * or {@link #connectAsync(String, int)}. This can be checked - * with {@link #isListening()} and {@link #isConnected()}. - * @return Port (rfcomm channel) - */ - public int getPort() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (!mIsListening && !isConnected()) { - throw new IOException("not listening or connected on socket"); - } - return mPort; - } - - /** - * Return true if this socket is listening ({@link #listen(int)} - * has been called successfully). - */ - public boolean isListening() { - return mIsListening; - } -} diff --git a/core/java/android/bluetooth/ScoSocket.java b/core/java/android/bluetooth/ScoSocket.java index 1bf786ffd3adc92d79818349cba7bfec6a66e1bf..116310ad9d7d821d22e5216726310ff0bec67652 100644 --- a/core/java/android/bluetooth/ScoSocket.java +++ b/core/java/android/bluetooth/ScoSocket.java @@ -87,7 +87,7 @@ public class ScoSocket { * Does not block. */ public synchronized boolean connect(String address) { - if (VDBG) log("connect() " + this); + if (DBG) log("connect() " + this); if (mState != STATE_READY) { if (DBG) log("connect(): Bad state"); return false; diff --git a/core/java/android/bluetooth/package.html b/core/java/android/bluetooth/package.html index 79abf0cb4a04635b7c3f4896aa13b370a79c58de..4f0755e715083801e7af4c95c88ec3a2173e5ab8 100644 --- a/core/java/android/bluetooth/package.html +++ b/core/java/android/bluetooth/package.html @@ -1,13 +1,109 @@ -Provides classes that manage Bluetooth functionality on the device. -

    -The Bluetooth APIs allow applications can connect and disconnect headsets, or scan -for other kinds of Bluetooth devices and pair them. Further control includes the -ability to write and modify the local Service Discovery Protocol (SDP) database, -query the SDP database of other Bluetooth devices, establish RFCOMM -channels/sockets on Android, and connect to specified sockets on other devices. +Provides classes that manage Bluetooth functionality, such as scanning for +devices, connecting with devices, and managing data transfer between devices. + +

    The Bluetooth APIs let applications:

    +
      +
    • Scan for other Bluetooth devices
    • +
    • Query the local Bluetooth adapter for paired Bluetooth devices
    • +
    • Establish RFCOMM channels/sockets
    • +
    • Connect to specified sockets on other devices
    • +
    • Transfer data to and from other devices
    • +
    + +

    Note: +To perform Bluetooth communication using these APIs, an application must +declare the {@link android.Manifest.permission#BLUETOOTH} permission. Some +additional functionality, such as requesting device discovery and +pairing also requires the {@link android.Manifest.permission#BLUETOOTH_ADMIN} +permission.

    -

    Remember, not all Android devices are guaranteed to have Bluetooth functionality.

    + +

    Overview

    + +

    Here's a basic introduction to the Bluetooth classes:

    +
    +
    {@link android.bluetooth.BluetoothAdapter}
    +
    This represents the local Bluetooth adapter, which is essentially the + entry-point to performing any interaction with Bluetooth. With it, you can + discover other Bluetooth devices, query a list of bonded (paired) devices, + initialize a {@link android.bluetooth.BluetoothDevice} using a known MAC + address, and create a {@link android.bluetooth.BluetoothServerSocket} to + listen for communications from other devices.
    + +
    {@link android.bluetooth.BluetoothDevice}
    +
    This represents a remote Bluetooth device. Use this to request a + connection with a remote device through a + {@link android.bluetooth.BluetoothSocket} + or query information about the device such as its name, address, class, and + bonding state.
    + +
    {@link android.bluetooth.BluetoothSocket}
    +
    This represents the interface for a Bluetooth socket + (similar to a TCP client-side {@link java.net.Socket}). This is the + connection point that allows an app to transfer data with another Bluetooth + device via {@link java.io.InputStream} and {@link java.io.OutputStream}.
    +
    {@link android.bluetooth.BluetoothServerSocket}
    + +
    This represents an open server socket that listens for incoming requests + (similar to a TCP server-side {@link java.net.ServerSocket}). + When attempting to connect two Android devices, one device will need to open + a server socket with this class. When a connection is accepted, a new + {@link android.bluetooth.BluetoothSocket} will be returned, + which can be used to manage the connection and transfer data.
    + +
    {@link android.bluetooth.BluetoothClass}
    +
    This represents the Bluetooth class for a device which describes general + characteristics and capabilities of a device. This class and its subclasses + don't provide any actual functionality. The sub-classes are entirely composed + of constants for the device and service class definitions.
    +
    + + +

    Example Procedure

    + +

    For example, here's an pseudo-code procedure for discovering and +connecting a remote device, and transfering data:

    + +
      +
    1. Register a {@link android.content.BroadcastReceiver} that accepts the + {@link android.bluetooth.BluetoothDevice#ACTION_FOUND} Intent.
    2. +
    3. Call {@link android.bluetooth.BluetoothAdapter#getDefaultAdapter} to + retrieve the Android system's local + {@link android.bluetooth.BluetoothAdapter}.
    4. +
    5. Call {@link android.bluetooth.BluetoothAdapter#startDiscovery() + BluetoothAdapter.startDiscovery()} to scan for local devices. This is where + the BroadcastReceiver comes in; Android now scans for devices and will + broadcast the {@link android.bluetooth.BluetoothDevice#ACTION_FOUND} Intent + for each remote device discovered. The + {@link android.content.BroadcastReceiver} + you created will receive each Intent.
    6. +
    7. The {@link android.bluetooth.BluetoothDevice#ACTION_FOUND} Intent + includes the {@link android.bluetooth.BluetoothDevice#EXTRA_DEVICE} + Parcelable extra, which is a {@link android.bluetooth.BluetoothDevice} + object. Extract this from the Intent and call + {@link android.bluetooth.BluetoothDevice#createRfcommSocketToServiceRecord(java.util.UUID) + BluetoothDevice.createRfcommSocketToServiceRecord()} + to open a {@link android.bluetooth.BluetoothSocket} with a chosen + remote device.
    8. +
    9. Call {@link android.bluetooth.BluetoothSocket#connect() + BluetoothSocket.connect()} to connect with the remote device.
    10. +
    11. When successfully connected, call + {@link android.bluetooth.BluetoothSocket#getInputStream() + BluetoothSocket.getInputStream()} and/or + {@link android.bluetooth.BluetoothSocket#getOutputStream() + BluetoothSocket.getOutputStream()} to retreive an + {@link java.io.InputStream} and {@link java.io.OutputStream}, respectively, + which are hooked into the socket.
    12. +
    13. Use {@link java.io.InputStream#read(byte[]) InputStream.read()} and + {@link java.io.OutputStream#write(byte[]) OutputStream.write()} to transfer + data.
    14. +
    + + + +

    Note: +Not all Android devices are guaranteed to have Bluetooth functionality.

    diff --git a/core/java/android/content/AbstractCursorEntityIterator.java b/core/java/android/content/AbstractCursorEntityIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..a804f3c43a834e7ff957da5e1fb4e5ac76875871 --- /dev/null +++ b/core/java/android/content/AbstractCursorEntityIterator.java @@ -0,0 +1,121 @@ +package android.content; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; + +/** + * An abstract class that makes it easy to implement an EntityIterator over a cursor. + * The user must implement {@link #newEntityFromCursorLocked}, which runs inside of a + * database transaction. + * @hide + */ +public abstract class AbstractCursorEntityIterator implements EntityIterator { + private final Cursor mEntityCursor; + private final SQLiteDatabase mDb; + private volatile Entity mNextEntity; + private volatile boolean mIsClosed; + + public AbstractCursorEntityIterator(SQLiteDatabase db, Cursor entityCursor) { + mEntityCursor = entityCursor; + mDb = db; + mNextEntity = null; + mIsClosed = false; + } + + /** + * If there are entries left in the cursor then advance the cursor and use the new row to + * populate mNextEntity. If the cursor is at the end or if advancing it causes the cursor + * to become at the end then set mEntityCursor to null. If newEntityFromCursor returns null + * then continue advancing until it either returns a non-null Entity or the cursor reaches + * the end. + */ + private void fillEntityIfAvailable() { + while (mNextEntity == null) { + if (!mEntityCursor.moveToNext()) { + // the cursor is at then end, bail out + return; + } + // This may return null if newEntityFromCursor is not able to create an entity + // from the current cursor position. In that case this method will loop and try + // the next cursor position + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + } + mDb.beginTransaction(); + try { + int position = mEntityCursor.getPosition(); + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + int newPosition = mEntityCursor.getPosition(); + if (newPosition != position) { + throw new IllegalStateException("the cursor position changed during the call to" + + "newEntityFromCursorLocked, from " + position + " to " + newPosition); + } + } finally { + mDb.endTransaction(); + } + } + + /** + * Checks if there are more Entities accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return true if the call to next() will return an Entity. + */ + public boolean hasNext() { + if (mIsClosed) { + throw new IllegalStateException("calling hasNext() when the iterator is closed"); + } + fillEntityIfAvailable(); + return mNextEntity != null; + } + + /** + * Returns the next Entity that is accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return the next Entity that is accessible via this iterator + */ + public Entity next() { + if (mIsClosed) { + throw new IllegalStateException("calling next() when the iterator is closed"); + } + if (!hasNext()) { + throw new IllegalStateException("you may only call next() if hasNext() is true"); + } + + try { + return mNextEntity; + } finally { + mNextEntity = null; + } + } + + public void reset() throws RemoteException { + if (mIsClosed) { + throw new IllegalStateException("calling reset() when the iterator is closed"); + } + mEntityCursor.moveToPosition(-1); + mNextEntity = null; + } + + /** + * Closes this iterator making it invalid. If is invalid for the user to call any public + * method on the iterator once it has been closed. + */ + public void close() { + if (mIsClosed) { + throw new IllegalStateException("closing when already closed"); + } + mIsClosed = true; + mEntityCursor.close(); + } + + /** + * Returns a new Entity from the current cursor position. This is called from within a + * database transaction. If a new entity cannot be created from this cursor position (e.g. + * if the row that is referred to no longer exists) then this may return null. The cursor + * is guaranteed to be pointing to a valid row when this call is made. The implementation + * of newEntityFromCursorLocked is not allowed to change the position of the cursor. + * @param cursor from where to read the data for the Entity + * @return an Entity that corresponds to the current cursor position or null + */ + public abstract Entity newEntityFromCursorLocked(Cursor cursor); +} diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index 249d9babd8a7470b10e693151802c328369bdf77..fbe3548e8a02521c72e1da6461eb31974ee8f423 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -4,8 +4,9 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase; import android.database.Cursor; import android.net.Uri; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.OnAccountsUpdateListener; +import android.accounts.Account; +import android.accounts.AccountManager; import android.provider.SyncConstValue; import android.util.Config; import android.util.Log; @@ -14,9 +15,12 @@ import android.text.TextUtils; import java.util.Collections; import java.util.Map; -import java.util.HashMap; import java.util.Vector; import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; + +import com.google.android.collect.Maps; /** * A specialization of the ContentProvider that centralizes functionality @@ -32,26 +36,30 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro private final String mDatabaseName; private final int mDatabaseVersion; private final Uri mContentUri; - private AccountMonitor mAccountMonitor; /** the account set in the last call to onSyncStart() */ - private String mSyncingAccount; + private Account mSyncingAccount; private SyncStateContentProviderHelper mSyncState = null; - private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT}; + private static final String[] sAccountProjection = + new String[] {SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT_TYPE}; private boolean mIsTemporary; private AbstractTableMerger mCurrentMerger = null; private boolean mIsMergeCancelled = false; - private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?"; + private static final String SYNC_ACCOUNT_WHERE_CLAUSE = + SyncConstValue._SYNC_ACCOUNT + "=? AND " + SyncConstValue._SYNC_ACCOUNT_TYPE + "=?"; protected boolean isTemporary() { return mIsTemporary; } + private final ThreadLocal mApplyingBatch = new ThreadLocal(); + private final ThreadLocal> mPendingBatchNotifications = new ThreadLocal>(); + /** * Indicates whether or not this ContentProvider contains a full * set of data or just diffs. This knowledge comes in handy when @@ -127,13 +135,16 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro public void onCreate(SQLiteDatabase db) { bootstrapDatabase(db); mSyncState.createDatabase(db); + ContentResolver.requestSync(null /* all accounts */, + mContentUri.getAuthority(), new Bundle()); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (!upgradeDatabase(db, oldVersion, newVersion)) { mSyncState.discardSyncData(db, null /* all accounts */); - getContext().getContentResolver().startSync(mContentUri, new Bundle()); + ContentResolver.requestSync(null /* all accounts */, + mContentUri.getAuthority(), new Bundle()); } } @@ -150,23 +161,36 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName); mSyncState = new SyncStateContentProviderHelper(mOpenHelper); - - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - // Some providers override onAccountsChanged(); give them a database to work with. - mDb = mOpenHelper.getWritableDatabase(); - onAccountsChanged(accounts); - TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter(); - if (syncAdapter != null) { - syncAdapter.onAccountsChanged(accounts); - } - } - }; - mAccountMonitor = new AccountMonitor(getContext(), listener); + AccountManager.get(getContext()).addOnAccountsUpdatedListener( + new OnAccountsUpdateListener() { + public void onAccountsUpdated(Account[] accounts) { + // Some providers override onAccountsChanged(); give them a database to + // work with. + mDb = mOpenHelper.getWritableDatabase(); + // Only call onAccountsChanged on GAIA accounts; otherwise, the contacts and + // calendar providers will choke as they try to sync unknown accounts with + // AbstractGDataSyncAdapter, which will put acore into a crash loop + ArrayList gaiaAccounts = new ArrayList(); + for (Account acct: accounts) { + if (acct.type.equals("com.google")) { + gaiaAccounts.add(acct); + } + } + accounts = new Account[gaiaAccounts.size()]; + int i = 0; + for (Account acct: gaiaAccounts) { + accounts[i++] = acct; + } + onAccountsChanged(accounts); + TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); + if (syncAdapter != null) { + syncAdapter.onAccountsChanged(accounts); + } + } + }, null /* handler */, true /* updateImmediately */); return true; } - /** * Get a non-persistent instance of this content provider. * You must call {@link #close} on the returned @@ -236,147 +260,117 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro return Collections.emptyList(); } - /** - *

    - * Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction(). - * {@link #endTransaction} MUST be called after calling this method. - * Those methods should be used like this: - *

    - * - *
    -     * boolean successful = false;
    -     * beginTransaction();
    -     * try {
    -     *     // Do something related to mDb
    -     *     successful = true;
    -     *     return ret;
    -     * } finally {
    -     *     endTransaction(successful);
    -     * }
    -     * 
    - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void beginTransaction() { + @Override + public final int update(final Uri url, final ContentValues values, + final String selection, final String[] selectionArgs) { mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); - } - - /** - *

    - * Call mDb.endTransaction(). If successful is true, try to call - * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). - * This method MUST be used with {@link #beginTransaction()}. - *

    - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void endTransaction(boolean successful) { + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - if (successful) { - // setTransactionSuccessful() must be called just once during opening the - // transaction. - mDb.setTransactionSuccessful(); + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().update( + url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; } - } finally { - mDb.endTransaction(); - } - } - @Override - public final int update(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); - try { - int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs); - successful = true; - return ret; + int result = updateInternal(url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); - } - } - - /** - * @hide - */ - public final int nonTransactionalUpdate(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().update( - uri, values, selection, selectionArgs); - return numRows; - } - - int result = updateInternal(uri, values, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); + if (notApplyingBatch) { + mDb.endTransaction(); + } } - - return result; } @Override - public final int delete(final Uri uri, final String selection, + public final int delete(final Uri url, final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - int ret = nonTransactionalDelete(uri, selection, selectionArgs); - successful = true; - return ret; + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; + } + int result = deleteInternal(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); + if (notApplyingBatch) { + mDb.endTransaction(); + } } } - /** - * @hide - */ - public final int nonTransactionalDelete(final Uri uri, final String selection, - final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs); - return numRows; - } - int result = deleteInternal(uri, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); } @Override - public final Uri insert(final Uri uri, final ContentValues values) { - boolean successful = false; - beginTransaction(); - try { - Uri ret = nonTransactionalInsert(uri, values); - successful = true; - return ret; - } finally { - endTransaction(successful); + public final Uri insert(final Uri url, final ContentValues values) { + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); } - } - - /** - * @hide - */ - public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) { - if (isTemporary() && mSyncState.matches(uri)) { - Uri result = mSyncState.asContentProvider().insert(uri, values); + try { + if (isTemporary() && mSyncState.matches(url)) { + Uri result = mSyncState.asContentProvider().insert(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return result; + } + Uri result = insertInternal(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result != null) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } return result; + } finally { + if (notApplyingBatch) { + mDb.endTransaction(); + } } - Uri result = insertInternal(uri, values); - if (!isTemporary() && result != null) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; } @Override @@ -410,6 +404,92 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro return completed; } + /** + *

    + * Start batch transaction. {@link #endTransaction} MUST be called after + * calling this method. Those methods should be used like this: + *

    + * + *
    +     * boolean successful = false;
    +     * beginBatch()
    +     * try {
    +     *     // Do something related to mDb
    +     *     successful = true;
    +     *     return ret;
    +     * } finally {
    +     *     endBatch(successful);
    +     * }
    +     * 
    + * + * @hide This method should be used only when {@link ContentProvider#applyBatch} is not enough and must be + * used with {@link #endBatch}. + * e.g. If returned value has to be used during one transaction, this method might be useful. + */ + public final void beginBatch() { + // initialize if this is the first time this thread has applied a batch + if (mApplyingBatch.get() == null) { + mApplyingBatch.set(false); + mPendingBatchNotifications.set(new HashSet()); + } + + if (applyingBatch()) { + throw new IllegalStateException( + "applyBatch is not reentrant but mApplyingBatch is already set"); + } + SQLiteDatabase db = getDatabase(); + db.beginTransaction(); + boolean successful = false; + try { + mApplyingBatch.set(true); + successful = true; + } finally { + if (!successful) { + // Something unexpected happened. We must call endTransaction() at least. + db.endTransaction(); + } + } + } + + /** + *

    + * Finish batch transaction. If "successful" is true, try to call + * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). + * This method MUST be used with {@link #beginBatch()}. + *

    + * + * @hide This method must be used with {@link #beginTransaction} + */ + public final void endBatch(boolean successful) { + try { + if (successful) { + // setTransactionSuccessful() must be called just once during opening the + // transaction. + mDb.setTransactionSuccessful(); + } + } finally { + mApplyingBatch.set(false); + getDatabase().endTransaction(); + for (Uri url : mPendingBatchNotifications.get()) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + } + } + + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + boolean successful = false; + beginBatch(); + try { + ContentProviderResult[] results = super.applyBatch(operations); + successful = true; + return results; + } finally { + endBatch(successful); + } + } + /** * Check if changes to this URI can be syncable changes. * @param uri the URI of the resource that was changed @@ -437,8 +517,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * @param context the sync context for the operation * @param account */ - public void onSyncStart(SyncContext context, String account) { - if (TextUtils.isEmpty(account)) { + public void onSyncStart(SyncContext context, Account account) { + if (account == null) { throw new IllegalArgumentException("you passed in an empty account"); } mSyncingAccount = account; @@ -457,7 +537,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * The account of the most recent call to onSyncStart() * @return the account */ - public String getSyncingAccount() { + public Account getSyncingAccount() { return mSyncingAccount; } @@ -568,12 +648,11 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected void onAccountsChanged(String[] accountsArray) { - Map accounts = new HashMap(); - for (String account : accountsArray) { + protected void onAccountsChanged(Account[] accountsArray) { + Map accounts = Maps.newHashMap(); + for (Account account : accountsArray) { accounts.put(account, false); } - accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map tableMap = db.getSyncedTables(); @@ -585,8 +664,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro try { mSyncState.onAccountsChanged(accountsArray); for (String table : tables) { - deleteRowsForRemovedAccounts(accounts, table, - SyncConstValue._SYNC_ACCOUNT); + deleteRowsForRemovedAccounts(accounts, table); } db.setTransactionSuccessful(); } finally { @@ -601,23 +679,23 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected void deleteRowsForRemovedAccounts(Map accounts, - String table, String accountColumnName) { + protected void deleteRowsForRemovedAccounts(Map accounts, String table) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(table, sAccountProjection, null, null, - accountColumnName, null, null); + "_sync_account, _sync_account_type", null, null); try { while (c.moveToNext()) { - String account = c.getString(0); - if (TextUtils.isEmpty(account)) { + String accountName = c.getString(0); + String accountType = c.getString(1); + if (TextUtils.isEmpty(accountName)) { continue; } + Account account = new Account(accountName, accountType); if (!accounts.containsKey(account)) { int numDeleted; - numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account}); + numDeleted = db.delete(table, "_sync_account=? AND _sync_account_type=?", + new String[]{account.name, account.type}); if (Config.LOGV) { Log.v(TAG, "deleted " + numDeleted + " records from table " + table @@ -634,7 +712,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public void wipeAccount(String account) { + public void wipeAccount(Account account) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map tableMap = db.getSyncedTables(); ArrayList tables = new ArrayList(); @@ -649,7 +727,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro // remove the data in the synced tables for (String table : tables) { - db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account}); + db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, + new String[]{account.name, account.type}); } db.setTransactionSuccessful(); } finally { @@ -660,14 +739,14 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(String account) { + public byte[] readSyncDataBytes(Account account) { return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account); } /** * Sets the SyncData bytes for the given account. The byte array may be null. */ - public void writeSyncDataBytes(String account, byte[] data) { + public void writeSyncDataBytes(Account account, byte[] data) { mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); } } diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java index 9f609a390ba6362af9966816137cf16d4eaed86a..9545fd7f1a74b4aa18512b349dd1dfe2b8f8e5e4 100644 --- a/core/java/android/content/AbstractTableMerger.java +++ b/core/java/android/content/AbstractTableMerger.java @@ -25,6 +25,7 @@ import android.provider.BaseColumns; import static android.provider.SyncConstValue.*; import android.text.TextUtils; import android.util.Log; +import android.accounts.Account; /** * @hide @@ -55,15 +56,17 @@ public abstract class AbstractTableMerger private volatile boolean mIsMergeCancelled; - private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?"; + private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = - _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?"; + _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; private static final String SELECT_UNSYNCED = - "(" + _SYNC_ACCOUNT + " IS NULL OR " + _SYNC_ACCOUNT + "=?) AND " - + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 AND " + "(" + _SYNC_ACCOUNT + " IS NULL OR (" + + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?)) and " + + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 and " + _SYNC_VERSION + " IS NOT NULL))"; public AbstractTableMerger(SQLiteDatabase database, @@ -134,7 +137,7 @@ public abstract class AbstractTableMerger * construct a temporary instance to hold them. */ public void merge(final SyncContext context, - final String account, + final Account account, final SyncableContentProvider serverDiffs, TempProviderSyncResult result, SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { @@ -157,7 +160,7 @@ public abstract class AbstractTableMerger * @hide this is public for testing purposes only */ public void mergeServerDiffs(SyncContext context, - String account, SyncableContentProvider serverDiffs, SyncResult syncResult) { + Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) { boolean diffsArePartial = serverDiffs.getContainsDiffs(); // mark the current rows so that we can distinguish these from new // inserts that occur during the merge @@ -166,342 +169,340 @@ public abstract class AbstractTableMerger mDb.update(mDeletedTable, mSyncMarkValues, null, null); } - // load the local database entries, so we can merge them with the server - final String[] accountSelectionArgs = new String[]{account}; - Cursor localCursor = mDb.query(mTable, syncDirtyProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mTable + "." + _SYNC_ID); - Cursor deletedCursor; - if (mDeletedTable != null) { - deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + Cursor localCursor = null; + Cursor deletedCursor = null; + Cursor diffsCursor = null; + try { + // load the local database entries, so we can merge them with the server + final String[] accountSelectionArgs = new String[]{account.name, account.type}; + localCursor = mDb.query(mTable, syncDirtyProjection, SELECT_MARKED, accountSelectionArgs, null, null, - mDeletedTable + "." + _SYNC_ID); - } else { - deletedCursor = - mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); - } - - // Apply updates and insertions from the server - Cursor diffsCursor = serverDiffs.query(mTableURL, - null, null, null, mTable + "." + _SYNC_ID); - int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); - int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - - String lastSyncId = null; - int diffsCount = 0; - int localCount = 0; - localCursor.moveToFirst(); - deletedCursor.moveToFirst(); - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - localCursor.close(); - deletedCursor.close(); - diffsCursor.close(); - return; - } - mDb.yieldIfContended(); - String serverSyncId = diffsCursor.getString(serverSyncIDColumn); - String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); - long localRowId = 0; - String localSyncVersion = null; - - diffsCount++; - context.setStatusText("Processing " + diffsCount + "/" - + diffsCursor.getCount()); - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + - diffsCount + ", " + serverSyncId); - - if (TRACE) { - if (diffsCount == 10) { - Debug.startMethodTracing("atmtrace"); - } - if (diffsCount == 20) { - Debug.stopMethodTracing(); - } + mTable + "." + _SYNC_ID); + if (mDeletedTable != null) { + deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + SELECT_MARKED, accountSelectionArgs, null, null, + mDeletedTable + "." + _SYNC_ID); + } else { + deletedCursor = + mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); } - boolean conflict = false; - boolean update = false; - boolean insert = false; + // Apply updates and insertions from the server + diffsCursor = serverDiffs.query(mTableURL, + null, null, null, mTable + "." + _SYNC_ID); + int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); + int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); + int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found event with serverSyncID " + serverSyncId); - } - if (TextUtils.isEmpty(serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.e(TAG, "server entry doesn't have a serverSyncID"); + String lastSyncId = null; + int diffsCount = 0; + int localCount = 0; + localCursor.moveToFirst(); + deletedCursor.moveToFirst(); + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; } - continue; - } - - // It is possible that the sync adapter wrote the same record multiple times, - // e.g. if the same record came via multiple feeds. If this happens just ignore - // the duplicate records. - if (serverSyncId.equals(lastSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); + mDb.yieldIfContended(); + String serverSyncId = diffsCursor.getString(serverSyncIDColumn); + String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); + long localRowId = 0; + String localSyncVersion = null; + + diffsCount++; + context.setStatusText("Processing " + diffsCount + "/" + + diffsCursor.getCount()); + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + + diffsCount + ", " + serverSyncId); + + if (TRACE) { + if (diffsCount == 10) { + Debug.startMethodTracing("atmtrace"); + } + if (diffsCount == 20) { + Debug.stopMethodTracing(); + } } - continue; - } - lastSyncId = serverSyncId; - String localSyncID = null; - boolean localSyncDirty = false; + boolean conflict = false; + boolean update = false; + boolean insert = false; - while (!localCursor.isAfterLast()) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "found event with serverSyncID " + serverSyncId); + } + if (TextUtils.isEmpty(serverSyncId)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.e(TAG, "server entry doesn't have a serverSyncID"); + } + continue; } - localCount++; - localSyncID = localCursor.getString(2); - // If the local record doesn't have a _sync_id then - // it is new. Ignore it for now, we will send an insert - // the the server later. - if (TextUtils.isEmpty(localSyncID)) { + // It is possible that the sync adapter wrote the same record multiple times, + // e.g. if the same record came via multiple feeds. If this happens just ignore + // the duplicate records. + if (serverSyncId.equals(lastSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has no _sync_id, ignoring"); + Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); } - localCursor.moveToNext(); - localSyncID = null; continue; } + lastSyncId = serverSyncId; - int comp = serverSyncId.compareTo(localSyncID); + String localSyncID = null; + boolean localSyncDirty = false; - // the local DB has a record that the server doesn't have - if (comp > 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is < server _sync_id " + serverSyncId); + while (!localCursor.isAfterLast()) { + if (mIsMergeCancelled) { + return; } - if (diffsArePartial) { - localCursor.moveToNext(); - } else { - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + localCount++; + localSyncID = localCursor.getString(2); + + // If the local record doesn't have a _sync_id then + // it is new. Ignore it for now, we will send an insert + // the the server later. + if (TextUtils.isEmpty(localSyncID)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has no _sync_id, ignoring"); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); + localCursor.moveToNext(); + localSyncID = null; + continue; } - localSyncID = null; - continue; - } - // the server has a record that the local DB doesn't have - if (comp < 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is > server _sync_id " + serverSyncId); + int comp = serverSyncId.compareTo(localSyncID); + + // the local DB has a record that the server doesn't have + if (comp > 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is < server _sync_id " + serverSyncId); + } + if (diffsArePartial) { + localCursor.moveToNext(); + } else { + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); + } + localSyncID = null; + continue; } - localSyncID = null; - } - // the server and the local DB both have this record - if (comp == 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that matches the server _sync_id"); + // the server has a record that the local DB doesn't have + if (comp < 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is > server _sync_id " + serverSyncId); + } + localSyncID = null; } - localSyncDirty = localCursor.getInt(0) != 0; - localRowId = localCursor.getLong(1); - localSyncVersion = localCursor.getString(3); - localCursor.moveToNext(); - } - break; - } + // the server and the local DB both have this record + if (comp == 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that matches the server _sync_id"); + } + localSyncDirty = localCursor.getInt(0) != 0; + localRowId = localCursor.getLong(1); + localSyncVersion = localCursor.getString(3); + localCursor.moveToNext(); + } - // If this record is in the deleted table then update the server version - // in the deleted table, if necessary, and then ignore it here. - // We will send a deletion indication to the server down a - // little further. - if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); + break; } - final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); - if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + + // If this record is in the deleted table then update the server version + // in the deleted table, if necessary, and then ignore it here. + // We will send a deletion indication to the server down a + // little further. + if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " - + serverSyncVersion); + Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); + } + final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); + if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " + + serverSyncVersion); + } + ContentValues values = new ContentValues(); + values.put(_SYNC_VERSION, serverSyncVersion); + mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); } - ContentValues values = new ContentValues(); - values.put(_SYNC_VERSION, serverSyncVersion); - mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + continue; } - continue; - } - // If the _sync_local_id is present in the diffsCursor - // then this record corresponds to a local record that was just - // inserted into the server and the _sync_local_id is the row id - // of the local record. Set these fields so that the next check - // treats this record as an update, which will allow the - // merger to update the record with the server's sync id - if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { - localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the remote record with sync id " + serverSyncId - + " has a local sync id, " + localRowId); + // If the _sync_local_id is present in the diffsCursor + // then this record corresponds to a local record that was just + // inserted into the server and the _sync_local_id is the row id + // of the local record. Set these fields so that the next check + // treats this record as an update, which will allow the + // merger to update the record with the server's sync id + if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { + localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the remote record with sync id " + serverSyncId + + " has a local sync id, " + localRowId); + } + localSyncID = serverSyncId; + localSyncDirty = false; + localSyncVersion = null; } - localSyncID = serverSyncId; - localSyncDirty = false; - localSyncVersion = null; - } - if (!TextUtils.isEmpty(localSyncID)) { - // An existing server item has changed - // If serverSyncVersion is null, there is no edit URL; - // server won't let this change be written. - boolean recordChanged = (localSyncVersion == null) || - (serverSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); + if (!TextUtils.isEmpty(localSyncID)) { + // An existing server item has changed + // If serverSyncVersion is null, there is no edit URL; + // server won't let this change be written. + boolean recordChanged = (localSyncVersion == null) || + (serverSyncVersion == null) || + !serverSyncVersion.equals(localSyncVersion); + if (recordChanged) { + if (localSyncDirty) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + + " conflicts with local _sync_id " + localSyncID + + ", local _id " + localRowId); + } + conflict = true; + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "remote record " + + serverSyncId + + " updates local _sync_id " + + localSyncID + ", local _id " + + localRowId); + } + update = true; } - conflict = true; } else { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); + "Skipping update: localSyncVersion: " + localSyncVersion + + ", serverSyncVersion: " + serverSyncVersion); } - update = true; } } else { + // the local db doesn't know about this record so add it if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "Skipping update: localSyncVersion: " + localSyncVersion + - ", serverSyncVersion: " + serverSyncVersion); + Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); } + insert = true; } - } else { - // the local db doesn't know about this record so add it - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + + if (update) { + updateRow(localRowId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (conflict) { + resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (insert) { + insertRow(serverDiffs, diffsCursor); + syncResult.stats.numInserts++; } - insert = true; } - if (update) { - updateRow(localRowId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (conflict) { - resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (insert) { - insertRow(serverDiffs, diffsCursor); - syncResult.stats.numInserts++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "processed " + diffsCount + " server entries"); } - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "processed " + diffsCount + " server entries"); - } - - // If tombstones aren't in use delete any remaining local rows that - // don't have corresponding server rows. Keep the rows that don't - // have a sync id since those were created locally and haven't been - // synced to the server yet. - if (!diffsArePartial) { - while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; - } - localCount++; - final String localSyncId = localCursor.getString(2); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "deleting local record " + - localCursor.getLong(1) + - " _sync_id " + localSyncId); - } - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + // If tombstones aren't in use delete any remaining local rows that + // don't have corresponding server rows. Keep the rows that don't + // have a sync id since those were created locally and haven't been + // synced to the server yet. + if (!diffsArePartial) { + while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { + if (mIsMergeCancelled) { + return; + } + localCount++; + final String localSyncId = localCursor.getString(2); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "deleting local record " + + localCursor.getLong(1) + + " _sync_id " + localSyncId); + } + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); } + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + + " local entries"); + } finally { + if (diffsCursor != null) diffsCursor.close(); + if (localCursor != null) localCursor.close(); + if (deletedCursor != null) deletedCursor.close(); } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + - " local entries"); - diffsCursor.deactivate(); - localCursor.deactivate(); - deletedCursor.deactivate(); if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server"); // Apply deletions from the server if (mDeletedTableURL != null) { diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null); - - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - diffsCursor.deactivate(); - return; + try { + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + // delete all rows that match each element in the diffsCursor + fullyDeleteMatchingRows(diffsCursor, account, syncResult); + mDb.yieldIfContended(); } - // delete all rows that match each element in the diffsCursor - fullyDeleteMatchingRows(diffsCursor, account, syncResult); - mDb.yieldIfContended(); + } finally { + diffsCursor.close(); } - diffsCursor.deactivate(); } } - private void fullyDeleteMatchingRows(Cursor diffsCursor, String account, + private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account, SyncResult syncResult) { int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn); // delete the rows explicitly so that the delete operation can be overridden - final Cursor c; final String[] selectionArgs; - if (deleteBySyncId) { - selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), account}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, - selectionArgs, null, null, null); - } else { - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, - null, null, null); - } + Cursor c = null; try { + if (deleteBySyncId) { + selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), + account.name, account.type}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, + selectionArgs, null, null, null); + } else { + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); + selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, + null, null, null); + } c.moveToFirst(); while (!c.isAfterLast()) { deleteRow(c); // advances the cursor syncResult.stats.numDeletes++; } } finally { - c.deactivate(); + if (c != null) c.close(); } if (deleteBySyncId && mDeletedTable != null) { mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); @@ -519,43 +520,46 @@ public abstract class AbstractTableMerger * Finds local changes, placing the results in the given result object. * @param temporaryInstanceFactory As an optimization for the case * where there are no client-side diffs, mergeResult may initially - * have no {@link android.content.TempProviderSyncResult#tempContentProvider}. If this is + * have no {@link TempProviderSyncResult#tempContentProvider}. If this is * the first in the sequence of AbstractTableMergers to find * client-side diffs, it will use the given ContentProvider to * create a temporary instance and store its {@link - * ContentProvider} in the mergeResult. + * android.content.ContentProvider} in the mergeResult. * @param account * @param syncResult */ private void findLocalChanges(TempProviderSyncResult mergeResult, - SyncableContentProvider temporaryInstanceFactory, String account, + SyncableContentProvider temporaryInstanceFactory, Account account, SyncResult syncResult) { SyncableContentProvider clientDiffs = mergeResult.tempContentProvider; if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates"); - final String[] accountSelectionArgs = new String[]{account}; + final String[] accountSelectionArgs = new String[]{account.name, account.type}; // Generate the client updates and insertions // Create a cursor for dirty records + long numInsertsOrUpdates = 0; Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs, null, null, null); - long numInsertsOrUpdates = localChangesCursor.getCount(); - while (localChangesCursor.moveToNext()) { - if (mIsMergeCancelled) { - localChangesCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numInsertsOrUpdates = localChangesCursor.getCount(); + while (localChangesCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + cursorRowToContentValues(localChangesCursor, mValues); + mValues.remove("_id"); + DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, + _SYNC_LOCAL_ID); + clientDiffs.insert(mTableURL, mValues); } - mValues.clear(); - cursorRowToContentValues(localChangesCursor, mValues); - mValues.remove("_id"); - DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, - _SYNC_LOCAL_ID); - clientDiffs.insert(mTableURL, mValues); + } finally { + localChangesCursor.close(); } - localChangesCursor.close(); // Generate the client deletions if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); @@ -564,23 +568,25 @@ public abstract class AbstractTableMerger if (mDeletedTable != null) { Cursor deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, - _SYNC_ACCOUNT + "=? AND " + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, + _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND " + + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, null, null, mDeletedTable + "." + _SYNC_ID); - - numDeletedEntries = deletedCursor.getCount(); - while (deletedCursor.moveToNext()) { - if (mIsMergeCancelled) { - deletedCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numDeletedEntries = deletedCursor.getCount(); + while (deletedCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); + clientDiffs.insert(mDeletedTableURL, mValues); } - mValues.clear(); - DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); - clientDiffs.insert(mDeletedTableURL, mValues); + } finally { + deletedCursor.close(); } - deletedCursor.close(); } if (clientDiffs != null) { diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..fb6091a669ac42166f0acdea6835fe7a5de03f24 --- /dev/null +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.accounts.Account; +import android.os.Bundle; +import android.os.Process; +import android.os.NetStat; +import android.os.IBinder; +import android.util.EventLog; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An abstract implementation of a SyncAdapter that spawns a thread to invoke a sync operation. + * If a sync operation is already in progress when a startSync() request is received then an error + * will be returned to the new request and the existing request will be allowed to continue. + * When a startSync() is received and there is no sync operation in progress then a thread + * will be started to run the operation and {@link #onPerformSync} will be invoked on that thread. + * If a cancelSync() is received that matches an existing sync operation then the thread + * that is running that sync operation will be interrupted, which will indicate to the thread + * that the sync has been canceled. + */ +public abstract class AbstractThreadedSyncAdapter { + private final Context mContext; + private final AtomicInteger mNumSyncStarts; + private final ISyncAdapterImpl mISyncAdapterImpl; + + // all accesses to this member variable must be synchronized on mSyncThreadLock + private SyncThread mSyncThread; + private final Object mSyncThreadLock = new Object(); + + /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ + public static final int LOG_SYNC_DETAILS = 2743; + private static final String TAG = "Sync"; + private final boolean mAutoInitialize; + + /** + * Creates an {@link AbstractThreadedSyncAdapter}. + * @param context the {@link android.content.Context} that this is running within. + * @param autoInitialize if true then sync requests that have + * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by + * {@link AbstractThreadedSyncAdapter} by calling + * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it + * is currently set to <0. + */ + public AbstractThreadedSyncAdapter(Context context, boolean autoInitialize) { + mContext = context; + mISyncAdapterImpl = new ISyncAdapterImpl(); + mNumSyncStarts = new AtomicInteger(0); + mSyncThread = null; + mAutoInitialize = autoInitialize; + } + + public Context getContext() { + return mContext; + } + + private class ISyncAdapterImpl extends ISyncAdapter.Stub { + public void startSync(ISyncContext syncContext, String authority, Account account, + Bundle extras) { + final SyncContext syncContextClient = new SyncContext(syncContext); + + boolean alreadyInProgress; + // synchronize to make sure that mSyncThread doesn't change between when we + // check it and when we use it + synchronized (mSyncThreadLock) { + if (mSyncThread == null) { + if (mAutoInitialize + && extras != null + && extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + if (ContentResolver.getIsSyncable(account, authority) < 0) { + ContentResolver.setIsSyncable(account, authority, 1); + } + syncContextClient.onFinished(new SyncResult()); + return; + } + mSyncThread = new SyncThread( + "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), + syncContextClient, authority, account, extras); + mSyncThread.start(); + alreadyInProgress = false; + } else { + alreadyInProgress = true; + } + } + + // do this outside since we don't want to call back into the syncContext while + // holding the synchronization lock + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + public void cancelSync(ISyncContext syncContext) { + // synchronize to make sure that mSyncThread doesn't change between when we + // check it and when we use it + synchronized (mSyncThreadLock) { + if (mSyncThread != null + && mSyncThread.mSyncContext.getSyncContextBinder() + == syncContext.asBinder()) { + mSyncThread.interrupt(); + } + } + } + } + + /** + * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires + * the provider for this sync before calling onPerformSync and releases it afterwards. Cancel + * this thread in order to cancel the sync. + */ + private class SyncThread extends Thread { + private final SyncContext mSyncContext; + private final String mAuthority; + private final Account mAccount; + private final Bundle mExtras; + private long mInitialTxBytes; + private long mInitialRxBytes; + + private SyncThread(String name, SyncContext syncContext, String authority, + Account account, Bundle extras) { + super(name); + mSyncContext = syncContext; + mAuthority = authority; + mAccount = account; + mExtras = extras; + } + + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + if (isCanceled()) { + return; + } + + SyncResult syncResult = new SyncResult(); + int uid = Process.myUid(); + mInitialTxBytes = NetStat.getUidTxBytes(uid); + mInitialRxBytes = NetStat.getUidRxBytes(uid); + ContentProviderClient provider = null; + try { + provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + if (provider != null) { + AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras, + mAuthority, provider, syncResult); + } else { + syncResult.databaseError = true; + } + } finally { + if (provider != null) { + provider.release(); + } + if (!isCanceled()) { + mSyncContext.onFinished(syncResult); + } + onLogSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, syncResult); + // synchronize so that the assignment will be seen by other threads + // that also synchronize accesses to mSyncThread + synchronized (mSyncThreadLock) { + mSyncThread = null; + } + } + } + + private boolean isCanceled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * @return a reference to the IBinder of the SyncAdapter service. + */ + public final IBinder getSyncAdapterBinder() { + return mISyncAdapterImpl.asBinder(); + } + + /** + * Perform a sync for this account. SyncAdapter-specific parameters may + * be specified in extras, which is guaranteed to not be null. Invocations + * of this method are guaranteed to be serialized. + * + * @param account the account that should be synced + * @param extras SyncAdapter-specific parameters + * @param authority the authority of this sync request + * @param provider a ContentProviderClient that points to the ContentProvider for this + * authority + * @param syncResult SyncAdapter-specific parameters + */ + public abstract void onPerformSync(Account account, Bundle extras, + String authority, ContentProviderClient provider, SyncResult syncResult); + + /** + * Logs details on the sync. + * Normally this will be overridden by a subclass that will provide + * provider-specific details. + * + * @param bytesSent number of bytes the sync sent over the network + * @param bytesReceived number of bytes the sync received over the network + * @param result The SyncResult object holding info on the sync + * @hide + */ + protected void onLogSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { + EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); + } +} diff --git a/core/java/android/content/ActiveSyncInfo.java b/core/java/android/content/ActiveSyncInfo.java index 63be8d19250b6f77ef3ff4dc3ca703a410e35cff..209dffadef979e87fd5fee3b0e6ce60dc9e9eba8 100644 --- a/core/java/android/content/ActiveSyncInfo.java +++ b/core/java/android/content/ActiveSyncInfo.java @@ -16,17 +16,18 @@ package android.content; +import android.accounts.Account; import android.os.Parcel; import android.os.Parcelable.Creator; /** @hide */ public class ActiveSyncInfo { public final int authorityId; - public final String account; + public final Account account; public final String authority; public final long startTime; - ActiveSyncInfo(int authorityId, String account, String authority, + ActiveSyncInfo(int authorityId, Account account, String authority, long startTime) { this.authorityId = authorityId; this.account = account; @@ -40,14 +41,14 @@ public class ActiveSyncInfo { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - parcel.writeString(account); + account.writeToParcel(parcel, 0); parcel.writeString(authority); parcel.writeLong(startTime); } ActiveSyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = parcel.readString(); + account = new Account(parcel); authority = parcel.readString(); startTime = parcel.readLong(); } diff --git a/core/java/android/content/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java index ac851ccbcc211540b7c35b68dd201c0b988acf28..0a4a804a52dc893a618b54bc3ea3161173781f61 100644 --- a/core/java/android/content/AsyncQueryHandler.java +++ b/core/java/android/content/AsyncQueryHandler.java @@ -38,7 +38,8 @@ public abstract class AsyncQueryHandler extends Handler { private static final int EVENT_ARG_INSERT = 2; private static final int EVENT_ARG_UPDATE = 3; private static final int EVENT_ARG_DELETE = 4; - + private static final int EVENT_ARG_QUERY_ENTITIES = 5; + /* package */ final WeakReference mResolver; private static Looper sLooper = null; @@ -85,12 +86,25 @@ public abstract class AsyncQueryHandler extends Handler { cursor.getCount(); } } catch (Exception e) { + Log.w(TAG, e.toString()); cursor = null; } args.result = cursor; break; + case EVENT_ARG_QUERY_ENTITIES: + EntityIterator iterator = null; + try { + iterator = resolver.queryEntities(args.uri, args.selection, + args.selectionArgs, args.orderBy); + } catch (Exception e) { + Log.w(TAG, e.toString()); + } + + args.result = iterator; + break; + case EVENT_ARG_INSERT: args.result = resolver.insert(args.uri, args.values); break; @@ -103,7 +117,6 @@ public abstract class AsyncQueryHandler extends Handler { case EVENT_ARG_DELETE: args.result = resolver.delete(args.uri, args.selection, args.selectionArgs); break; - } // passing the original token value back to the caller @@ -128,7 +141,7 @@ public abstract class AsyncQueryHandler extends Handler { if (sLooper == null) { HandlerThread thread = new HandlerThread("AsyncQueryWorker"); thread.start(); - + sLooper = thread.getLooper(); } } @@ -181,6 +194,45 @@ public abstract class AsyncQueryHandler extends Handler { mWorkerThreadHandler.sendMessage(msg); } + /** + * This method begins an asynchronous query for an {@link EntityIterator}. + * When the query is done {@link #onQueryEntitiesComplete} is called. + * + * @param token A token passed into {@link #onQueryComplete} to identify the + * query. + * @param cookie An object that gets passed into {@link #onQueryComplete} + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that + * they appear in the selection. The values will be bound as + * Strings. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @hide + */ + public void startQueryEntities(int token, Object cookie, Uri uri, String selection, + String[] selectionArgs, String orderBy) { + // Use the token as what so cancelOperations works properly + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_ARG_QUERY_ENTITIES; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.selection = selection; + args.selectionArgs = selectionArgs; + args.orderBy = orderBy; + args.cookie = cookie; + msg.obj = args; + + mWorkerThreadHandler.sendMessage(msg); + } + /** * Attempts to cancel operation that has not already started. Note that * there is no guarantee that the operation will be canceled. They still may @@ -279,14 +331,26 @@ public abstract class AsyncQueryHandler extends Handler { * Called when an asynchronous query is completed. * * @param token the token to identify the query, passed in from - * {@link #startQuery}. - * @param cookie the cookie object that's passed in from {@link #startQuery}. + * {@link #startQuery}. + * @param cookie the cookie object passed in from {@link #startQuery}. * @param cursor The cursor holding the results from the query. */ protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // Empty } + /** + * Called when an asynchronous query is completed. + * + * @param token The token to identify the query. + * @param cookie The cookie object. + * @param iterator The iterator holding the query results. + * @hide + */ + protected void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) { + // Empty + } + /** * Called when an asynchronous insert is completed. * @@ -337,13 +401,17 @@ public abstract class AsyncQueryHandler extends Handler { int token = msg.what; int event = msg.arg1; - + // pass token back to caller on each callback. switch (event) { case EVENT_ARG_QUERY: onQueryComplete(token, args.cookie, (Cursor) args.result); break; + case EVENT_ARG_QUERY_ENTITIES: + onQueryEntitiesComplete(token, args.cookie, (EntityIterator)args.result); + break; + case EVENT_ARG_INSERT: onInsertComplete(token, args.cookie, (Uri) args.result); break; diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java index b391c57df5ee6eadbc76050ad882d03644fc2885..b63d026c2b331298dd4cd7166545bb263f8740f1 100644 --- a/core/java/android/content/BroadcastReceiver.java +++ b/core/java/android/content/BroadcastReceiver.java @@ -383,6 +383,24 @@ public abstract class BroadcastReceiver { mAbortBroadcast = false; } + /** + * Returns true if the receiver is currently processing an ordered + * broadcast. + */ + public final boolean isOrderedBroadcast() { + return mOrderedHint; + } + + /** + * Returns true if the receiver is currently processing the initial + * value of a sticky broadcast -- that is, the value that was last + * broadcast and is currently held in the sticky cache, so this is + * not directly the result of a broadcast right now. + */ + public final boolean isInitialStickyBroadcast() { + return mInitialStickyHint; + } + /** * For internal use, sets the hint about whether this BroadcastReceiver is * running in ordered mode. @@ -391,6 +409,14 @@ public abstract class BroadcastReceiver { mOrderedHint = isOrdered; } + /** + * For internal use, sets the hint about whether this BroadcastReceiver is + * receiving the initial sticky broadcast value. @hide + */ + public final void setInitialStickyHint(boolean isInitialSticky) { + mInitialStickyHint = isInitialSticky; + } + /** * Control inclusion of debugging help for mismatched * calls to {@ Context#registerReceiver(BroadcastReceiver, IntentFilter) @@ -414,7 +440,10 @@ public abstract class BroadcastReceiver { } void checkSynchronousHint() { - if (mOrderedHint) { + // Note that we don't assert when receiving the initial sticky value, + // since that may have come from an ordered broadcast. We'll catch + // them later when the real broadcast happens again. + if (mOrderedHint || mInitialStickyHint) { return; } RuntimeException e = new RuntimeException( @@ -429,5 +458,6 @@ public abstract class BroadcastReceiver { private boolean mAbortBroadcast; private boolean mDebugUnregister; private boolean mOrderedHint; + private boolean mInitialStickyHint; } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 6b504055ce8de2b93e46a308e69056e8ff9cd6d5..a341c9be8fda86fb7bd53b22181fb6e2ac040475 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -34,6 +34,7 @@ import android.os.Process; import java.io.File; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * Content providers are one of the primary building blocks of Android applications, providing @@ -130,6 +131,15 @@ public abstract class ContentProvider implements ComponentCallbacks { selectionArgs, sortOrder); } + /** + * @hide + */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + enforceReadPermission(uri); + return ContentProvider.this.queryEntities(uri, selection, selectionArgs, sortOrder); + } + public String getType(Uri uri) { return ContentProvider.this.getType(uri); } @@ -145,6 +155,20 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.bulkInsert(uri, initialValues); } + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + for (ContentProviderOperation operation : operations) { + if (operation.isReadOperation()) { + enforceReadPermission(operation.getUri()); + } + + if (operation.isWriteOperation()) { + enforceWritePermission(operation.getUri()); + } + } + return ContentProvider.this.applyBatch(operations); + } + public int delete(Uri uri, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.delete(uri, selection, selectionArgs); @@ -170,12 +194,6 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.openAssetFile(uri, mode); } - public ISyncAdapter getSyncAdapter() { - enforceWritePermission(null); - SyncAdapter sa = ContentProvider.this.getSyncAdapter(); - return sa != null ? sa.getISyncAdapter() : null; - } - private void enforceReadPermission(Uri uri) { final int uid = Binder.getCallingUid(); if (uid == mMyUid) { @@ -377,9 +395,10 @@ public abstract class ContentProvider implements ComponentCallbacks { * Example client call:

    *

    // Request a specific record.
          * Cursor managedCursor = managedQuery(
    -                Contacts.People.CONTENT_URI.addId(2),
    +                ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2),
                     projection,    // Which columns to return.
                     null,          // WHERE clause.
    +                null,          // WHERE clause value substitution
                     People.NAME + " ASC");   // Sort order.
    * Example implementation:

    *

    // SQLiteQueryBuilder is a helper class that creates the
    @@ -408,20 +427,31 @@ public abstract class ContentProvider implements ComponentCallbacks {
             return c;
    * * @param uri The URI to query. This will be the full URI sent by the client; - * if the client is requesting a specific record, the URI will end in a record number - * that the implementation should parse and add to a WHERE or HAVING clause, specifying - * that _id value. + * if the client is requesting a specific record, the URI will end in a record number + * that the implementation should parse and add to a WHERE or HAVING clause, specifying + * that _id value. * @param projection The list of columns to put into the cursor. If * null all columns are included. * @param selection A selection criteria to apply when filtering rows. * If null then all rows are included. + * @param selectionArgs You may include ?s in selection, which will be replaced by + * the values from selectionArgs, in order that they appear in the selection. + * The values will be bound as Strings. * @param sortOrder How the rows in the cursor should be sorted. - * If null then the provider is free to define the sort order. + * If null then the provider is free to define the sort order. * @return a Cursor or null. */ public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); + /** + * @hide + */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + /** * Return the MIME type of the data at the given URI. This should start with * vnd.android.cursor.item for a single record, @@ -519,7 +549,7 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Open a file blob associated with a content URI. * This method can be called from multiple - * threads, as described in + * threads, as described inentity * Application Fundamentals: * Processes and Threads. * @@ -638,23 +668,6 @@ public abstract class ContentProvider implements ComponentCallbacks { return ParcelFileDescriptor.open(new File(path), modeBits); } - /** - * Get the sync adapter that is to be used by this content provider. - * This is intended for use by the sync system. If null then this - * content provider is considered not syncable. - * This method can be called from multiple - * threads, as described in - * Application Fundamentals: - * Processes and Threads. - * - * @return the SyncAdapter that is to be used by this ContentProvider, or null - * if this ContentProvider is not syncable - * @hide - */ - public SyncAdapter getSyncAdapter() { - return null; - } - /** * Returns true if this instance is a temporary content provider. * @return true if this instance is a temporary content provider @@ -697,4 +710,27 @@ public abstract class ContentProvider implements ComponentCallbacks { ContentProvider.this.onCreate(); } } -} + + /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + */ + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + results[i] = operations.get(i).apply(this, results, i); + } + return results; + } +} \ No newline at end of file diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java new file mode 100644 index 0000000000000000000000000000000000000000..403c4d87a4cd78417a041312b88167a003c7dfe6 --- /dev/null +++ b/core/java/android/content/ContentProviderClient.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.os.ParcelFileDescriptor; +import android.content.res.AssetFileDescriptor; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +/** + * The public interface object used to interact with a {@link ContentProvider}. This is obtained by + * calling {@link ContentResolver#acquireContentProviderClient}. This object must be released + * using {@link #release} in order to indicate to the system that the {@link ContentProvider} is + * no longer needed and can be killed to free up resources. + */ +public class ContentProviderClient { + private final IContentProvider mContentProvider; + private final ContentResolver mContentResolver; + + /** + * @hide + */ + ContentProviderClient(ContentResolver contentResolver, IContentProvider contentProvider) { + mContentProvider = contentProvider; + mContentResolver = contentResolver; + } + + /** see {@link ContentProvider#query} */ + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder) throws RemoteException { + return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#getType} */ + public String getType(Uri url) throws RemoteException { + return mContentProvider.getType(url); + } + + /** see {@link ContentProvider#insert} */ + public Uri insert(Uri url, ContentValues initialValues) + throws RemoteException { + return mContentProvider.insert(url, initialValues); + } + + /** see {@link ContentProvider#bulkInsert} */ + public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException { + return mContentProvider.bulkInsert(url, initialValues); + } + + /** see {@link ContentProvider#delete} */ + public int delete(Uri url, String selection, String[] selectionArgs) + throws RemoteException { + return mContentProvider.delete(url, selection, selectionArgs); + } + + /** see {@link ContentProvider#update} */ + public int update(Uri url, ContentValues values, String selection, + String[] selectionArgs) throws RemoteException { + return mContentProvider.update(url, values, selection, selectionArgs); + } + + /** see {@link ContentProvider#openFile} */ + public ParcelFileDescriptor openFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openFile(url, mode); + } + + /** see {@link ContentProvider#openAssetFile} */ + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openAssetFile(url, mode); + } + + /** + * see {@link ContentProvider#queryEntities} + * @hide + */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) throws RemoteException { + return mContentProvider.queryEntities(uri, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#applyBatch} */ + public ContentProviderResult[] applyBatch(ArrayList operations) + throws RemoteException, OperationApplicationException { + return mContentProvider.applyBatch(operations); + } + + /** + * Call this to indicate to the system that the associated {@link ContentProvider} is no + * longer needed by this {@link ContentProviderClient}. + * @return true if this was release, false if it was already released + */ + public boolean release() { + return mContentResolver.releaseProvider(mContentProvider); + } + + /** + * Get a reference to the {@link ContentProvider} that is associated with this + * client. If the {@link ContentProvider} is running in a different process then + * null will be returned. This can be used if you know you are running in the same + * process as a provider, and want to get direct access to its implementation details. + * + * @return If the associated {@link ContentProvider} is local, returns it. + * Otherwise returns null. + */ + public ContentProvider getLocalContentProvider() { + return ContentProvider.coerceToLocalContentProvider(mContentProvider); + } +} diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index e5e3f749a8d750d3699c5d4c3a0191395586b61e..adc3f60f484f83a1ab1ab4b475455c68062d8d94 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -33,6 +33,7 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * {@hide} @@ -105,6 +106,20 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case QUERY_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String selection = data.readString(); + String[] selectionArgs = data.readStringArray(); + String sortOrder = data.readString(); + EntityIterator entityIterator = queryEntities(url, selection, selectionArgs, + sortOrder); + reply.writeNoException(); + reply.writeStrongBinder(new IEntityIteratorImpl(entityIterator).asBinder()); + return true; + } + case GET_TYPE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -140,6 +155,21 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case APPLY_BATCH_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + final int numOperations = data.readInt(); + final ArrayList operations = + new ArrayList(numOperations); + for (int i = 0; i < numOperations; i++) { + operations.add(i, ContentProviderOperation.CREATOR.createFromParcel(data)); + } + final ContentProviderResult[] results = applyBatch(operations); + reply.writeNoException(); + reply.writeTypedArray(results, 0); + return true; + } + case DELETE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -206,15 +236,6 @@ abstract public class ContentProviderNative extends Binder implements IContentPr } return true; } - - case GET_SYNC_ADAPTER_TRANSACTION: - { - data.enforceInterface(IContentProvider.descriptor); - ISyncAdapter sa = getSyncAdapter(); - reply.writeNoException(); - reply.writeStrongBinder(sa != null ? sa.asBinder() : null); - return true; - } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -224,6 +245,32 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return super.onTransact(code, data, reply, flags); } + /** + * @hide + */ + private class IEntityIteratorImpl extends IEntityIterator.Stub { + private final EntityIterator mEntityIterator; + + IEntityIteratorImpl(EntityIterator iterator) { + mEntityIterator = iterator; + } + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void reset() throws RemoteException { + mEntityIterator.reset(); + } + + public void close() throws RemoteException { + mEntityIterator.close(); + } + } + public IBinder asBinder() { return this; @@ -297,7 +344,7 @@ final class ContentProviderProxy implements IContentProvider BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); IBulkCursor bulkCursor = bulkQuery(url, projection, selection, selectionArgs, sortOrder, adaptor.getObserver(), window); - + if (bulkCursor == null) { return null; } @@ -305,6 +352,64 @@ final class ContentProviderProxy implements IContentProvider return adaptor; } + /** + * @hide + */ + public EntityIterator queryEntities(Uri url, String selection, String[] selectionArgs, + String sortOrder) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(selection); + data.writeStringArray(selectionArgs); + data.writeString(sortOrder); + + mRemote.transact(IContentProvider.QUERY_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + + IBinder entityIteratorBinder = reply.readStrongBinder(); + + data.recycle(); + reply.recycle(); + + return new RemoteEntityIterator(IEntityIterator.Stub.asInterface(entityIteratorBinder)); + } + + /** + * @hide + */ + static class RemoteEntityIterator implements EntityIterator { + private final IEntityIterator mEntityIterator; + RemoteEntityIterator(IEntityIterator entityIterator) { + mEntityIterator = entityIterator; + } + + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void reset() throws RemoteException { + mEntityIterator.reset(); + } + + public void close() { + try { + mEntityIterator.close(); + } catch (RemoteException e) { + // doesn't matter + } + } + } + public String getType(Uri url) throws RemoteException { Parcel data = Parcel.obtain(); @@ -366,6 +471,28 @@ final class ContentProviderProxy implements IContentProvider return count; } + public ContentProviderResult[] applyBatch(ArrayList operations) + throws RemoteException, OperationApplicationException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + data.writeInt(operations.size()); + for (ContentProviderOperation operation : operations) { + operation.writeToParcel(data, 0); + } + mRemote.transact(IContentProvider.APPLY_BATCH_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithOperationApplicationExceptionFromParcel(reply); + final ContentProviderResult[] results = + reply.createTypedArray(ContentProviderResult.CREATOR); + + data.recycle(); + reply.recycle(); + + return results; + } + public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); @@ -456,23 +583,6 @@ final class ContentProviderProxy implements IContentProvider return fd; } - public ISyncAdapter getSyncAdapter() throws RemoteException { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IContentProvider.descriptor); - - mRemote.transact(IContentProvider.GET_SYNC_ADAPTER_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - ISyncAdapter syncAdapter = ISyncAdapter.Stub.asInterface(reply.readStrongBinder()); - - data.recycle(); - reply.recycle(); - - return syncAdapter; - } - private IBinder mRemote; } diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java new file mode 100644 index 0000000000000000000000000000000000000000..ca36df2fbebb499a23e0eef89d05200be8658741 --- /dev/null +++ b/core/java/android/content/ContentProviderOperation.java @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class ContentProviderOperation implements Parcelable { + /** @hide exposed for unit tests */ + public final static int TYPE_INSERT = 1; + /** @hide exposed for unit tests */ + public final static int TYPE_UPDATE = 2; + /** @hide exposed for unit tests */ + public final static int TYPE_DELETE = 3; + /** @hide exposed for unit tests */ + public final static int TYPE_ASSERT = 4; + + private final int mType; + private final Uri mUri; + private final String mSelection; + private final String[] mSelectionArgs; + private final ContentValues mValues; + private final Integer mExpectedCount; + private final ContentValues mValuesBackReferences; + private final Map mSelectionArgsBackReferences; + private final boolean mYieldAllowed; + + /** + * Creates a {@link ContentProviderOperation} by copying the contents of a + * {@link Builder}. + */ + private ContentProviderOperation(Builder builder) { + mType = builder.mType; + mUri = builder.mUri; + mValues = builder.mValues; + mSelection = builder.mSelection; + mSelectionArgs = builder.mSelectionArgs; + mExpectedCount = builder.mExpectedCount; + mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences; + mValuesBackReferences = builder.mValuesBackReferences; + mYieldAllowed = builder.mYieldAllowed; + } + + private ContentProviderOperation(Parcel source) { + mType = source.readInt(); + mUri = Uri.CREATOR.createFromParcel(source); + mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null; + mSelection = source.readInt() != 0 ? source.readString() : null; + mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null; + mExpectedCount = source.readInt() != 0 ? source.readInt() : null; + mValuesBackReferences = source.readInt() != 0 + ? ContentValues.CREATOR.createFromParcel(source) + : null; + mSelectionArgsBackReferences = source.readInt() != 0 + ? new HashMap() + : null; + if (mSelectionArgsBackReferences != null) { + final int count = source.readInt(); + for (int i = 0; i < count; i++) { + mSelectionArgsBackReferences.put(source.readInt(), source.readInt()); + } + } + mYieldAllowed = source.readInt() != 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + Uri.writeToParcel(dest, mUri); + if (mValues != null) { + dest.writeInt(1); + mValues.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelection != null) { + dest.writeInt(1); + dest.writeString(mSelection); + } else { + dest.writeInt(0); + } + if (mSelectionArgs != null) { + dest.writeInt(1); + dest.writeStringArray(mSelectionArgs); + } else { + dest.writeInt(0); + } + if (mExpectedCount != null) { + dest.writeInt(1); + dest.writeInt(mExpectedCount); + } else { + dest.writeInt(0); + } + if (mValuesBackReferences != null) { + dest.writeInt(1); + mValuesBackReferences.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelectionArgsBackReferences != null) { + dest.writeInt(1); + dest.writeInt(mSelectionArgsBackReferences.size()); + for (Map.Entry entry : mSelectionArgsBackReferences.entrySet()) { + dest.writeInt(entry.getKey()); + dest.writeInt(entry.getValue()); + } + } else { + dest.writeInt(0); + } + dest.writeInt(mYieldAllowed ? 1 : 0); + } + + /** + * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the insert. + * @return a {@link Builder} + */ + public static Builder newInsert(Uri uri) { + return new Builder(TYPE_INSERT, uri); + } + + /** + * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the update. + * @return a {@link Builder} + */ + public static Builder newUpdate(Uri uri) { + return new Builder(TYPE_UPDATE, uri); + } + + /** + * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the delete. + * @return a {@link Builder} + */ + public static Builder newDelete(Uri uri) { + return new Builder(TYPE_DELETE, uri); + } + + /** + * Create a {@link Builder} suitable for building a + * {@link ContentProviderOperation} to assert a set of values as provided + * through {@link Builder#withValues(ContentValues)}. + */ + public static Builder newAssertQuery(Uri uri) { + return new Builder(TYPE_ASSERT, uri); + } + + public Uri getUri() { + return mUri; + } + + public boolean isYieldAllowed() { + return mYieldAllowed; + } + + /** @hide exposed for unit tests */ + public int getType() { + return mType; + } + + public boolean isWriteOperation() { + return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; + } + + public boolean isReadOperation() { + return mType == TYPE_ASSERT; + } + + /** + * Applies this operation using the given provider. The backRefs array is used to resolve any + * back references that were requested using + * {@link Builder#withValueBackReferences(ContentValues)} and + * {@link Builder#withSelectionBackReference}. + * @param provider the {@link ContentProvider} on which this batch is applied + * @param backRefs a {@link ContentProviderResult} array that will be consulted + * to resolve any requested back references. + * @param numBackRefs the number of valid results on the backRefs array. + * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted + * row if this was an insert otherwise the number of rows affected. + * @throws OperationApplicationException thrown if either the insert fails or + * if the number of rows affected didn't match the expected count + */ + public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs, + int numBackRefs) throws OperationApplicationException { + ContentValues values = resolveValueBackReferences(backRefs, numBackRefs); + String[] selectionArgs = + resolveSelectionArgsBackReferences(backRefs, numBackRefs); + + if (mType == TYPE_INSERT) { + Uri newUri = provider.insert(mUri, values); + if (newUri == null) { + throw new OperationApplicationException("insert failed"); + } + return new ContentProviderResult(newUri); + } + + int numRows; + if (mType == TYPE_DELETE) { + numRows = provider.delete(mUri, mSelection, selectionArgs); + } else if (mType == TYPE_UPDATE) { + numRows = provider.update(mUri, values, mSelection, selectionArgs); + } else if (mType == TYPE_ASSERT) { + // Assert that all rows match expected values + String[] projection = null; + if (values != null) { + // Build projection map from expected values + final ArrayList projectionList = new ArrayList(); + for (Map.Entry entry : values.valueSet()) { + projectionList.add(entry.getKey()); + } + projection = projectionList.toArray(new String[projectionList.size()]); + } + final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null); + try { + numRows = cursor.getCount(); + if (projection != null) { + while (cursor.moveToNext()) { + for (int i = 0; i < projection.length; i++) { + final String cursorValue = cursor.getString(i); + final String expectedValue = values.getAsString(projection[i]); + if (!TextUtils.equals(cursorValue, expectedValue)) { + // Throw exception when expected values don't match + throw new OperationApplicationException("Found value " + cursorValue + + " when expected " + expectedValue + " for column " + + projection[i]); + } + } + } + } + } finally { + cursor.close(); + } + } else { + throw new IllegalStateException("bad type, " + mType); + } + + if (mExpectedCount != null && mExpectedCount != numRows) { + throw new OperationApplicationException("wrong number of rows: " + numRows); + } + + return new ContentProviderResult(numRows); + } + + /** + * The ContentValues back references are represented as a ContentValues object where the + * key refers to a column and the value is an index of the back reference whose + * valued should be associated with the column. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public ContentValues resolveValueBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mValuesBackReferences == null) { + return mValues; + } + final ContentValues values; + if (mValues == null) { + values = new ContentValues(); + } else { + values = new ContentValues(mValues); + } + for (Map.Entry entry : mValuesBackReferences.valueSet()) { + String key = entry.getKey(); + Integer backRefIndex = mValuesBackReferences.getAsInteger(key); + if (backRefIndex == null) { + throw new IllegalArgumentException("values backref " + key + " is not an integer"); + } + values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex)); + } + return values; + } + + /** + * The Selection Arguments back references are represented as a Map of Integer->Integer where + * the key is an index into the selection argument array (see {@link Builder#withSelection}) + * and the value is the index of the previous result that should be used for that selection + * argument array slot. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public String[] resolveSelectionArgsBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mSelectionArgsBackReferences == null) { + return mSelectionArgs; + } + String[] newArgs = new String[mSelectionArgs.length]; + System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length); + for (Map.Entry selectionArgBackRef + : mSelectionArgsBackReferences.entrySet()) { + final Integer selectionArgIndex = selectionArgBackRef.getKey(); + final int backRefIndex = selectionArgBackRef.getValue(); + newArgs[selectionArgIndex] = + String.valueOf(backRefToValue(backRefs, numBackRefs, backRefIndex)); + } + return newArgs; + } + + /** + * Return the string representation of the requested back reference. + * @param backRefs an array of results + * @param numBackRefs the number of items in the backRefs array that are valid + * @param backRefIndex which backRef to be used + * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than + * the numBackRefs + * @return the string representation of the requested back reference. + */ + private static long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs, + Integer backRefIndex) { + if (backRefIndex >= numBackRefs) { + throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex + + " but there are only " + numBackRefs + " back refs"); + } + ContentProviderResult backRef = backRefs[backRefIndex]; + long backRefValue; + if (backRef.uri != null) { + backRefValue = ContentUris.parseId(backRef.uri); + } else { + backRefValue = backRef.count; + } + return backRefValue; + } + + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = + new Creator() { + public ContentProviderOperation createFromParcel(Parcel source) { + return new ContentProviderOperation(source); + } + + public ContentProviderOperation[] newArray(int size) { + return new ContentProviderOperation[size]; + } + }; + + + /** + * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is + * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)}, + * {@link ContentProviderOperation#newUpdate(android.net.Uri)}, + * {@link ContentProviderOperation#newDelete(android.net.Uri)} or + * {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods + * can then be used to add parameters to the builder. See the specific methods to find for + * which {@link Builder} type each is allowed. Call {@link #build} to create the + * {@link ContentProviderOperation} once all the parameters have been supplied. + */ + public static class Builder { + private final int mType; + private final Uri mUri; + private String mSelection; + private String[] mSelectionArgs; + private ContentValues mValues; + private Integer mExpectedCount; + private ContentValues mValuesBackReferences; + private Map mSelectionArgsBackReferences; + private boolean mYieldAllowed; + + /** Create a {@link Builder} of a given type. The uri must not be null. */ + private Builder(int type, Uri uri) { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + mType = type; + mUri = uri; + } + + /** Create a ContentProviderOperation from this {@link Builder}. */ + public ContentProviderOperation build() { + if (mType == TYPE_UPDATE) { + if ((mValues == null || mValues.size() == 0) + && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) { + throw new IllegalArgumentException("Empty values"); + } + } + if (mType == TYPE_ASSERT) { + if ((mValues == null || mValues.size() == 0) + && (mValuesBackReferences == null || mValuesBackReferences.size() == 0) + && (mExpectedCount == null)) { + throw new IllegalArgumentException("Empty values"); + } + } + return new ContentProviderOperation(this); + } + + /** + * Add a {@link ContentValues} of back references. The key is the name of the column + * and the value is an integer that is the index of the previous result whose + * value should be used for the column. The value is added as a {@link String}. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReferences(ContentValues backReferences) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts can have value back-references"); + } + mValuesBackReferences = backReferences; + return this; + } + + /** + * Add a ContentValues back reference. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReference(String key, int previousResult) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts can have value back-references"); + } + if (mValuesBackReferences == null) { + mValuesBackReferences = new ContentValues(); + } + mValuesBackReferences.put(key, previousResult); + return this; + } + + /** + * Add a back references as a selection arg. Any value at that index of the selection arg + * that was specified by {@link #withSelection} will be overwritten. + * This can only be used with builders of type update, delete, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException("only updates, deletes, and asserts " + + "can have selection back-references"); + } + if (mSelectionArgsBackReferences == null) { + mSelectionArgsBackReferences = new HashMap(); + } + mSelectionArgsBackReferences.put(selectionArgIndex, previousResult); + return this; + } + + /** + * The ContentValues to use. This may be null. These values may be overwritten by + * the corresponding value specified by {@link #withValueBackReference} or by + * future calls to {@link #withValues} or {@link #withValue}. + * This can only be used with builders of type insert, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValues(ContentValues values) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + mValues.putAll(values); + return this; + } + + /** + * A value to insert or update. This value may be overwritten by + * the corresponding value specified by {@link #withValueBackReference}. + * This can only be used with builders of type insert, update, or assert. + * @param key the name of this value + * @param value the value itself. the type must be acceptable for insertion by + * {@link ContentValues#put} + * @return this builder, to allow for chaining. + */ + public Builder withValue(String key, Object value) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException("only inserts and updates can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + if (value == null) { + mValues.putNull(key); + } else if (value instanceof String) { + mValues.put(key, (String) value); + } else if (value instanceof Byte) { + mValues.put(key, (Byte) value); + } else if (value instanceof Short) { + mValues.put(key, (Short) value); + } else if (value instanceof Integer) { + mValues.put(key, (Integer) value); + } else if (value instanceof Long) { + mValues.put(key, (Long) value); + } else if (value instanceof Float) { + mValues.put(key, (Float) value); + } else if (value instanceof Double) { + mValues.put(key, (Double) value); + } else if (value instanceof Boolean) { + mValues.put(key, (Boolean) value); + } else if (value instanceof byte[]) { + mValues.put(key, (byte[]) value); + } else { + throw new IllegalArgumentException("bad value type: " + value.getClass().getName()); + } + return this; + } + + /** + * The selection and arguments to use. An occurrence of '?' in the selection will be + * replaced with the corresponding occurence of the selection argument. Any of the + * selection arguments may be overwritten by a selection argument back reference as + * specified by {@link #withSelectionBackReference}. + * This can only be used with builders of type update, delete, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withSelection(String selection, String[] selectionArgs) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only updates, deletes, and asserts can have selections"); + } + mSelection = selection; + if (selectionArgs == null) { + mSelectionArgs = null; + } else { + mSelectionArgs = new String[selectionArgs.length]; + System.arraycopy(selectionArgs, 0, mSelectionArgs, 0, selectionArgs.length); + } + return this; + } + + /** + * If set then if the number of rows affected by this operation do not match + * this count {@link OperationApplicationException} will be throw. + * This can only be used with builders of type update, delete, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withExpectedCount(int count) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only updates, deletes, and asserts can have expected counts"); + } + mExpectedCount = count; + return this; + } + + public Builder withYieldAllowed(boolean yieldAllowed) { + mYieldAllowed = yieldAllowed; + return this; + } + } +} diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java new file mode 100644 index 0000000000000000000000000000000000000000..5d188ef2bed1e7c83416ad0c559660b1baed59f1 --- /dev/null +++ b/core/java/android/content/ContentProviderResult.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.net.Uri; +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Contains the result of the application of a {@link ContentProviderOperation}. It is guaranteed + * to have exactly one of {@link #uri} or {@link #count} set. + */ +public class ContentProviderResult implements Parcelable { + public final Uri uri; + public final Integer count; + + public ContentProviderResult(Uri uri) { + if (uri == null) throw new IllegalArgumentException("uri must not be null"); + this.uri = uri; + this.count = null; + } + + public ContentProviderResult(int count) { + this.count = count; + this.uri = null; + } + + public ContentProviderResult(Parcel source) { + int type = source.readInt(); + if (type == 1) { + count = source.readInt(); + uri = null; + } else { + count = null; + uri = Uri.CREATOR.createFromParcel(source); + } + } + + public void writeToParcel(Parcel dest, int flags) { + if (uri == null) { + dest.writeInt(1); + dest.writeInt(count); + } else { + dest.writeInt(2); + uri.writeToParcel(dest, 0); + } + } + + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = + new Creator() { + public ContentProviderResult createFromParcel(Parcel source) { + return new ContentProviderResult(source); + } + + public ContentProviderResult[] newArray(int size) { + return new ContentProviderResult[size]; + } + }; + + public String toString() { + if (uri != null) { + return "ContentProviderResult(uri=" + uri.toString() + ")"; + } + return "ContentProviderResult(count=" + count + ")"; + } +} \ No newline at end of file diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 74144fc5608fa5b3c219f887f539f8ff12a1efe6..c4b0807692e5a20436a6e3ea6157607fe4e5e24e 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -30,6 +30,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; +import android.accounts.Account; import android.util.Config; import android.util.Log; @@ -40,19 +41,40 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.ArrayList; /** * This class provides applications access to the content model. */ public abstract class ContentResolver { - public final static String SYNC_EXTRAS_ACCOUNT = "account"; + /** + * @deprecated instead use + * {@link #requestSync(android.accounts.Account, String, android.os.Bundle)} + */ + @Deprecated + public static final String SYNC_EXTRAS_ACCOUNT = "account"; public static final String SYNC_EXTRAS_EXPEDITED = "expedited"; + /** + * @deprecated instead use + * {@link #SYNC_EXTRAS_MANUAL} + */ + @Deprecated public static final String SYNC_EXTRAS_FORCE = "force"; + public static final String SYNC_EXTRAS_MANUAL = "force"; public static final String SYNC_EXTRAS_UPLOAD = "upload"; public static final String SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS = "deletions_override"; public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions"; + /** + * Set by the SyncManager to request that the SyncAdapter initialize itself for + * the given account/authority pair. One required initialization step is to + * ensure that {@link #setIsSyncable(android.accounts.Account, String, int)} has been + * called with a >= 0 value. When this flag is set the SyncAdapter does not need to + * do a full sync, though it is allowed to do so. + */ + public static final String SYNC_EXTRAS_INITIALIZE = "initialize"; + public static final String SCHEME_CONTENT = "content"; public static final String SCHEME_ANDROID_RESOURCE = "android.resource"; public static final String SCHEME_FILE = "file"; @@ -88,7 +110,35 @@ public abstract class ContentResolver { * in the cursor is the same. */ public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; - + + /** @hide */ + public static final int SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS = 1; + /** @hide */ + public static final int SYNC_ERROR_AUTHENTICATION = 2; + /** @hide */ + public static final int SYNC_ERROR_IO = 3; + /** @hide */ + public static final int SYNC_ERROR_PARSE = 4; + /** @hide */ + public static final int SYNC_ERROR_CONFLICT = 5; + /** @hide */ + public static final int SYNC_ERROR_TOO_MANY_DELETIONS = 6; + /** @hide */ + public static final int SYNC_ERROR_TOO_MANY_RETRIES = 7; + /** @hide */ + public static final int SYNC_ERROR_INTERNAL = 8; + + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_SETTINGS = 1<<0; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_PENDING = 1<<1; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_ACTIVE = 1<<2; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_STATUS = 1<<3; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_ALL = 0x7fffffff; + public ContentResolver(Context context) { mContext = context; } @@ -165,6 +215,96 @@ public abstract class ContentResolver { } } + /** + * EntityIterator wrapper that releases the associated ContentProviderClient when the + * iterator is closed. + * @hide + */ + private class EntityIteratorWrapper implements EntityIterator { + private final EntityIterator mInner; + private final ContentProviderClient mClient; + private volatile boolean mClientReleased; + + EntityIteratorWrapper(EntityIterator inner, ContentProviderClient client) { + mInner = inner; + mClient = client; + mClientReleased = false; + } + + public boolean hasNext() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.hasNext(); + } + + public Entity next() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.next(); + } + + public void reset() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + mInner.reset(); + } + + public void close() { + mClient.release(); + mInner.close(); + mClientReleased = true; + } + + protected void finalize() throws Throwable { + if (!mClientReleased) { + mClient.release(); + } + super.finalize(); + } + } + + /** + * Query the given URI, returning an {@link EntityIterator} over the result set. + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that they + * appear in the selection. The values will be bound as Strings. + * @param sortOrder How to order the rows, formatted as an SQL ORDER BY + * clause (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @return An EntityIterator object + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + * @throws IllegalArgumentException thrown if there is no provider that matches the uri + * @hide + */ + public final EntityIterator queryEntities(Uri uri, + String selection, String[] selectionArgs, String sortOrder) throws RemoteException { + ContentProviderClient provider = acquireContentProviderClient(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + uri); + } + try { + EntityIterator entityIterator = + provider.queryEntities(uri, selection, selectionArgs, sortOrder); + return new EntityIteratorWrapper(entityIterator, provider); + } catch(RuntimeException e) { + provider.release(); + throw e; + } catch(RemoteException e) { + provider.release(); + throw e; + } + } + /** * Open a stream on to the content associated with a content URI. If there * is no data associated with the URI, FileNotFoundException is thrown. @@ -389,12 +529,22 @@ public abstract class ContentResolver { } } - class OpenResourceIdResult { - Resources r; - int id; + /** + * A resource identified by the {@link Resources} that contains it, and a resource id. + * + * @hide + */ + public class OpenResourceIdResult { + public Resources r; + public int id; } - - OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { + + /** + * Resolves an android.resource URI to a {@link Resources} and a resource id. + * + * @hide + */ + public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { String authority = uri.getAuthority(); Resources r; if (TextUtils.isEmpty(authority)) { @@ -484,6 +634,36 @@ public abstract class ContentResolver { } } + /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param authority the authority of the ContentProvider to which this batch should be applied + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + */ + public ContentProviderResult[] applyBatch(String authority, + ArrayList operations) + throws RemoteException, OperationApplicationException { + ContentProviderClient provider = acquireContentProviderClient(authority); + if (provider == null) { + throw new IllegalArgumentException("Unknown authority " + authority); + } + try { + return provider.applyBatch(operations); + } finally { + provider.release(); + } + } + /** * Inserts multiple rows into a table at the given URL. * @@ -591,6 +771,46 @@ public abstract class ContentResolver { return acquireProvider(mContext, name); } + /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param uri specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(Uri uri) { + IContentProvider provider = acquireProvider(uri); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + + /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param name specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(String name) { + IContentProvider provider = acquireProvider(name); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + /** * Register an observer class that gets callbacks when data identified by a * given content URI changes. @@ -676,11 +896,43 @@ public abstract class ContentResolver { * * @param uri the uri of the provider to sync or null to sync all providers. * @param extras any extras to pass to the SyncAdapter. + * @deprecated instead use + * {@link #requestSync(android.accounts.Account, String, android.os.Bundle)} */ + @Deprecated public void startSync(Uri uri, Bundle extras) { + Account account = null; + if (extras != null) { + String accountName = extras.getString(SYNC_EXTRAS_ACCOUNT); + if (!TextUtils.isEmpty(accountName)) { + account = new Account(accountName, "com.google"); + } + extras.remove(SYNC_EXTRAS_ACCOUNT); + } + requestSync(account, uri != null ? uri.getAuthority() : null, extras); + } + + /** + * Start an asynchronous sync operation. If you want to monitor the progress + * of the sync you may register a SyncObserver. Only values of the following + * types may be used in the extras bundle: + *
      + *
    • Integer
    • + *
    • Long
    • + *
    • Boolean
    • + *
    • Float
    • + *
    • Double
    • + *
    • String
    • + *
    + * + * @param account which account should be synced + * @param authority which authority should be synced + * @param extras any extras to pass to the SyncAdapter. + */ + public static void requestSync(Account account, String authority, Bundle extras) { validateSyncExtrasBundle(extras); try { - getContentService().startSync(uri, extras); + getContentService().requestSync(account, authority, extras); } catch (RemoteException e) { } } @@ -694,6 +946,7 @@ public abstract class ContentResolver { *
  • Float
  • *
  • Double
  • *
  • String
  • + *
  • Account
  • *
  • null
  • * * @param extras the Bundle to check @@ -709,6 +962,7 @@ public abstract class ContentResolver { if (value instanceof Float) continue; if (value instanceof Double) continue; if (value instanceof String) continue; + if (value instanceof Account) continue; throw new IllegalArgumentException("unexpected value type: " + value.getClass().getName()); } @@ -719,13 +973,211 @@ public abstract class ContentResolver { } } + /** + * Cancel any active or pending syncs that match the Uri. If the uri is null then + * all syncs will be canceled. + * + * @param uri the uri of the provider to sync or null to sync all providers. + * @deprecated instead use {@link #cancelSync(android.accounts.Account, String)} + */ + @Deprecated public void cancelSync(Uri uri) { + cancelSync(null /* all accounts */, uri != null ? uri.getAuthority() : null); + } + + /** + * Cancel any active or pending syncs that match account and authority. The account and + * authority can each independently be set to null, which means that syncs with any account + * or authority, respectively, will match. + * + * @param account filters the syncs that match by this account + * @param authority filters the syncs that match by this authority + */ + public static void cancelSync(Account account, String authority) { + try { + getContentService().cancelSync(account, authority); + } catch (RemoteException e) { + } + } + + /** + * Get information about the SyncAdapters that are known to the system. + * @return an array of SyncAdapters that have registered with the system + */ + public static SyncAdapterType[] getSyncAdapterTypes() { + try { + return getContentService().getSyncAdapterTypes(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Check if the provider should be synced when a network tickle is received + * + * @param account the account whose setting we are querying + * @param authority the provider whose setting we are querying + * @return true if the provider should be synced when a network tickle is received + */ + public static boolean getSyncAutomatically(Account account, String authority) { try { - getContentService().cancelSync(uri); + return getContentService().getSyncAutomatically(account, authority); } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); } } + /** + * Set whether or not the provider is synced when it receives a network tickle. + * + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + public static void setSyncAutomatically(Account account, String authority, boolean sync) { + try { + getContentService().setSyncAutomatically(account, authority, sync); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Check if this account/provider is syncable. + * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. + */ + public static int getIsSyncable(Account account, String authority) { + try { + return getContentService().getIsSyncable(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Set whether this account/provider is syncable. + * @param syncable >0 denotes syncable, 0 means not syncable, <0 means unknown + */ + public static void setIsSyncable(Account account, String authority, int syncable) { + try { + getContentService().setIsSyncable(account, authority, syncable); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Gets the master auto-sync setting that applies to all the providers and accounts. + * If this is false then the per-provider auto-sync setting is ignored. + * + * @return the master auto-sync setting that applies to all the providers and accounts + */ + public static boolean getMasterSyncAutomatically() { + try { + return getContentService().getMasterSyncAutomatically(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Sets the master auto-sync setting that applies to all the providers and accounts. + * If this is false then the per-provider auto-sync setting is ignored. + * + * @param sync the master auto-sync setting that applies to all the providers and accounts + */ + public static void setMasterSyncAutomatically(boolean sync) { + try { + getContentService().setMasterSyncAutomatically(sync); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Returns true if there is currently a sync operation for the given + * account or authority in the pending list, or actively being processed. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return true if a sync is active for the given account or authority. + */ + public static boolean isSyncActive(Account account, String authority) { + try { + return getContentService().isSyncActive(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * If a sync is active returns the information about it, otherwise returns false. + * @return the ActiveSyncInfo for the currently active sync or null if one is not active. + * @hide + */ + public static ActiveSyncInfo getActiveSync() { + try { + return getContentService().getActiveSync(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Returns the status that matches the authority. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return the SyncStatusInfo for the authority, or null if none exists + * @hide + */ + public static SyncStatusInfo getSyncStatus(Account account, String authority) { + try { + return getContentService().getSyncStatus(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Return true if the pending status is true of any matching authorities. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return true if there is a pending sync with the matching account and authority + */ + public static boolean isSyncPending(Account account, String authority) { + try { + return getContentService().isSyncPending(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static Object addStatusChangeListener(int mask, final SyncStatusObserver callback) { + try { + ISyncStatusObserver.Stub observer = new ISyncStatusObserver.Stub() { + public void onStatusChanged(int which) throws RemoteException { + callback.onStatusChanged(which); + } + }; + getContentService().addStatusChangeListener(mask, observer); + return observer; + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static void removeStatusChangeListener(Object handle) { + try { + getContentService().removeStatusChangeListener((ISyncStatusObserver.Stub) handle); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + private final class CursorWrapperInner extends CursorWrapper { private IContentProvider mContentProvider; public static final String TAG="CursorWrapperInner"; diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 6cd2c54a265cd328a585f9eb524c435a2e2859a4..974a6670aab4e600c7ef57ec7ee2df9aa9666ec9 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.database.IContentObserver; import android.database.sqlite.SQLiteException; import android.net.Uri; @@ -160,7 +161,9 @@ public final class ContentService extends IContentService.Stub { } if (syncToNetwork) { SyncManager syncManager = getSyncManager(); - if (syncManager != null) syncManager.scheduleLocalSync(uri); + if (syncManager != null) { + syncManager.scheduleLocalSync(null /* all accounts */, uri.getAuthority()); + } } } finally { restoreCallingIdentity(identityToken); @@ -186,14 +189,17 @@ public final class ContentService extends IContentService.Stub { } } - public void startSync(Uri url, Bundle extras) { + public void requestSync(Account account, String authority, Bundle extras) { ContentResolver.validateSyncExtrasBundle(extras); // This makes it so that future permission checks will be in the context of this // process rather than the caller's process. We will restore this before returning. long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); - if (syncManager != null) syncManager.startSync(url, extras); + if (syncManager != null) { + syncManager.scheduleSync(account, authority, extras, 0 /* no delay */, + false /* onlyThoseWithUnkownSyncableState */); + } } finally { restoreCallingIdentity(identityToken); } @@ -201,34 +207,50 @@ public final class ContentService extends IContentService.Stub { /** * Clear all scheduled sync operations that match the uri and cancel the active sync - * if it matches the uri. If the uri is null, clear all scheduled syncs and cancel - * the active one, if there is one. - * @param uri Filter on the sync operations to cancel, or all if null. + * if they match the authority and account, if they are present. + * @param account filter the pending and active syncs to cancel using this account + * @param authority filter the pending and active syncs to cancel using this authority */ - public void cancelSync(Uri uri) { + public void cancelSync(Account account, String authority) { // This makes it so that future permission checks will be in the context of this // process rather than the caller's process. We will restore this before returning. long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.clearScheduledSyncOperations(uri); - syncManager.cancelActiveSync(uri); + syncManager.clearScheduledSyncOperations(account, authority); + syncManager.cancelActiveSync(account, authority); } } finally { restoreCallingIdentity(identityToken); } } - public boolean getSyncProviderAutomatically(String providerName) { + /** + * Get information about the SyncAdapters that are known to the system. + * @return an array of SyncAdapters that have registered with the system + */ + public SyncAdapterType[] getSyncAdapterTypes() { + // This makes it so that future permission checks will be in the context of this + // process rather than the caller's process. We will restore this before returning. + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + return syncManager.getSyncAdapterTypes(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean getSyncAutomatically(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getSyncProviderAutomatically( - null, providerName); + return syncManager.getSyncStorageEngine().getSyncAutomatically( + account, providerName); } } finally { restoreCallingIdentity(identityToken); @@ -236,51 +258,82 @@ public final class ContentService extends IContentService.Stub { return false; } - public void setSyncProviderAutomatically(String providerName, boolean sync) { + public void setSyncAutomatically(Account account, String providerName, boolean sync) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().setSyncProviderAutomatically( - null, providerName, sync); + syncManager.getSyncStorageEngine().setSyncAutomatically( + account, providerName, sync); } } finally { restoreCallingIdentity(identityToken); } } - public boolean getListenForNetworkTickles() { + public int getIsSyncable(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getListenForNetworkTickles(); + return syncManager.getSyncStorageEngine().getIsSyncable( + account, providerName); + } + } finally { + restoreCallingIdentity(identityToken); + } + return -1; + } + + public void setIsSyncable(Account account, String providerName, int syncable) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + syncManager.getSyncStorageEngine().setIsSyncable( + account, providerName, syncable); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean getMasterSyncAutomatically() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, + "no permission to read the sync settings"); + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + return syncManager.getSyncStorageEngine().getMasterSyncAutomatically(); } } finally { restoreCallingIdentity(identityToken); } return false; } - - public void setListenForNetworkTickles(boolean flag) { + + public void setMasterSyncAutomatically(boolean flag) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().setListenForNetworkTickles(flag); + syncManager.getSyncStorageEngine().setMasterSyncAutomatically(flag); } } finally { restoreCallingIdentity(identityToken); } } - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); @@ -295,7 +348,7 @@ public final class ContentService extends IContentService.Stub { } return false; } - + public ActiveSyncInfo getActiveSync() { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); @@ -310,65 +363,62 @@ public final class ContentService extends IContentService.Stub { } return null; } - - public SyncStatusInfo getStatusByAuthority(String authority) { + + public SyncStatusInfo getSyncStatus(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getStatusByAuthority( - authority); + return syncManager.getSyncStorageEngine().getStatusByAccountAndAuthority( + account, authority); } } finally { restoreCallingIdentity(identityToken); } return null; } - - public boolean isAuthorityPending(String account, String authority) { + + public boolean isSyncPending(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().isAuthorityPending( - account, authority); + return syncManager.getSyncStorageEngine().isSyncPending(account, authority); } } finally { restoreCallingIdentity(identityToken); } return false; } - + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().addStatusChangeListener( - mask, callback); + syncManager.getSyncStorageEngine().addStatusChangeListener(mask, callback); } } finally { restoreCallingIdentity(identityToken); } } - + public void removeStatusChangeListener(ISyncStatusObserver callback) { long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().removeStatusChangeListener( - callback); + syncManager.getSyncStorageEngine().removeStatusChangeListener(callback); } } finally { restoreCallingIdentity(identityToken); } } - + public static IContentService main(Context context, boolean factoryTest) { ContentService service = new ContentService(context, factoryTest); ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 25b5de37128f6fc0c0c1de786db57406240dde8e..8f1c671046ae65e45e105260b4c9f3a403b7e3e9 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -488,90 +488,52 @@ public abstract class Context { public abstract String[] databaseList(); /** - * Like {@link #peekWallpaper}, but always returns a valid Drawable. If - * no wallpaper is set, the system default wallpaper is returned. - * - * @return Returns a Drawable object that will draw the wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#getDrawable + * WallpaperManager.get()} instead. */ + @Deprecated public abstract Drawable getWallpaper(); /** - * Retrieve the current system wallpaper. This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. If there is no wallpaper set, - * a null pointer is returned. - * - * @return Returns a Drawable object that will draw the wallpaper or a - * null pointer if these is none. + * @deprecated Use {@link android.app.WallpaperManager#peekDrawable + * WallpaperManager.peek()} instead. */ + @Deprecated public abstract Drawable peekWallpaper(); /** - * Returns the desired minimum width for the wallpaper. Callers of - * {@link #setWallpaper(android.graphics.Bitmap)} or - * {@link #setWallpaper(java.io.InputStream)} should check this value - * beforehand to make sure the supplied wallpaper respects the desired - * minimum width. - * - * If the returned value is <= 0, the caller should use the width of - * the default display instead. - * - * @return The desired minimum width for the wallpaper. This value should - * be honored by applications that set the wallpaper but it is not - * mandatory. + * @deprecated Use {@link android.app.WallpaperManager#getDesiredMinimumWidth() + * WallpaperManager.getDesiredMinimumWidth()} instead. */ + @Deprecated public abstract int getWallpaperDesiredMinimumWidth(); /** - * Returns the desired minimum height for the wallpaper. Callers of - * {@link #setWallpaper(android.graphics.Bitmap)} or - * {@link #setWallpaper(java.io.InputStream)} should check this value - * beforehand to make sure the supplied wallpaper respects the desired - * minimum height. - * - * If the returned value is <= 0, the caller should use the height of - * the default display instead. - * - * @return The desired minimum height for the wallpaper. This value should - * be honored by applications that set the wallpaper but it is not - * mandatory. + * @deprecated Use {@link android.app.WallpaperManager#getDesiredMinimumHeight() + * WallpaperManager.getDesiredMinimumHeight()} instead. */ + @Deprecated public abstract int getWallpaperDesiredMinimumHeight(); /** - * Change the current system wallpaper to a bitmap. The given bitmap is - * converted to a PNG and stored as the wallpaper. On success, the intent - * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast. - * - * @param bitmap The bitmap to save. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#setBitmap(Bitmap) + * WallpaperManager.set()} instead. */ + @Deprecated public abstract void setWallpaper(Bitmap bitmap) throws IOException; /** - * Change the current system wallpaper to a specific byte stream. The - * give InputStream is copied into persistent storage and will now be - * used as the wallpaper. Currently it must be either a JPEG or PNG - * image. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} - * is broadcast. - * - * @param data A stream containing the raw data to install as a wallpaper. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#setStream(InputStream) + * WallpaperManager.set()} instead. */ + @Deprecated public abstract void setWallpaper(InputStream data) throws IOException; /** - * Remove any currently set wallpaper, reverting to the system's default - * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} - * is broadcast. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#clear + * WallpaperManager.clear()} instead. */ + @Deprecated public abstract void clearWallpaper() throws IOException; /** @@ -596,6 +558,27 @@ public abstract class Context { */ public abstract void startActivity(Intent intent); + /** + * Like {@link #startActivity(Intent)}, but taking a IntentSender + * to start. If the IntentSender is for an activity, that activity will be started + * as if you had called the regular {@link #startActivity(Intent)} + * here; otherwise, its associated action will be executed (such as + * sending a broadcast) as if you had called + * {@link IntentSender#sendIntent IntentSender.sendIntent} on it. + * + * @param intent The IntentSender to launch. + * @param fillInIntent If non-null, this will be provided as the + * intent parameter to {@link IntentSender#sendIntent}. + * @param flagsMask Intent flags in the original IntentSender that you + * would like to change. + * @param flagsValues Desired values for any bits set in + * flagsMask + * @param extraFlags Always set to 0. + */ + public abstract void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException; + /** * Broadcast the given intent to all interested BroadcastReceivers. This * call is asynchronous; it returns immediately, and you will continue @@ -674,8 +657,7 @@ public abstract class Context { * supplying your own BroadcastReceiver when calling, which will be * treated as a final receiver at the end of the broadcast -- its * {@link BroadcastReceiver#onReceive} method will be called with - * the result values collected from the other receivers. If you use - * an resultReceiver with this method, then the broadcast will + * the result values collected from the other receivers. The broadcast will * be serialized in the same way as calling * {@link #sendOrderedBroadcast(Intent, String)}. * @@ -706,6 +688,7 @@ public abstract class Context { * @see #sendBroadcast(Intent, String) * @see #sendOrderedBroadcast(Intent, String) * @see #sendStickyBroadcast(Intent) + * @see #sendStickyOrderedBroadcast(Intent, BroadcastReceiver, Handler, int, String, Bundle) * @see android.content.BroadcastReceiver * @see #registerReceiver * @see android.app.Activity#RESULT_OK @@ -732,8 +715,55 @@ public abstract class Context { * be re-broadcast to future receivers. * * @see #sendBroadcast(Intent) + * @see #sendStickyOrderedBroadcast(Intent, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendStickyBroadcast(Intent intent); + + /** + * Version of {@link #sendStickyBroadcast} that allows you to + * receive data back from the broadcast. This is accomplished by + * supplying your own BroadcastReceiver when calling, which will be + * treated as a final receiver at the end of the broadcast -- its + * {@link BroadcastReceiver#onReceive} method will be called with + * the result values collected from the other receivers. The broadcast will + * be serialized in the same way as calling + * {@link #sendOrderedBroadcast(Intent, String)}. + * + *

    Like {@link #sendBroadcast(Intent)}, this method is + * asynchronous; it will return before + * resultReceiver.onReceive() is called. Note that the sticky data + * stored is only the data you initially supply to the broadcast, not + * the result of any changes made by the receivers. + * + *

    See {@link BroadcastReceiver} for more information on Intent broadcasts. + * + * @param intent The Intent to broadcast; all receivers matching this + * Intent will receive the broadcast. + * @param resultReceiver Your own BroadcastReceiver to treat as the final + * receiver of the broadcast. + * @param scheduler A custom Handler with which to schedule the + * resultReceiver callback; if null it will be + * scheduled in the Context's main thread. + * @param initialCode An initial value for the result code. Often + * Activity.RESULT_OK. + * @param initialData An initial value for the result data. Often + * null. + * @param initialExtras An initial value for the result extras. Often + * null. + * + * @see #sendBroadcast(Intent) + * @see #sendBroadcast(Intent, String) + * @see #sendOrderedBroadcast(Intent, String) + * @see #sendStickyBroadcast(Intent) + * @see android.content.BroadcastReceiver + * @see #registerReceiver + * @see android.app.Activity#RESULT_OK + */ + public abstract void sendStickyOrderedBroadcast(Intent intent, + BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras); + /** * Remove the data previously sent with {@link #sendStickyBroadcast}, @@ -1108,6 +1138,16 @@ public abstract class Context { * @see android.view.LayoutInflater */ public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.accounts.AccountManager} for receiving intents at a + * time of your choosing. + * TODO STOPSHIP perform a final review of the the account apis before shipping + * + * @see #getSystemService + * @see android.accounts.AccountManager + */ + public static final String ACCOUNT_SERVICE = "account"; /** * Use with {@link #getSystemService} to retrieve a * {@link android.app.ActivityManager} for interacting with the global @@ -1177,15 +1217,6 @@ public abstract class Context { * @see android.hardware.SensorManager */ public static final String SENSOR_SERVICE = "sensor"; - /** - * Use with {@link #getSystemService} to retrieve a {@link - * android.bluetooth.BluetoothDevice} for interacting with Bluetooth. - * - * @see #getSystemService - * @see android.bluetooth.BluetoothDevice - * @hide - */ - public static final String BLUETOOTH_SERVICE = "bluetooth"; /** * Use with {@link #getSystemService} to retrieve a * com.android.server.WallpaperService for accessing wallpapers. diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 15612cefcc27ac851fc46bc78036ea21feb99b41..1b34320cab1fcd49182cb4a24ab6b5a04d498e8f 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -248,6 +248,14 @@ public class ContextWrapper extends Context { mBase.startActivity(intent); } + @Override + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + mBase.startIntentSender(intent, fillInIntent, flagsMask, + flagsValues, extraFlags); + } + @Override public void sendBroadcast(Intent intent) { mBase.sendBroadcast(intent); @@ -279,6 +287,16 @@ public class ContextWrapper extends Context { mBase.sendStickyBroadcast(intent); } + @Override + public void sendStickyOrderedBroadcast( + Intent intent, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + mBase.sendStickyOrderedBroadcast(intent, + resultReceiver, scheduler, initialCode, + initialData, initialExtras); + } + @Override public void removeStickyBroadcast(Intent intent) { mBase.removeStickyBroadcast(intent); diff --git a/core/java/android/content/Entity.aidl b/core/java/android/content/Entity.aidl new file mode 100644 index 0000000000000000000000000000000000000000..fb201f35c931cc05cdcb8b9880df364e251e0671 --- /dev/null +++ b/core/java/android/content/Entity.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/content/Entity.aidl +** +** 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 android.content; + +parcelable Entity; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java new file mode 100644 index 0000000000000000000000000000000000000000..ee8112e9da588b0b6e7014f34eea55175a5b0e29 --- /dev/null +++ b/core/java/android/content/Entity.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Parcelable; +import android.os.Parcel; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Objects that pass through the ContentProvider and ContentResolver's methods that deal with + * Entities must implement this abstract base class and thus themselves be Parcelable. + * @hide + */ +public final class Entity implements Parcelable { + final private ContentValues mValues; + final private ArrayList mSubValues; + + public Entity(ContentValues values) { + mValues = values; + mSubValues = new ArrayList(); + } + + public ContentValues getEntityValues() { + return mValues; + } + + public ArrayList getSubValues() { + return mSubValues; + } + + public void addSubValue(Uri uri, ContentValues values) { + mSubValues.add(new Entity.NamedContentValues(uri, values)); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + mValues.writeToParcel(dest, 0); + dest.writeInt(mSubValues.size()); + for (NamedContentValues value : mSubValues) { + value.uri.writeToParcel(dest, 0); + value.values.writeToParcel(dest, 0); + } + } + + private Entity(Parcel source) { + mValues = ContentValues.CREATOR.createFromParcel(source); + final int numValues = source.readInt(); + mSubValues = new ArrayList(numValues); + for (int i = 0; i < numValues; i++) { + final Uri uri = Uri.CREATOR.createFromParcel(source); + final ContentValues values = ContentValues.CREATOR.createFromParcel(source); + mSubValues.add(new NamedContentValues(uri, values)); + } + } + + public static final Creator CREATOR = new Creator() { + public Entity createFromParcel(Parcel source) { + return new Entity(source); + } + + public Entity[] newArray(int size) { + return new Entity[size]; + } + }; + + public static class NamedContentValues { + public final Uri uri; + public final ContentValues values; + + public NamedContentValues(Uri uri, ContentValues values) { + this.uri = uri; + this.values = values; + } + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Entity: ").append(getEntityValues()); + for (Entity.NamedContentValues namedValue : getSubValues()) { + sb.append("\n ").append(namedValue.uri); + sb.append("\n -> ").append(namedValue.values); + } + return sb.toString(); + } +} diff --git a/core/java/android/content/EntityIterator.java b/core/java/android/content/EntityIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..1b7343936cbc2d713238192fbbbdfc75921beb5b --- /dev/null +++ b/core/java/android/content/EntityIterator.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.RemoteException; + +/** + * @hide + */ +public interface EntityIterator { + /** + * Returns whether there are more elements to iterate, i.e. whether the + * iterator is positioned in front of an element. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + * @see #next + * @since Android 1.0 + */ + public boolean hasNext() throws RemoteException; + + /** + * Returns the next object in the iteration, i.e. returns the element in + * front of the iterator and advances the iterator by one position. + * + * @return the next object. + * @throws java.util.NoSuchElementException + * if there are no more elements. + * @see #hasNext + * @since Android 1.0 + */ + public Entity next() throws RemoteException; + + public void reset() throws RemoteException; + + /** + * Indicates that this iterator is no longer needed and that any associated resources + * may be released (such as a SQLite cursor). + */ + public void close(); +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 06069569db974db104ca3ea49c689e34f4dfc377..0798adf1e20702de1b5561235fde31e09dc227b0 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -28,6 +28,7 @@ import android.os.IInterface; import android.os.ParcelFileDescriptor; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * The ipc interface to talk to a content provider. @@ -43,6 +44,12 @@ public interface IContentProvider extends IInterface { CursorWindow window) throws RemoteException; public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException; + /** + * @hide + */ + public EntityIterator queryEntities(Uri url, String selection, + String[] selectionArgs, String sortOrder) + throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; @@ -55,7 +62,8 @@ public interface IContentProvider extends IInterface { throws RemoteException, FileNotFoundException; public AssetFileDescriptor openAssetFile(Uri url, String mode) throws RemoteException, FileNotFoundException; - public ISyncAdapter getSyncAdapter() throws RemoteException; + public ContentProviderResult[] applyBatch(ArrayList operations) + throws RemoteException, OperationApplicationException; /* IPC constants */ static final String descriptor = "android.content.IContentProvider"; @@ -65,8 +73,12 @@ public interface IContentProvider extends IInterface { static final int INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; static final int DELETE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; static final int UPDATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9; - static final int GET_SYNC_ADAPTER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10; static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12; static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13; static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; + /** + * @hide + */ + static final int QUERY_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 18; + static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19; } diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 8617d949c4fbe603a1391c457a2972178d5fa87b..b0f14c15cbb493fe37b3ab27ac0a4b25210dab71 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -16,8 +16,10 @@ package android.content; +import android.accounts.Account; import android.content.ActiveSyncInfo; import android.content.ISyncStatusObserver; +import android.content.SyncAdapterType; import android.content.SyncStatusInfo; import android.net.Uri; import android.os.Bundle; @@ -34,15 +36,15 @@ interface IContentService { void notifyChange(in Uri uri, IContentObserver observer, boolean observerWantsSelfNotifications, boolean syncToNetwork); - void startSync(in Uri url, in Bundle extras); - void cancelSync(in Uri uri); + void requestSync(in Account account, String authority, in Bundle extras); + void cancelSync(in Account account, String authority); /** * Check if the provider should be synced when a network tickle is received * @param providerName the provider whose setting we are querying * @return true of the provider should be synced when a network tickle is received */ - boolean getSyncProviderAutomatically(String providerName); + boolean getSyncAutomatically(in Account account, String providerName); /** * Set whether or not the provider is synced when it receives a network tickle. @@ -50,32 +52,50 @@ interface IContentService { * @param providerName the provider whose behavior is being controlled * @param sync true if the provider should be synced when tickles are received for it */ - void setSyncProviderAutomatically(String providerName, boolean sync); + void setSyncAutomatically(in Account account, String providerName, boolean sync); - void setListenForNetworkTickles(boolean flag); + /** + * Check if this account/provider is syncable. + * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. + */ + int getIsSyncable(in Account account, String providerName); + + /** + * Set whether this account/provider is syncable. + * @param syncable, >0 denotes syncable, 0 means not syncable, <0 means unknown + */ + void setIsSyncable(in Account account, String providerName, int syncable); - boolean getListenForNetworkTickles(); + void setMasterSyncAutomatically(boolean flag); + + boolean getMasterSyncAutomatically(); /** * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - boolean isSyncActive(String account, String authority); + boolean isSyncActive(in Account account, String authority); ActiveSyncInfo getActiveSync(); + /** + * Returns the types of the SyncAdapters that are registered with the system. + * @return Returns the types of the SyncAdapters that are registered with the system. + */ + SyncAdapterType[] getSyncAdapterTypes(); + /** * Returns the status that matches the authority. If there are multiples accounts for * the authority, the one with the latest "lastSuccessTime" status is returned. * @param authority the authority whose row should be selected * @return the SyncStatusInfo for the authority, or null if none exists */ - SyncStatusInfo getStatusByAuthority(String authority); + SyncStatusInfo getSyncStatus(in Account account, String authority); /** * Return true if the pending status is true of any matching authorities. */ - boolean isAuthorityPending(String account, String authority); + boolean isSyncPending(in Account account, String authority); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IEntityIterator.java b/core/java/android/content/IEntityIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..068581e965518ee73f106570cbb572d86ac90eba --- /dev/null +++ b/core/java/android/content/IEntityIterator.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Log; + +/** + * ICPC interface methods for an iterator over Entity objects. + * @hide + */ +public interface IEntityIterator extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IEntityIterator { + private static final String TAG = "IEntityIterator"; + private static final java.lang.String DESCRIPTOR = "android.content.IEntityIterator"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + /** + * Cast an IBinder object into an IEntityIterator interface, + * generating a proxy if needed. + */ + public static IEntityIterator asInterface(IBinder obj) { + if ((obj==null)) { + return null; + } + IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof IEntityIterator))) { + return ((IEntityIterator)iin); + } + return new IEntityIterator.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: + { + reply.writeString(DESCRIPTOR); + return true; + } + + case TRANSACTION_hasNext: + { + data.enforceInterface(DESCRIPTOR); + boolean _result; + try { + _result = this.hasNext(); + } catch (Exception e) { + Log.e(TAG, "caught exception in hasNext()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + reply.writeInt(((_result)?(1):(0))); + return true; + } + + case TRANSACTION_next: + { + data.enforceInterface(DESCRIPTOR); + Entity entity; + try { + entity = this.next(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in next()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + entity.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + return true; + } + + case TRANSACTION_reset: + { + data.enforceInterface(DESCRIPTOR); + try { + this.reset(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in next()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + return true; + } + + case TRANSACTION_close: + { + data.enforceInterface(DESCRIPTOR); + try { + this.close(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in close()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IEntityIterator { + private IBinder mRemote; + Proxy(IBinder remote) { + mRemote = remote; + } + public IBinder asBinder() { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + public boolean hasNext() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_hasNext, _data, _reply, 0); + _reply.readException(); + _result = (0!=_reply.readInt()); + } + finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public Entity next() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_next, _data, _reply, 0); + _reply.readException(); + return Entity.CREATOR.createFromParcel(_reply); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void reset() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_reset, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void close() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_close, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_hasNext = (IBinder.FIRST_CALL_TRANSACTION + 0); + static final int TRANSACTION_next = (IBinder.FIRST_CALL_TRANSACTION + 1); + static final int TRANSACTION_close = (IBinder.FIRST_CALL_TRANSACTION + 2); + static final int TRANSACTION_reset = (IBinder.FIRST_CALL_TRANSACTION + 3); + } + public boolean hasNext() throws RemoteException; + public Entity next() throws RemoteException; + public void reset() throws RemoteException; + public void close() throws RemoteException; +} diff --git a/core/java/android/content/IIntentReceiver.aidl b/core/java/android/content/IIntentReceiver.aidl index 443db2d06d0f603e5a5a9490bd9187d439262a89..6f2f7c4053b4831f4ee8f83fd125e356b43d9f29 100755 --- a/core/java/android/content/IIntentReceiver.aidl +++ b/core/java/android/content/IIntentReceiver.aidl @@ -28,6 +28,6 @@ import android.os.Bundle; */ oneway interface IIntentReceiver { void performReceive(in Intent intent, int resultCode, - String data, in Bundle extras, boolean ordered); + String data, in Bundle extras, boolean ordered, boolean sticky); } diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl index 671188cd50c57c38f6819cfbfa941aa8b809a0da..4660527925c525ad739ad23fd190e3eddaf62320 100644 --- a/core/java/android/content/ISyncAdapter.aidl +++ b/core/java/android/content/ISyncAdapter.aidl @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.os.Bundle; import android.content.ISyncContext; @@ -30,14 +31,17 @@ oneway interface ISyncAdapter { * * @param syncContext the ISyncContext used to indicate the progress of the sync. When * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. + * @param authority the authority that should be synced * @param account the account that should be synced * @param extras SyncAdapter-specific parameters */ - void startSync(ISyncContext syncContext, String account, in Bundle extras); + void startSync(ISyncContext syncContext, String authority, + in Account account, in Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive * after the ISyncContext.onFinished() for that sync was called. + * @param syncContext the ISyncContext that was passed to {@link #startSync} */ - void cancelSync(); + void cancelSync(ISyncContext syncContext); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c62d66bc5532900f56270222cb2a40051d1daa77..b785dbf76afcc9c11c7beb390960daadfc1d6628 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -75,10 +75,10 @@ import java.util.Set; *

    Some examples of action/data pairs are:

    * *
      - *
    • {@link #ACTION_VIEW} content://contacts/1 -- Display + *

    • {@link #ACTION_VIEW} content://contacts/people/1 -- Display * information about the person whose identifier is "1".

      *
    • - *
    • {@link #ACTION_DIAL} content://contacts/1 -- Display + *

    • {@link #ACTION_DIAL} content://contacts/people/1 -- Display * the phone dialer with the person filled in.

      *
    • *
    • {@link #ACTION_VIEW} tel:123 -- Display @@ -89,10 +89,10 @@ import java.util.Set; *

    • {@link #ACTION_DIAL} tel:123 -- Display * the phone dialer with the given number filled in.

      *
    • - *
    • {@link #ACTION_EDIT} content://contacts/1 -- Edit + *

    • {@link #ACTION_EDIT} content://contacts/people/1 -- Edit * information about the person whose identifier is "1".

      *
    • - *
    • {@link #ACTION_VIEW} content://contacts/ -- Display + *

    • {@link #ACTION_VIEW} content://contacts/people/ -- Display * a list of people, which the user can browse through. This example is a * typical top-level entry into the Contacts application, showing you the * list of people. Selecting a particular person to view would result in a @@ -156,7 +156,7 @@ import java.util.Set; * defined in the Intent class, but applications can also define their own. * These strings use java style scoping, to ensure they are unique -- for * example, the standard {@link #ACTION_VIEW} is called - * "android.app.action.VIEW".

      + * "android.intent.action.VIEW".

      * *

      Put together, the set of actions, data types, categories, and extra data * defines a language for the system allowing for the expression of phrases @@ -347,7 +347,7 @@ import java.util.Set; *

    • { action=android.app.action.MAIN, * category=android.app.category.LAUNCHER } is the actual intent * used by the Launcher to populate its top-level list.

      - *
    • { action=android.app.action.VIEW + *

    • { action=android.intent.action.VIEW * data=content://com.google.provider.NotePad/notes } * displays a list of all the notes under * "content://com.google.provider.NotePad/notes", which @@ -399,7 +399,7 @@ import java.util.Set; * NoteEditor activity:

      * *
        - *
      • { action=android.app.action.VIEW + *

      • { action=android.intent.action.VIEW * data=content://com.google.provider.NotePad/notes/{ID} } * shows the user the content of note {ID}.

        *
      • { action=android.app.action.EDIT @@ -529,6 +529,8 @@ import java.util.Set; *

      • {@link #CATEGORY_HOME} *
      • {@link #CATEGORY_PREFERENCE} *
      • {@link #CATEGORY_TEST} + *
      • {@link #CATEGORY_CAR_DOCK} + *
      • {@link #CATEGORY_DESK_DOCK} *
      * *

      Standard Extra Data

      @@ -923,6 +925,16 @@ public class Intent implements Parcelable { * get*ArrayListExtra can have either a {@link #EXTRA_TEXT} or {@link * #EXTRA_STREAM} field, containing the data to be sent. *

      + * Multiple types are supported, and receivers should handle mixed types + * whenever possible. The right way for the receiver to check them is to + * use the content resolver on each URI. The intent sender should try to + * put the most concrete mime type in the intent type, but it can fall + * back to {@literal /*} or {@literal *}/* as needed. + *

      + * e.g. if you are sending image/jpg and image/jpg, the intent's type can + * be image/jpg, but if you are sending image/jpg and image/png, then the + * intent's type should be image/*. + *

      * Optional standard extras, which may be interpreted by some recipients as * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC}, * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}. @@ -1267,6 +1279,8 @@ public class Intent implements Parcelable { * enabled or disabled. The data contains the name of the package. *

        *
      • {@link #EXTRA_UID} containing the integer uid assigned to the package. + *
      • {@link #EXTRA_CHANGED_COMPONENT_NAME} containing the class name of the changed component. + *
      • {@link #EXTRA_DONT_KILL_APP} containing boolean field to override the default action of restarting the application. *
      * *

      This is a protected intent that can only be sent @@ -1316,7 +1330,7 @@ public class Intent implements Parcelable { public static final String ACTION_UID_REMOVED = "android.intent.action.UID_REMOVED"; /** * Broadcast Action: The current system wallpaper has changed. See - * {@link Context#getWallpaper} for retrieving the new wallpaper. + * {@link android.app.WallpaperManager} for retrieving the new wallpaper. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED"; @@ -1338,14 +1352,20 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED"; /** - * Broadcast Action: The charging state, or charge level of the battery has - * changed. + * Broadcast Action: This is a sticky broadcast containing the + * charging state, level, and other information about the battery. + * See {@link android.os.BatteryManager} for documentation on the + * contents of the Intent. * *

      * You can not receive this through components declared * in manifests, only by explicitly registering for it with * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) - * Context.registerReceiver()}. + * Context.registerReceiver()}. See {@link #ACTION_BATTERY_LOW}, + * {@link #ACTION_BATTERY_OKAY}, {@link #ACTION_POWER_CONNECTED}, + * and {@link #ACTION_POWER_DISCONNECTED} for distinct battery-related + * broadcasts that are sent and can be received through manifest + * receivers. * *

      This is a protected intent that can only be sent * by the system. @@ -1394,13 +1414,14 @@ public class Intent implements Parcelable { * by the system. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED"; + public static final String ACTION_POWER_DISCONNECTED = + "android.intent.action.ACTION_POWER_DISCONNECTED"; /** * Broadcast Action: Device is shutting down. * This is broadcast when the device is being shut down (completely turned * off, not sleeping). Once the broadcast is complete, the final shutdown * will proceed and all unsaved data lost. Apps will not normally need - * to handle this, since the forground activity will be paused as well. + * to handle this, since the foreground activity will be paused as well. * *

      This is a protected intent that can only be sent * by the system. @@ -1408,7 +1429,19 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_SHUTDOWN = "android.intent.action.ACTION_SHUTDOWN"; /** - * Broadcast Action: Indicates low memory condition on the device + * Activity Action: Start this activity to request system shutdown. + * The optional boolean extra field {@link #EXTRA_KEY_CONFIRM} can be set to true + * to request confirmation from the user before shutting down. + * + *

      This is a protected intent that can only be sent + * by the system. + * + * {@hide} + */ + public static final String ACTION_REQUEST_SHUTDOWN = "android.intent.action.ACTION_REQUEST_SHUTDOWN"; + /** + * Broadcast Action: A sticky broadcast that indicates low memory + * condition on the device * *

      This is a protected intent that can only be sent * by the system. @@ -1684,6 +1717,44 @@ public class Intent implements Parcelable { public static final String ACTION_REBOOT = "android.intent.action.REBOOT"; + /** + * Broadcast Action: A sticky broadcast indicating the phone was docked + * or undocked. Includes the extra + * field {@link #EXTRA_DOCK_STATE}, containing the current dock state. + * This is intended for monitoring the current dock state. + * To launch an activity from a dock state change, use {@link #CATEGORY_CAR_DOCK} + * or {@link #CATEGORY_DESK_DOCK} instead. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DOCK_EVENT = + "android.intent.action.DOCK_EVENT"; + + /** + * Broadcast Action: a remote intent is to be broadcasted. + * + * A remote intent is used for remote RPC between devices. The remote intent + * is serialized and sent from one device to another device. The receiving + * device parses the remote intent and broadcasts it. Note that anyone can + * broadcast a remote intent. However, if the intent receiver of the remote intent + * does not trust intent broadcasts from arbitrary intent senders, it should require + * the sender to hold certain permissions so only trusted sender's broadcast will be + * let through. + * @hide + */ + public static final String ACTION_REMOTE_INTENT = + "android.intent.action.REMOTE_INTENT"; + + /** + * Broadcast Action: hook for permforming cleanup after a system update. + * + * The broadcast is sent when the system is booting, before the + * BOOT_COMPLETED broadcast. It is only sent to receivers in the system + * image. A receiver for this should do its work and then disable itself + * so that it does not get run again at the next boot. + * @hide + */ + public static final String ACTION_PRE_BOOT_COMPLETED = + "android.intent.action.PRE_BOOT_COMPLETED"; // --------------------------------------------------------------------- // --------------------------------------------------------------------- @@ -1812,6 +1883,21 @@ public class Intent implements Parcelable { */ public static final String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST = "android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"; + /** + * An activity to run when device is inserted into a car dock. + * Used with {@link #ACTION_MAIN} to launch an activity. + * To monitor dock state, use {@link #ACTION_DOCK_EVENT} instead. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_CAR_DOCK = "android.intent.category.CAR_DOCK"; + /** + * An activity to run when device is inserted into a car dock. + * Used with {@link #ACTION_MAIN} to launch an activity. + * To monitor dock state, use {@link #ACTION_DOCK_EVENT} instead. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_DESK_DOCK = "android.intent.category.DESK_DOCK"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard extra data keys. @@ -1871,12 +1957,29 @@ public class Intent implements Parcelable { */ public static final String EXTRA_TITLE = "android.intent.extra.TITLE"; + /** + * A Parcelable[] of {@link Intent} or + * {@link android.content.pm.LabeledIntent} objects as set with + * {@link #putExtra(String, Parcelable[])} of additional activities to place + * a the front of the list of choices, when shown to the user with a + * {@link #ACTION_CHOOSER}. + */ + public static final String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; + /** * A {@link android.view.KeyEvent} object containing the event that * triggered the creation of the Intent it is in. */ public static final String EXTRA_KEY_EVENT = "android.intent.extra.KEY_EVENT"; + /** + * Set to true in {@link #ACTION_REQUEST_SHUTDOWN} to request confirmation from the user + * before shutting down. + * + * {@hide} + */ + public static final String EXTRA_KEY_CONFIRM = "android.intent.extra.KEY_CONFIRM"; + /** * Used as an boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} or * {@link android.content.Intent#ACTION_PACKAGE_CHANGED} intents to override the default action @@ -1925,6 +2028,39 @@ public class Intent implements Parcelable { */ public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT"; + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_DOCK_EVENT} + * intents to request the dock state. Possible values are + * {@link android.content.Intent#EXTRA_DOCK_STATE_UNDOCKED}, + * {@link android.content.Intent#EXTRA_DOCK_STATE_DESK}, or + * {@link android.content.Intent#EXTRA_DOCK_STATE_CAR}. + */ + public static final String EXTRA_DOCK_STATE = "android.intent.extra.DOCK_STATE"; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is not in any dock. + */ + public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is in a desk dock. + */ + public static final int EXTRA_DOCK_STATE_DESK = 1; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is in a car dock. + */ + public static final int EXTRA_DOCK_STATE_CAR = 2; + + /** + * Boolean that can be supplied as meta-data with a dock activity, to + * indicate that the dock should take over the home key when it is active. + */ + public static final String METADATA_DOCK_HOME = "android.dock_home"; + /** * Used as a parcelable extra field in {@link #ACTION_APP_ERROR}, containing * the bug report. @@ -1943,6 +2079,39 @@ public class Intent implements Parcelable { public static final String EXTRA_INSTALLER_PACKAGE_NAME = "android.intent.extra.INSTALLER_PACKAGE_NAME"; + /** + * Used in the extra field in the remote intent. It's astring token passed with the + * remote intent. + */ + public static final String EXTRA_REMOTE_INTENT_TOKEN = + "android.intent.extra.remote_intent_token"; + + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_PACKAGE_CHANGED} + * intent to supply the name of the component that changed. + * + */ + public static final String EXTRA_CHANGED_COMPONENT_NAME = + "android.intent.extra.changed_component_name"; + + /** + * @hide + * Magic extra system code can use when binding, to give a label for + * who it is that has bound to a service. This is an integer giving + * a framework string resource that can be displayed to the user. + */ + public static final String EXTRA_CLIENT_LABEL = + "android.intent.extra.client_label"; + + /** + * @hide + * Magic extra system code can use when binding, to give a PendingIntent object + * that can be launched for the user to disable the system's use of this + * service. + */ + public static final String EXTRA_CLIENT_INTENT = + "android.intent.extra.client_intent"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). @@ -2037,12 +2206,14 @@ public class Intent implements Parcelable { * of activity B, then C and D will be finished and B receive the given * Intent, resulting in the stack now being: A, B. * - *

      The currently running instance of task B in the above example will + *

      The currently running instance of activity B in the above example will * either receive the new intent you are starting here in its * onNewIntent() method, or be itself finished and restarted with the * new intent. If it has declared its launch mode to be "multiple" (the - * default) it will be finished and re-created; for all other launch modes - * it will receive the Intent in the current instance. + * default) and you have not set {@link #FLAG_ACTIVITY_SINGLE_TOP} in + * the same intent, then it will be finished and re-created; for all other + * launch modes or if {@link #FLAG_ACTIVITY_SINGLE_TOP} is set then this + * Intent will be delivered to the current instance's onNewIntent(). * *

      This launch mode can also be used to good effect in conjunction with * {@link #FLAG_ACTIVITY_NEW_TASK}: if used to start the root activity @@ -2152,6 +2323,18 @@ public class Intent implements Parcelable { * specified. */ public static final int FLAG_ACTIVITY_REORDER_TO_FRONT = 0X00020000; + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will prevent the system from applying an activity transition + * animation to go to the next activity state. This doesn't mean an + * animation will never run -- if another activity change happens that doesn't + * specify this flag before the activity started here is displayed, then + * that transition will be used. This this flag can be put to good use + * when you are going to do a series of activity operations but the + * animation seen by the user shouldn't be driven by the first activity + * change but rather a later one. + */ + public static final int FLAG_ACTIVITY_NO_ANIMATION = 0X00010000; /** * If set, when sending a broadcast only registered receivers will be * called -- no BroadcastReceiver components will be launched. @@ -2171,7 +2354,21 @@ public class Intent implements Parcelable { * @hide */ public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x20000000; + /** + * Set when this broadcast is for a boot upgrade, a special mode that + * allows the broadcast to be sent before the system is ready and launches + * the app process with no providers running in it. + * @hide + */ + public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x10000000; + /** + * @hide Flags that can't be changed with PendingIntent. + */ + public static final int IMMUTABLE_FLAGS = + FLAG_GRANT_READ_URI_PERMISSION + | FLAG_GRANT_WRITE_URI_PERMISSION; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // toUri() and parseUri() options. @@ -2352,6 +2549,7 @@ public class Intent implements Parcelable { * * @param uri The URI to turn into an Intent. * @param flags Additional processing flags. Either 0 or + * {@link #URI_INTENT_SCHEME}. * * @return Intent The newly created Intent object. * @@ -2485,24 +2683,24 @@ public class Intent implements Parcelable { int i = uri.lastIndexOf('#'); if (i >= 0) { - Uri data = null; String action = null; - if (i > 0) { - data = Uri.parse(uri.substring(0, i)); - } + final int intentFragmentStart = i; + boolean isIntentFragment = false; i++; if (uri.regionMatches(i, "action(", 0, 7)) { + isIntentFragment = true; i += 7; int j = uri.indexOf(')', i); action = uri.substring(i, j); i = j + 1; } - intent = new Intent(action, data); + intent = new Intent(action); if (uri.regionMatches(i, "categories(", 0, 11)) { + isIntentFragment = true; i += 11; int j = uri.indexOf(')', i); while (i < j) { @@ -2517,6 +2715,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "type(", 0, 5)) { + isIntentFragment = true; i += 5; int j = uri.indexOf(')', i); intent.mType = uri.substring(i, j); @@ -2524,6 +2723,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "launchFlags(", 0, 12)) { + isIntentFragment = true; i += 12; int j = uri.indexOf(')', i); intent.mFlags = Integer.decode(uri.substring(i, j)).intValue(); @@ -2531,6 +2731,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "component(", 0, 10)) { + isIntentFragment = true; i += 10; int j = uri.indexOf(')', i); int sep = uri.indexOf('!', i); @@ -2543,6 +2744,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "extras(", 0, 7)) { + isIntentFragment = true; i += 7; final int closeParen = uri.indexOf(')', i); @@ -2614,6 +2816,12 @@ public class Intent implements Parcelable { } } + if (isIntentFragment) { + intent.mData = Uri.parse(uri.substring(0, intentFragmentStart)); + } else { + intent.mData = Uri.parse(uri); + } + if (intent.mAction == null) { // By default, if no action is specified, then use VIEW. intent.mAction = ACTION_VIEW; @@ -3403,7 +3611,7 @@ public class Intent implements Parcelable { } } else { ResolveInfo info = pm.resolveActivity( - this, PackageManager.MATCH_DEFAULT_ONLY); + this, PackageManager.MATCH_DEFAULT_ONLY | flags); if (info != null) { ai = info.activityInfo; } @@ -4929,7 +5137,8 @@ public class Intent implements Parcelable { } }; - private Intent(Parcel in) { + /** @hide */ + protected Intent(Parcel in) { readFromParcel(in); } diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java index 0e4d98498e8fe44a042fc2198135f705640df2f1..e182021f8ef1b76c08856b476720a7f7d8338105 100644 --- a/core/java/android/content/IntentSender.java +++ b/core/java/android/content/IntentSender.java @@ -113,7 +113,7 @@ public class IntentSender implements Parcelable { mHandler = handler; } public void performReceive(Intent intent, int resultCode, - String data, Bundle extras, boolean serialized) { + String data, Bundle extras, boolean serialized, boolean sticky) { mIntent = intent; mResultCode = resultCode; mResultData = data; @@ -248,6 +248,11 @@ public class IntentSender implements Parcelable { return b != null ? new IntentSender(b) : null; } + /** @hide */ + public IIntentSender getTarget() { + return mTarget; + } + /** @hide */ public IntentSender(IIntentSender target) { mTarget = target; diff --git a/core/java/android/content/OperationApplicationException.java b/core/java/android/content/OperationApplicationException.java new file mode 100644 index 0000000000000000000000000000000000000000..2fc19bb5ca69d56f84b8906f2557400b088a9ace --- /dev/null +++ b/core/java/android/content/OperationApplicationException.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +/** + * Thrown when an application of a {@link ContentProviderOperation} fails due the specified + * constraints. + */ +public class OperationApplicationException extends Exception { + private final int mNumSuccessfulYieldPoints; + + public OperationApplicationException() { + super(); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(String message) { + super(message); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(String message, Throwable cause) { + super(message, cause); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(Throwable cause) { + super(cause); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(int numSuccessfulYieldPoints) { + super(); + mNumSuccessfulYieldPoints = numSuccessfulYieldPoints; + } + public OperationApplicationException(String message, int numSuccessfulYieldPoints) { + super(message); + mNumSuccessfulYieldPoints = numSuccessfulYieldPoints; + } + + public int getNumSuccessfulYieldPoints() { + return mNumSuccessfulYieldPoints; + } +} diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 7826e5052ab7c7164bbf08835fa1691ca01d937f..88dc3320d35156baaf9298f69c1f9dd240753a4f 100644 --- a/core/java/android/content/SyncAdapter.java +++ b/core/java/android/content/SyncAdapter.java @@ -18,6 +18,7 @@ package android.content; import android.os.Bundle; import android.os.RemoteException; +import android.accounts.Account; /** * @hide @@ -29,12 +30,12 @@ public abstract class SyncAdapter { public static final int LOG_SYNC_DETAILS = 2743; class Transport extends ISyncAdapter.Stub { - public void startSync(ISyncContext syncContext, String account, + public void startSync(ISyncContext syncContext, String authority, Account account, Bundle extras) throws RemoteException { - SyncAdapter.this.startSync(new SyncContext(syncContext), account, extras); + SyncAdapter.this.startSync(new SyncContext(syncContext), account, authority, extras); } - public void cancelSync() throws RemoteException { + public void cancelSync(ISyncContext syncContext) throws RemoteException { SyncAdapter.this.cancelSync(); } } @@ -42,9 +43,9 @@ public abstract class SyncAdapter { Transport mTransport = new Transport(); /** - * Get the Transport object. (note this is package private). + * Get the Transport object. */ - final ISyncAdapter getISyncAdapter() + public final ISyncAdapter getISyncAdapter() { return mTransport; } @@ -57,9 +58,11 @@ public abstract class SyncAdapter { * @param syncContext the ISyncContext used to indicate the progress of the sync. When * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. * @param account the account that should be synced + * @param authority the authority if the sync request * @param extras SyncAdapter-specific parameters */ - public abstract void startSync(SyncContext syncContext, String account, Bundle extras); + public abstract void startSync(SyncContext syncContext, Account account, String authority, + Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive diff --git a/core/java/android/content/SyncAdapterType.aidl b/core/java/android/content/SyncAdapterType.aidl new file mode 100644 index 0000000000000000000000000000000000000000..e67841f6ee95c72af47f18dc50b4896db91bfe6d --- /dev/null +++ b/core/java/android/content/SyncAdapterType.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +parcelable SyncAdapterType; + diff --git a/core/java/android/content/SyncAdapterType.java b/core/java/android/content/SyncAdapterType.java new file mode 100644 index 0000000000000000000000000000000000000000..25cbdb195f5eea291447650ae33782a3657cbd36 --- /dev/null +++ b/core/java/android/content/SyncAdapterType.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.text.TextUtils; +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Value type that represents a SyncAdapterType. This object overrides {@link #equals} and + * {@link #hashCode}, making it suitable for use as the key of a {@link java.util.Map} + */ +public class SyncAdapterType implements Parcelable { + public final String authority; + public final String accountType; + public final boolean isKey; + private final boolean userVisible; + private final boolean supportsUploading; + + public SyncAdapterType(String authority, String accountType, boolean userVisible, + boolean supportsUploading) { + if (TextUtils.isEmpty(authority)) { + throw new IllegalArgumentException("the authority must not be empty: " + authority); + } + if (TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("the accountType must not be empty: " + accountType); + } + this.authority = authority; + this.accountType = accountType; + this.userVisible = userVisible; + this.supportsUploading = supportsUploading; + this.isKey = false; + } + + private SyncAdapterType(String authority, String accountType) { + if (TextUtils.isEmpty(authority)) { + throw new IllegalArgumentException("the authority must not be empty: " + authority); + } + if (TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("the accountType must not be empty: " + accountType); + } + this.authority = authority; + this.accountType = accountType; + this.userVisible = true; + this.supportsUploading = true; + this.isKey = true; + } + + public boolean supportsUploading() { + if (isKey) { + throw new IllegalStateException( + "this method is not allowed to be called when this is a key"); + } + return supportsUploading; + } + + public boolean isUserVisible() { + if (isKey) { + throw new IllegalStateException( + "this method is not allowed to be called when this is a key"); + } + return userVisible; + } + + public static SyncAdapterType newKey(String authority, String accountType) { + return new SyncAdapterType(authority, accountType); + } + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof SyncAdapterType)) return false; + final SyncAdapterType other = (SyncAdapterType)o; + // don't include userVisible or supportsUploading in the equality check + return authority.equals(other.authority) && accountType.equals(other.accountType); + } + + public int hashCode() { + int result = 17; + result = 31 * result + authority.hashCode(); + result = 31 * result + accountType.hashCode(); + // don't include userVisible or supportsUploading the hash + return result; + } + + public String toString() { + if (isKey) { + return "SyncAdapterType Key {name=" + authority + + ", type=" + accountType + + "}"; + } else { + return "SyncAdapterType {name=" + authority + + ", type=" + accountType + + ", userVisible=" + userVisible + + ", supportsUploading=" + supportsUploading + + "}"; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + if (isKey) { + throw new IllegalStateException("keys aren't parcelable"); + } + + dest.writeString(authority); + dest.writeString(accountType); + dest.writeInt(userVisible ? 1 : 0); + dest.writeInt(supportsUploading ? 1 : 0); + } + + public SyncAdapterType(Parcel source) { + this( + source.readString(), + source.readString(), + source.readInt() != 0, + source.readInt() != 0); + } + + public static final Creator CREATOR = new Creator() { + public SyncAdapterType createFromParcel(Parcel source) { + return new SyncAdapterType(source); + } + + public SyncAdapterType[] newArray(int size) { + return new SyncAdapterType[size]; + } + }; +} \ No newline at end of file diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java new file mode 100644 index 0000000000000000000000000000000000000000..6ade83781470c88b0a5f2d04955f103f8f82f653 --- /dev/null +++ b/core/java/android/content/SyncAdaptersCache.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.content.pm.RegisteredServicesCache; +import android.content.pm.XmlSerializerAndParser; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A cache of services that export the {@link android.content.ISyncAdapter} interface. + * @hide + */ +/* package private */ class SyncAdaptersCache extends RegisteredServicesCache { + private static final String TAG = "Account"; + + private static final String SERVICE_INTERFACE = "android.content.SyncAdapter"; + private static final String SERVICE_META_DATA = "android.content.SyncAdapter"; + private static final String ATTRIBUTES_NAME = "sync-adapter"; + private static final MySerializer sSerializer = new MySerializer(); + + SyncAdaptersCache(Context context) { + super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, sSerializer); + } + + public SyncAdapterType parseServiceAttributes(String packageName, AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.SyncAdapter); + try { + final String authority = + sa.getString(com.android.internal.R.styleable.SyncAdapter_contentAuthority); + final String accountType = + sa.getString(com.android.internal.R.styleable.SyncAdapter_accountType); + if (authority == null || accountType == null) { + return null; + } + final boolean userVisible = + sa.getBoolean(com.android.internal.R.styleable.SyncAdapter_userVisible, true); + final boolean supportsUploading = + sa.getBoolean(com.android.internal.R.styleable.SyncAdapter_supportsUploading, + true); + return new SyncAdapterType(authority, accountType, userVisible, supportsUploading); + } finally { + sa.recycle(); + } + } + + static class MySerializer implements XmlSerializerAndParser { + public void writeAsXml(SyncAdapterType item, XmlSerializer out) throws IOException { + out.attribute(null, "authority", item.authority); + out.attribute(null, "accountType", item.accountType); + } + + public SyncAdapterType createFromXml(XmlPullParser parser) + throws IOException, XmlPullParserException { + final String authority = parser.getAttributeValue(null, "authority"); + final String accountType = parser.getAttributeValue(null, "accountType"); + return SyncAdapterType.newKey(authority, accountType); + } + } +} \ No newline at end of file diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java index f4faa041421da571f317ab670bea444cbc0eea04..587586d8a6f4a74e072f5bdab5e34106f501f7de 100644 --- a/core/java/android/content/SyncContext.java +++ b/core/java/android/content/SyncContext.java @@ -18,16 +18,17 @@ package android.content; import android.os.RemoteException; import android.os.SystemClock; +import android.os.IBinder; -/** - * @hide - */ public class SyncContext { private ISyncContext mSyncContext; private long mLastHeartbeatSendTime; private static final long HEARTBEAT_SEND_INTERVAL_IN_MS = 1000; + /** + * @hide + */ public SyncContext(ISyncContext syncContextInterface) { mSyncContext = syncContextInterface; mLastHeartbeatSendTime = 0; @@ -38,6 +39,8 @@ public class SyncContext { * {@link #updateHeartbeat}, so it also takes the place of a call to that. * * @param message the current status message for this sync + * + * @hide */ public void setStatusText(String message) { updateHeartbeat(); @@ -48,7 +51,7 @@ public class SyncContext { * downloads or sends records to/from the server, this may be called after each record * is downloaded or uploaded. */ - public void updateHeartbeat() { + private void updateHeartbeat() { final long now = SystemClock.elapsedRealtime(); if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return; try { @@ -67,7 +70,7 @@ public class SyncContext { } } - public ISyncContext getISyncContext() { - return mSyncContext; + public IBinder getSyncContextBinder() { + return mSyncContext.asBinder(); } } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 4d2cce8818976286af62f651fa6176ef7eb22839..ba186159680d5d9fd2eec56611f3b47c13b0cdc6 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -21,8 +21,9 @@ import com.google.android.collect.Maps; import com.android.internal.R; import com.android.internal.util.ArrayUtils; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdateListener; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,11 +31,11 @@ import android.app.PendingIntent; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; +import android.content.pm.RegisteredServicesCache; +import android.content.pm.ProviderInfo; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -48,7 +49,6 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; -import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; @@ -72,11 +72,13 @@ import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; /** * @hide */ -class SyncManager { +class SyncManager implements OnAccountsUpdateListener { private static final String TAG = "SyncManager"; // used during dumping of the Sync history @@ -86,13 +88,37 @@ class SyncManager { private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4; /** Delay a sync due to local changes this long. In milliseconds */ - private static final long LOCAL_SYNC_DELAY = 30 * 1000; // 30 seconds + private static final long LOCAL_SYNC_DELAY; /** * If a sync takes longer than this and the sync queue is not empty then we will * cancel it and add it back to the end of the sync queue. In milliseconds. */ - private static final long MAX_TIME_PER_SYNC = 5 * 60 * 1000; // 5 minutes + private static final long MAX_TIME_PER_SYNC; + + static { + String localSyncDelayString = SystemProperties.get("sync.local_sync_delay"); + long localSyncDelay = 30 * 1000; // 30 seconds + if (localSyncDelayString != null) { + try { + localSyncDelay = Long.parseLong(localSyncDelayString); + } catch (NumberFormatException nfe) { + // ignore, use default + } + } + LOCAL_SYNC_DELAY = localSyncDelay; + + String maxTimePerSyncString = SystemProperties.get("sync.max_time_per_sync"); + long maxTimePerSync = 5 * 60 * 1000; // 5 minutes + if (maxTimePerSyncString != null) { + try { + maxTimePerSync = Long.parseLong(maxTimePerSyncString); + } catch (NumberFormatException nfe) { + // ignore, use default + } + } + MAX_TIME_PER_SYNC = maxTimePerSync; + } private static final long SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds @@ -117,14 +143,11 @@ class SyncManager { private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; - private ContentResolver mContentResolver; private String mStatusText = ""; private long mHeartbeatTime = 0; - private AccountMonitor mAccountMonitor; - - private volatile String[] mAccounts = null; + private volatile Account[] mAccounts = null; volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; @@ -150,18 +173,22 @@ class SyncManager { private volatile boolean mSyncPollInitialized; private final PendingIntent mSyncAlarmIntent; private final PendingIntent mSyncPollAlarmIntent; + // Synchronized on "this". Instead of using this directly one should instead call + // its accessor, getConnManager(). + private ConnectivityManager mConnManagerDoNotUseDirectly; + + private final SyncAdaptersCache mSyncAdapters; private BroadcastReceiver mStorageIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { - ensureContentResolver(); String action = intent.getAction(); if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is low."); } mStorageIsLow = true; - cancelActiveSync(null /* no url */); + cancelActiveSync(null /* any account */, null /* any authority */); } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is ok."); @@ -172,6 +199,62 @@ class SyncManager { } }; + private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + mSyncHandler.onBootCompleted(); + } + }; + + private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (getConnectivityManager().getBackgroundDataSetting()) { + scheduleSync(null /* account */, null /* authority */, new Bundle(), 0 /* delay */, + false /* onlyThoseWithUnknownSyncableState */); + } + } + }; + + public void onAccountsUpdated(Account[] accounts) { + // remember if this was the first time this was called after an update + final boolean justBootedUp = mAccounts == null; + mAccounts = accounts; + + // if a sync is in progress yet it is no longer in the accounts list, + // cancel it + ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (activeSyncContext != null) { + if (!ArrayUtils.contains(accounts, activeSyncContext.mSyncOperation.account)) { + Log.d(TAG, "canceling sync since the account has been removed"); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + // we must do this since we don't bother scheduling alarms when + // the accounts are not set yet + sendCheckAlarmsMessage(); + + mSyncStorageEngine.doDatabaseCleanup(accounts); + + if (accounts.length > 0) { + // If this is the first time this was called after a bootup then + // the accounts haven't really changed, instead they were just loaded + // from the AccountManager. Otherwise at least one of the accounts + // has a change. + // + // If there was a real account change then force a sync of all accounts. + // This is a bit of overkill, but at least it will end up retrying syncs + // that failed due to an authentication failure and thus will recover if the + // account change was a password update. + // + // If this was the bootup case then don't sync everything, instead only + // sync those that have an unknown syncable state, which will give them + // a chance to set their syncable state. + boolean onlyThoseWithUnkownSyncableState = justBootedUp; + scheduleSync(null, null, null, 0 /* no delay */, onlyThoseWithUnkownSyncableState); + } + } + private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { @@ -229,7 +312,23 @@ class SyncManager { private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; + private final boolean mFactoryTest; + + private volatile boolean mBootCompleted = false; + + private ConnectivityManager getConnectivityManager() { + synchronized (this) { + if (mConnManagerDoNotUseDirectly == null) { + mConnManagerDoNotUseDirectly = (ConnectivityManager)mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + } + return mConnManagerDoNotUseDirectly; + } + } + public SyncManager(Context context, boolean factoryTest) { + mFactoryTest = factoryTest; + // Initialize the SyncStorageEngine first, before registering observers // and creating threads and so on; it may fail if the disk is full. SyncStorageEngine.init(context); @@ -244,6 +343,8 @@ class SyncManager { mPackageManager = null; + mSyncAdapters = new SyncAdaptersCache(mContext); + mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); @@ -253,6 +354,14 @@ class SyncManager { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); + if (!factoryTest) { + intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mBootCompletedReceiver, intentFilter); + } + + intentFilter = new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED); + context.registerReceiver(mBackgroundDataSettingChanged, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); @@ -282,47 +391,18 @@ class SyncManager { mHandleAlarmWakeLock.setReferenceCounted(false); mSyncStorageEngine.addStatusChangeListener( - SyncStorageEngine.CHANGE_SETTINGS, new ISyncStatusObserver.Stub() { + ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() { public void onStatusChanged(int which) { // force the sync loop to run if the settings change sendCheckAlarmsMessage(); } }); - - if (!factoryTest) { - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - final boolean hadAccountsAlready = mAccounts != null; - // copy the accounts into a new array and change mAccounts to point to it - String[] newAccounts = new String[accounts.length]; - System.arraycopy(accounts, 0, newAccounts, 0, accounts.length); - mAccounts = newAccounts; - - // if a sync is in progress yet it is no longer in the accounts list, cancel it - ActiveSyncContext activeSyncContext = mActiveSyncContext; - if (activeSyncContext != null) { - if (!ArrayUtils.contains(newAccounts, - activeSyncContext.mSyncOperation.account)) { - Log.d(TAG, "canceling sync since the account has been removed"); - sendSyncFinishedOrCanceledMessage(activeSyncContext, - null /* no result since this is a cancel */); - } - } - - // we must do this since we don't bother scheduling alarms when - // the accounts are not set yet - sendCheckAlarmsMessage(); - - mSyncStorageEngine.doDatabaseCleanup(accounts); - if (hadAccountsAlready && mAccounts.length > 0) { - // request a sync so that if the password was changed we will retry any sync - // that failed when it was wrong - startSync(null /* all providers */, null /* no extras */); - } - } - }; - mAccountMonitor = new AccountMonitor(context, listener); + if (!factoryTest) { + AccountManager.get(mContext).addOnAccountsUpdatedListener(SyncManager.this, + mSyncHandler, false /* updateImmediately */); + // do this synchronously to ensure we have the accounts before this call returns + onAccountsUpdated(AccountManager.get(mContext).getAccounts()); } } @@ -397,7 +477,8 @@ class SyncManager { scheduleSyncPollAlarm(nextRelativePollTimeMs); // perform a poll - scheduleSync(null /* sync all syncable providers */, new Bundle(), 0 /* no delay */); + scheduleSync(null /* sync all syncable accounts */, null /* sync all syncable providers */, + new Bundle(), 0 /* no delay */, false /* onlyThoseWithUnkownSyncableState */); } private void writeSyncPollTime(long when) { @@ -451,12 +532,6 @@ class SyncManager { public SyncStorageEngine getSyncStorageEngine() { return mSyncStorageEngine; } - - private void ensureContentResolver() { - if (mContentResolver == null) { - mContentResolver = mContext.getContentResolver(); - } - } private void ensureAlarmService() { if (mAlarmService == null) { @@ -464,7 +539,7 @@ class SyncManager { } } - public String getSyncingAccount() { + public Account getSyncingAccount() { ActiveSyncContext activeSyncContext = mActiveSyncContext; return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } @@ -499,21 +574,21 @@ class SyncManager { * *

      You'll start getting callbacks after this. * - * @param url The Uri of a specific provider to be synced, or - * null to sync all providers. + * @param requestedAccount the account to sync, may be null to signify all accounts + * @param requestedAuthority the authority to sync, may be null to indicate all authorities * @param extras a Map of SyncAdapter-specific information to control * syncs of a specific provider. Can be null. Is ignored * if the url is null. * @param delay how many milliseconds in the future to wait before performing this - * sync. -1 means to make this the next sync to perform. + * @param onlyThoseWithUnkownSyncableState */ - public void scheduleSync(Uri url, Bundle extras, long delay) { + public void scheduleSync(Account requestedAccount, String requestedAuthority, + Bundle extras, long delay, boolean onlyThoseWithUnkownSyncableState) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - if (isLoggable) { - Log.v(TAG, "scheduleSync:" - + " delay " + delay - + ", url " + ((url == null) ? "(null)" : url) - + ", extras " + ((extras == null) ? "(null)" : extras)); + + if (mAccounts == null) { + Log.e(TAG, "scheduleSync: the accounts aren't known yet, this should never happen"); + return; } if (!isSyncEnabled()) { @@ -524,7 +599,9 @@ class SyncManager { return; } - if (mAccounts == null) setStatusText("The accounts aren't known yet."); + final boolean backgroundDataUsageAllowed = !mBootCompleted || + getConnectivityManager().getBackgroundDataSetting(); + if (!mDataConnectionIsConnected) setStatusText("No data connection"); if (mStorageIsLow) setStatusText("Memory low"); @@ -535,10 +612,9 @@ class SyncManager { delay = -1; // this means schedule at the front of the queue } - String[] accounts; - String accountFromExtras = extras.getString(ContentResolver.SYNC_EXTRAS_ACCOUNT); - if (!TextUtils.isEmpty(accountFromExtras)) { - accounts = new String[]{accountFromExtras}; + Account[] accounts; + if (requestedAccount != null) { + accounts = new Account[]{requestedAccount}; } else { // if the accounts aren't configured yet then we can't support an account-less // sync request @@ -560,14 +636,14 @@ class SyncManager { } final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); - final boolean force = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); int source; if (uploadOnly) { source = SyncStorageEngine.SOURCE_LOCAL; - } else if (force) { + } else if (manualSync) { source = SyncStorageEngine.SOURCE_USER; - } else if (url == null) { + } else if (requestedAuthority == null) { source = SyncStorageEngine.SOURCE_POLL; } else { // this isn't strictly server, since arbitrary callers can (and do) request @@ -575,59 +651,85 @@ class SyncManager { source = SyncStorageEngine.SOURCE_SERVER; } - List names = new ArrayList(); - List providers = new ArrayList(); - populateProvidersList(url, names, providers); + // Compile a list of authorities that have sync adapters. + // For each authority sync each account that matches a sync adapter. + final HashSet syncableAuthorities = new HashSet(); + for (RegisteredServicesCache.ServiceInfo syncAdapter : + mSyncAdapters.getAllServices()) { + syncableAuthorities.add(syncAdapter.type.authority); + } - final int numProviders = providers.size(); - for (int i = 0; i < numProviders; i++) { - if (!providers.get(i).isSyncable) continue; - final String name = names.get(i); - for (String account : accounts) { - scheduleSyncOperation(new SyncOperation(account, source, name, extras, delay)); - // TODO: remove this when Calendar supports multiple accounts. Until then - // pretend that only the first account exists when syncing calendar. - if ("calendar".equals(name)) { - break; - } - } + // if the url was specified then replace the list of authorities with just this authority + // or clear it if this authority isn't syncable + if (requestedAuthority != null) { + final boolean hasSyncAdapter = syncableAuthorities.contains(requestedAuthority); + syncableAuthorities.clear(); + if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority); } - } - private void setStatusText(String message) { - mStatusText = message; - } + final boolean masterSyncAutomatically = mSyncStorageEngine.getMasterSyncAutomatically(); - private void populateProvidersList(Uri url, List names, List providers) { - try { - final IPackageManager packageManager = getPackageManager(); - if (url == null) { - packageManager.querySyncProviders(names, providers); - } else { - final String authority = url.getAuthority(); - ProviderInfo info = packageManager.resolveContentProvider(url.getAuthority(), 0); - if (info != null) { - // only set this provider if the requested authority is the primary authority - String[] providerNames = info.authority.split(";"); - if (url.getAuthority().equals(providerNames[0])) { - names.add(authority); - providers.add(info); + for (String authority : syncableAuthorities) { + for (Account account : accounts) { + int isSyncable = mSyncStorageEngine.getIsSyncable(account, authority); + if (isSyncable == 0) { + continue; + } + if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) { + continue; + } + final RegisteredServicesCache.ServiceInfo syncAdapterInfo = + mSyncAdapters.getServiceInfo( + SyncAdapterType.newKey(authority, account.type)); + if (syncAdapterInfo != null) { + if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) { + continue; + } + + // make this an initialization sync if the isSyncable state is unknown + Bundle extrasCopy = extras; + long delayCopy = delay; + if (isSyncable < 0) { + extrasCopy = new Bundle(extras); + extrasCopy.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + delayCopy = -1; // expedite this + } else { + final boolean syncAutomatically = masterSyncAutomatically + && mSyncStorageEngine.getSyncAutomatically(account, authority); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + if (!syncAllowed) { + if (isLoggable) { + Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority + + " is not allowed, dropping request"); + } + continue; + } + } + if (isLoggable) { + Log.v(TAG, "scheduleSync:" + + " delay " + delayCopy + + ", source " + source + + ", account " + account + + ", authority " + authority + + ", extras " + extrasCopy); } + scheduleSyncOperation( + new SyncOperation(account, source, authority, extrasCopy, delayCopy)); } } - } catch (RemoteException ex) { - // we should really never get this, but if we do then clear the lists, which - // will result in the dropping of the sync request - Log.e(TAG, "error trying to get the ProviderInfo for " + url, ex); - names.clear(); - providers.clear(); } } - public void scheduleLocalSync(Uri url) { + private void setStatusText(String message) { + mStatusText = message; + } + + public void scheduleLocalSync(Account account, String authority) { final Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); - scheduleSync(url, extras, LOCAL_SYNC_DELAY); + scheduleSync(account, authority, extras, LOCAL_SYNC_DELAY, + false /* onlyThoseWithUnkownSyncableState */); } private IPackageManager getPackageManager() { @@ -641,18 +743,16 @@ class SyncManager { return mPackageManager; } - /** - * Initiate a sync for this given URL, or pass null for a full sync. - * - *

      You'll start getting callbacks after this. - * - * @param url The Uri of a specific provider to be synced, or - * null to sync all providers. - * @param extras a Map of SyncAdapter specific information to control - * syncs of a specific provider. Can be null. Is ignored - */ - public void startSync(Uri url, Bundle extras) { - scheduleSync(url, extras, 0 /* no delay */); + public SyncAdapterType[] getSyncAdapterTypes() { + final Collection> serviceInfos = + mSyncAdapters.getAllServices(); + SyncAdapterType[] types = new SyncAdapterType[serviceInfos.size()]; + int i = 0; + for (RegisteredServicesCache.ServiceInfo serviceInfo : serviceInfos) { + types[i] = serviceInfo.type; + ++i; + } + return types; } public void updateHeartbeatTime() { @@ -711,7 +811,7 @@ class SyncManager { private long rescheduleWithDelay(SyncOperation syncOperation) { long newDelayInMs; - if (syncOperation.delay == 0) { + if (syncOperation.delay <= 0) { // The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS, (long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1)); @@ -721,8 +821,7 @@ class SyncManager { } // Cap the delay - ensureContentResolver(); - long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver, + long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContext.getContentResolver(), Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS, DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { @@ -736,17 +835,22 @@ class SyncManager { } /** - * Cancel the active sync if it matches the uri. The uri corresponds to the one passed - * in to startSync(). - * @param uri If non-null, the active sync is only canceled if it matches the uri. - * If null, any active sync is canceled. + * Cancel the active sync if it matches the authority and account. + * @param account limit the cancelations to syncs with this account, if non-null + * @param authority limit the cancelations to syncs with this authority, if non-null */ - public void cancelActiveSync(Uri uri) { + public void cancelActiveSync(Account account, String authority) { ActiveSyncContext activeSyncContext = mActiveSyncContext; if (activeSyncContext != null) { - // if a Uri was specified then only cancel the sync if it matches the the uri - if (uri != null) { - if (!uri.getAuthority().equals(activeSyncContext.mSyncOperation.authority)) { + // if an authority was specified then only cancel the sync if it matches + if (account != null) { + if (!account.equals(activeSyncContext.mSyncOperation.account)) { + return; + } + } + // if an account was specified then only cancel the sync if it matches + if (authority != null) { + if (!authority.equals(activeSyncContext.mSyncOperation.authority)) { return; } } @@ -798,14 +902,13 @@ class SyncManager { } /** - * Remove any scheduled sync operations that match uri. The uri corresponds to the one passed - * in to startSync(). - * @param uri If non-null, only operations that match the uri are cleared. - * If null, all operations are cleared. + * Remove scheduled sync operations. + * @param account limit the removals to operations with this account, if non-null + * @param authority limit the removals to operations with this authority, if non-null */ - public void clearScheduledSyncOperations(Uri uri) { + public void clearScheduledSyncOperations(Account account, String authority) { synchronized (mSyncQueue) { - mSyncQueue.clear(null, uri != null ? uri.getAuthority() : null); + mSyncQueue.clear(account, authority); } } @@ -857,7 +960,7 @@ class SyncManager { * Value type that represents a sync operation. */ static class SyncOperation implements Comparable { - final String account; + final Account account; int syncSource; String authority; Bundle extras; @@ -866,7 +969,7 @@ class SyncManager { long delay; SyncStorageEngine.PendingOperation pendingOperation; - SyncOperation(String account, int source, String authority, Bundle extras, long delay) { + SyncOperation(Account account, int source, String authority, Bundle extras, long delay) { this.account = account; this.syncSource = source; this.authority = authority; @@ -937,21 +1040,19 @@ class SyncManager { /** * @hide */ - class ActiveSyncContext extends ISyncContext.Stub { + class ActiveSyncContext extends ISyncContext.Stub implements ServiceConnection { final SyncOperation mSyncOperation; final long mHistoryRowId; - final IContentProvider mContentProvider; - final ISyncAdapter mSyncAdapter; + ISyncAdapter mSyncAdapter; final long mStartTime; long mTimeoutStartTime; - public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider, - ISyncAdapter syncAdapter, long historyRowId) { + public ActiveSyncContext(SyncOperation syncOperation, + long historyRowId) { super(); mSyncOperation = syncOperation; mHistoryRowId = historyRowId; - mContentProvider = contentProvider; - mSyncAdapter = syncAdapter; + mSyncAdapter = null; mStartTime = SystemClock.elapsedRealtime(); mTimeoutStartTime = mStartTime; } @@ -977,6 +1078,41 @@ class SyncManager { .append(", syncOperation ").append(mSyncOperation); } + public void onServiceConnected(ComponentName name, IBinder service) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_CONNECTED; + msg.obj = new ServiceConnectionData(this, ISyncAdapter.Stub.asInterface(service)); + mSyncHandler.sendMessage(msg); + } + + public void onServiceDisconnected(ComponentName name) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_DISCONNECTED; + msg.obj = new ServiceConnectionData(this, null); + mSyncHandler.sendMessage(msg); + } + + boolean bindToSyncAdapter(RegisteredServicesCache.ServiceInfo info) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "bindToSyncAdapter: " + info.componentName + ", connection " + this); + } + Intent intent = new Intent(); + intent.setAction("android.content.SyncAdapter"); + intent.setComponent(info.componentName); + intent.putExtra(Intent.EXTRA_CLIENT_LABEL, + com.android.internal.R.string.sync_binding_label); + intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( + mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0)); + return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + void unBindFromSyncAdapter() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "unBindFromSyncAdapter: connection " + this); + } + mContext.unbindService(this); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -991,6 +1127,12 @@ class SyncManager { if (isSyncEnabled()) { dumpSyncHistory(pw, sb); } + + pw.println(); + pw.println("SyncAdapters:"); + for (RegisteredServicesCache.ServiceInfo info : mSyncAdapters.getAllServices()) { + pw.println(" " + info); + } } static String formatTime(long time) { @@ -998,13 +1140,13 @@ class SyncManager { tobj.set(time); return tobj.format("%Y-%m-%d %H:%M:%S"); } - + protected void dumpSyncState(PrintWriter pw, StringBuilder sb) { pw.print("sync enabled: "); pw.println(isSyncEnabled()); pw.print("data connected: "); pw.println(mDataConnectionIsConnected); pw.print("memory low: "); pw.println(mStorageIsLow); - final String[] accounts = mAccounts; + final Account[] accounts = mAccounts; pw.print("accounts: "); if (accounts != null) { pw.println(accounts.length); @@ -1068,7 +1210,8 @@ class SyncManager { for (int i=0; i 0) { @@ -1156,7 +1300,7 @@ class SyncManager { } pw.println(")"); } - + protected void dumpSyncHistory(PrintWriter pw, StringBuilder sb) { SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics(); if (dses != null && dses[0] != null) { @@ -1166,18 +1310,18 @@ class SyncManager { int today = dses[0].day; int i; SyncStorageEngine.DayStats ds; - + // Print each day in the current week. for (i=1; i<=6 && i < dses.length; i++) { ds = dses[i]; if (ds == null) break; int delta = today-ds.day; if (delta > 6) break; - + pw.print(" Day-"); pw.print(delta); pw.print(": "); dumpDayStatistic(pw, ds); } - + // Aggregate all following days into weeks and print totals. int weekDay = today; while (i < dses.length) { @@ -1192,7 +1336,7 @@ class SyncManager { int delta = weekDay-ds.day; if (delta > 6) break; i++; - + if (aggr == null) { aggr = new SyncStorageEngine.DayStats(weekDay); } @@ -1207,7 +1351,7 @@ class SyncManager { } } } - + ArrayList items = mSyncStorageEngine.getSyncHistory(); if (items != null && items.size() > 0) { @@ -1219,9 +1363,15 @@ class SyncManager { SyncStorageEngine.AuthorityInfo authority = mSyncStorageEngine.getAuthority(item.authorityId); pw.print(" #"); pw.print(i+1); pw.print(": "); - pw.print(authority != null ? authority.account : ""); - pw.print(" "); - pw.print(authority != null ? authority.authority : ""); + if (authority != null) { + pw.print(authority.account.name); + pw.print(":"); + pw.print(authority.account.type); + pw.print(" "); + pw.print(authority.authority); + } else { + pw.print(""); + } Time time = new Time(); time.set(item.eventTime); pw.print(" "); pw.print(SyncStorageEngine.SOURCES[item.source]); @@ -1278,6 +1428,15 @@ class SyncManager { } } + class ServiceConnectionData { + public final ActiveSyncContext activeSyncContext; + public final ISyncAdapter syncAdapter; + ServiceConnectionData(ActiveSyncContext activeSyncContext, ISyncAdapter syncAdapter) { + this.activeSyncContext = activeSyncContext; + this.syncAdapter = syncAdapter; + } + } + /** * Handles SyncOperation Messages that are posted to the associated * HandlerThread. @@ -1287,6 +1446,8 @@ class SyncManager { private static final int MESSAGE_SYNC_FINISHED = 1; private static final int MESSAGE_SYNC_ALARM = 2; private static final int MESSAGE_CHECK_ALARMS = 3; + private static final int MESSAGE_SERVICE_CONNECTED = 4; + private static final int MESSAGE_SERVICE_DISCONNECTED = 5; public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; @@ -1295,13 +1456,35 @@ class SyncManager { // used to track if we have installed the error notification so that we don't reinstall // it if sync is still failing private boolean mErrorNotificationInstalled = false; + private volatile CountDownLatch mReadyToRunLatch = new CountDownLatch(1); + + public void onBootCompleted() { + mBootCompleted = true; + if (mReadyToRunLatch != null) { + mReadyToRunLatch.countDown(); + } + } + private void waitUntilReadyToRun() { + CountDownLatch latch = mReadyToRunLatch; + if (latch != null) { + while (true) { + try { + latch.await(); + mReadyToRunLatch = null; + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } /** * Used to keep track of whether a sync notification is active and who it is for. */ class SyncNotificationInfo { // only valid if isActive is true - public String account; + public Account account; // only valid if isActive is true public String authority; @@ -1333,11 +1516,8 @@ class SyncManager { } public void handleMessage(Message msg) { - handleSyncHandlerMessage(msg); - } - - private void handleSyncHandlerMessage(Message msg) { try { + waitUntilReadyToRun(); switch (msg.what) { case SyncHandler.MESSAGE_SYNC_FINISHED: if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -1358,6 +1538,53 @@ class SyncManager { runStateIdle(); break; + case SyncHandler.MESSAGE_SERVICE_CONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + runBoundToSyncAdapter(msgData.syncAdapter); + } + break; + } + + case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + // cancel the sync if we have a syncadapter, which means one is + // outstanding + if (mActiveSyncContext.mSyncAdapter != null) { + try { + mActiveSyncContext.mSyncAdapter.cancelSync(mActiveSyncContext); + } catch (RemoteException e) { + // we don't need to retry this in this case + } + } + + // pretend that the sync failed with an IOException, + // which is a soft error + SyncResult syncResult = new SyncResult(); + syncResult.stats.numIoExceptions++; + runSyncFinishedOrCanceled(syncResult); + + // since we are no longer syncing, check if it is time to start a new + // sync + runStateIdle(); + } + + break; + } + case SyncHandler.MESSAGE_SYNC_ALARM: { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) { @@ -1456,7 +1683,7 @@ class SyncManager { // If the accounts aren't known yet then we aren't ready to run. We will be kicked // when the account lookup request does complete. - String[] accounts = mAccounts; + Account[] accounts = mAccounts; if (accounts == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: accounts not known, skipping"); @@ -1468,14 +1695,13 @@ class SyncManager { // Otherwise consume SyncOperations from the head of the SyncQueue until one is // found that is runnable (not disabled, etc). If that one is ready to run then // start it, otherwise just get out. - SyncOperation syncOperation; - final ConnectivityManager connManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - final boolean backgroundDataSetting = connManager.getBackgroundDataSetting(); + SyncOperation op; + final boolean backgroundDataUsageAllowed = + getConnectivityManager().getBackgroundDataSetting(); synchronized (mSyncQueue) { while (true) { - syncOperation = mSyncQueue.head(); - if (syncOperation == null) { + op = mSyncQueue.head(); + if (op == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: no more sync operations, returning"); } @@ -1485,39 +1711,49 @@ class SyncManager { // Sync is disabled, drop this operation. if (!isSyncEnabled()) { if (isLoggable) { - Log.v(TAG, "runStateIdle: sync disabled, dropping " + syncOperation); + Log.v(TAG, "runStateIdle: sync disabled, dropping " + op); } mSyncQueue.popHead(); continue; } - // skip the sync if it isn't a force and the settings are off for this provider - final boolean force = syncOperation.extras.getBoolean( - ContentResolver.SYNC_EXTRAS_FORCE, false); - if (!force && (!backgroundDataSetting - || !mSyncStorageEngine.getListenForNetworkTickles() - || !mSyncStorageEngine.getSyncProviderAutomatically( - null, syncOperation.authority))) { + // skip the sync if it isn't manual and auto sync is disabled + final boolean manualSync = op.extras.getBoolean( + ContentResolver.SYNC_EXTRAS_MANUAL, false); + final boolean syncAutomatically = + mSyncStorageEngine.getSyncAutomatically(op.account, op.authority) + && mSyncStorageEngine.getMasterSyncAutomatically(); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + int isSyncable = mSyncStorageEngine.getIsSyncable(op.account, op.authority); + if (isSyncable == 0) { + // if not syncable, don't allow + syncAllowed = false; + } else if (isSyncable < 0) { + // if the syncable state is unknown, only allow initialization syncs + syncAllowed = + op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); + } + if (!syncAllowed) { if (isLoggable) { - Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation); + Log.v(TAG, "runStateIdle: sync off, dropping " + op); } mSyncQueue.popHead(); continue; } // skip the sync if the account of this operation no longer exists - if (!ArrayUtils.contains(accounts, syncOperation.account)) { + if (!ArrayUtils.contains(accounts, op.account)) { mSyncQueue.popHead(); if (isLoggable) { - Log.v(TAG, "runStateIdle: account not present, dropping " - + syncOperation); + Log.v(TAG, "runStateIdle: account not present, dropping " + op); } continue; } // go ahead and try to sync this syncOperation if (isLoggable) { - Log.v(TAG, "runStateIdle: found sync candidate: " + syncOperation); + Log.v(TAG, "runStateIdle: found sync candidate: " + op); } break; } @@ -1525,11 +1761,10 @@ class SyncManager { // If the first SyncOperation isn't ready to run schedule a wakeup and // get out. final long now = SystemClock.elapsedRealtime(); - if (syncOperation.earliestRunTime > now) { + if (op.earliestRunTime > now) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "runStateIdle: the time is " + now + " yet the next " - + "sync operation is for " + syncOperation.earliestRunTime - + ": " + syncOperation); + + "sync operation is for " + op.earliestRunTime + ": " + op); } return; } @@ -1537,72 +1772,72 @@ class SyncManager { // We will do this sync. Remove it from the queue and run it outside of the // synchronized block. if (isLoggable) { - Log.v(TAG, "runStateIdle: we are going to sync " + syncOperation); + Log.v(TAG, "runStateIdle: we are going to sync " + op); } mSyncQueue.popHead(); } - String providerName = syncOperation.authority; - ensureContentResolver(); - IContentProvider contentProvider; - - // acquire the provider and update the sync history - try { - contentProvider = mContentResolver.acquireProvider(providerName); - if (contentProvider == null) { - Log.e(TAG, "Provider " + providerName + " doesn't exist"); - return; - } - if (contentProvider.getSyncAdapter() == null) { - Log.e(TAG, "Provider " + providerName + " isn't syncable, " + contentProvider); - return; + // connect to the sync adapter + SyncAdapterType syncAdapterType = SyncAdapterType.newKey(op.authority, op.account.type); + RegisteredServicesCache.ServiceInfo syncAdapterInfo = + mSyncAdapters.getServiceInfo(syncAdapterType); + if (syncAdapterInfo == null) { + if (Config.LOGD) { + Log.d(TAG, "can't find a sync adapter for " + syncAdapterType); } - } catch (RemoteException remoteExc) { - Log.e(TAG, "Caught a RemoteException while preparing for sync, rescheduling " - + syncOperation, remoteExc); - rescheduleWithDelay(syncOperation); + runStateIdle(); return; - } catch (RuntimeException exc) { - Log.e(TAG, "Caught a RuntimeException while validating sync of " + providerName, - exc); + } + + ActiveSyncContext activeSyncContext = + new ActiveSyncContext(op, insertStartSyncEvent(op)); + mActiveSyncContext = activeSyncContext; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runStateIdle: setting mActiveSyncContext to " + mActiveSyncContext); + } + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo)) { + Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo); + mActiveSyncContext = null; + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + runStateIdle(); return; } - final long historyRowId = insertStartSyncEvent(syncOperation); + mSyncWakeLock.acquire(); + // no need to schedule an alarm, as that will be done by our caller. + + // the next step will occur when we get either a timeout or a + // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message + } + private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) { + mActiveSyncContext.mSyncAdapter = syncAdapter; + final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; try { - ISyncAdapter syncAdapter = contentProvider.getSyncAdapter(); - ActiveSyncContext activeSyncContext = new ActiveSyncContext(syncOperation, - contentProvider, syncAdapter, historyRowId); - mSyncWakeLock.acquire(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "starting sync of " + syncOperation); - } - syncAdapter.startSync(activeSyncContext, syncOperation.account, - syncOperation.extras); - mActiveSyncContext = activeSyncContext; - mSyncStorageEngine.setActiveSync(mActiveSyncContext); + syncAdapter.startSync(mActiveSyncContext, syncOperation.authority, + syncOperation.account, syncOperation.extras); } catch (RemoteException remoteExc) { if (Config.LOGD) { Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc); } + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); rescheduleWithDelay(syncOperation); } catch (RuntimeException exc) { + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); Log.e(TAG, "Caught a RuntimeException while starting the sync " + syncOperation, exc); } - - // no need to schedule an alarm, as that will be done by our caller. } private void runSyncFinishedOrCanceled(SyncResult syncResult) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) Log.v(TAG, "runSyncFinishedOrCanceled"); - ActiveSyncContext activeSyncContext = mActiveSyncContext; + final ActiveSyncContext activeSyncContext = mActiveSyncContext; mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); @@ -1642,10 +1877,12 @@ class SyncManager { Log.v(TAG, "runSyncFinishedOrCanceled: is a cancel: operation " + syncOperation); } - try { - activeSyncContext.mSyncAdapter.cancelSync(); - } catch (RemoteException e) { - // we don't need to retry this in this case + if (activeSyncContext.mSyncAdapter != null) { + try { + activeSyncContext.mSyncAdapter.cancelSync(activeSyncContext); + } catch (RemoteException e) { + // we don't need to retry this in this case + } } historyMessage = SyncStorageEngine.MESG_CANCELED; downstreamActivity = 0; @@ -1655,7 +1892,7 @@ class SyncManager { stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage, upstreamActivity, downstreamActivity, elapsedTime); - mContentResolver.releaseProvider(activeSyncContext.mContentProvider); + activeSyncContext.unBindFromSyncAdapter(); if (syncResult != null && syncResult.tooManyDeletions) { installHandleTooManyDeletesNotification(syncOperation.account, @@ -1683,21 +1920,21 @@ class SyncManager { */ private int syncResultToErrorNumber(SyncResult syncResult) { if (syncResult.syncAlreadyInProgress) - return SyncStorageEngine.ERROR_SYNC_ALREADY_IN_PROGRESS; + return ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; if (syncResult.stats.numAuthExceptions > 0) - return SyncStorageEngine.ERROR_AUTHENTICATION; + return ContentResolver.SYNC_ERROR_AUTHENTICATION; if (syncResult.stats.numIoExceptions > 0) - return SyncStorageEngine.ERROR_IO; + return ContentResolver.SYNC_ERROR_IO; if (syncResult.stats.numParseExceptions > 0) - return SyncStorageEngine.ERROR_PARSE; + return ContentResolver.SYNC_ERROR_PARSE; if (syncResult.stats.numConflictDetectedExceptions > 0) - return SyncStorageEngine.ERROR_CONFLICT; + return ContentResolver.SYNC_ERROR_CONFLICT; if (syncResult.tooManyDeletions) - return SyncStorageEngine.ERROR_TOO_MANY_DELETIONS; + return ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS; if (syncResult.tooManyRetries) - return SyncStorageEngine.ERROR_TOO_MANY_RETRIES; + return ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES; if (syncResult.databaseError) - return SyncStorageEngine.ERROR_INTERNAL; + return ContentResolver.SYNC_ERROR_INTERNAL; throw new IllegalStateException("we are not in an error state, " + syncResult); } @@ -1738,9 +1975,10 @@ class SyncManager { } else { final boolean timeToShowNotification = now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY; - final boolean syncIsForced = syncOperation.extras - .getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); - shouldInstall = timeToShowNotification || syncIsForced; + // show the notification immediately if this is a manual sync + final boolean manualSync = syncOperation.extras + .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); + shouldInstall = timeToShowNotification || manualSync; } } @@ -1855,19 +2093,29 @@ class SyncManager { private void sendSyncStateIntent() { Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED); + syncStateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); syncStateIntent.putExtra("active", mNeedSyncActiveNotification); syncStateIntent.putExtra("failing", mNeedSyncErrorNotification); mContext.sendBroadcast(syncStateIntent); } - private void installHandleTooManyDeletesNotification(String account, String authority, + private void installHandleTooManyDeletesNotification(Account account, String authority, long numDeletes) { if (mNotificationMgr == null) return; + + final ProviderInfo providerInfo = mContext.getPackageManager().resolveContentProvider( + authority, 0 /* flags */); + if (providerInfo == null) { + return; + } + CharSequence authorityName = providerInfo.loadLabel(mContext.getPackageManager()); + Intent clickIntent = new Intent(); clickIntent.setClassName("com.android.providers.subscribedfeeds", "com.android.settings.SyncActivityTooManyDeletes"); clickIntent.putExtra("account", account); - clickIntent.putExtra("provider", authority); + clickIntent.putExtra("authority", authority); + clickIntent.putExtra("provider", authorityName.toString()); clickIntent.putExtra("numDeletes", numDeletes); if (!isActivityAvailable(clickIntent)) { @@ -1881,14 +2129,13 @@ class SyncManager { CharSequence tooManyDeletesDescFormat = mContext.getResources().getText( R.string.contentServiceTooManyDeletesNotificationDesc); - String[] authorities = authority.split(";"); Notification notification = new Notification(R.drawable.stat_notify_sync_error, mContext.getString(R.string.contentServiceSync), System.currentTimeMillis()); notification.setLatestEventInfo(mContext, mContext.getString(R.string.contentServiceSyncNotificationTitle), - String.format(tooManyDeletesDescFormat.toString(), authorities[0]), + String.format(tooManyDeletesDescFormat.toString(), authorityName), pendingIntent); notification.flags |= Notification.FLAG_ONGOING_EVENT; mNotificationMgr.notify(account.hashCode() ^ authority.hashCode(), notification); @@ -1920,7 +2167,8 @@ class SyncManager { final long now = System.currentTimeMillis(); EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_START, source); + SyncStorageEngine.EVENT_START, source, + syncOperation.account.name.hashCode()); return mSyncStorageEngine.insertStartSyncEvent( syncOperation.account, syncOperation.authority, now, source); @@ -1929,7 +2177,8 @@ class SyncManager { public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage, int upstreamActivity, int downstreamActivity, long elapsedTime) { EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_STOP, syncOperation.syncSource); + SyncStorageEngine.EVENT_STOP, syncOperation.syncSource, + syncOperation.account.name.hashCode()); mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage, downstreamActivity, upstreamActivity); @@ -1961,7 +2210,7 @@ class SyncManager { syncOperation.pendingOperation = op; add(syncOperation, op); } - + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } @@ -1995,9 +2244,9 @@ class SyncManager { SyncOperation existingOperation = mOpsByKey.get(operationKey); // if this operation matches an existing operation that is being retried (delay > 0) - // and this operation isn't forced, ignore this operation + // and this isn't a manual sync operation, ignore this operation if (existingOperation != null && existingOperation.delay > 0) { - if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)) { + if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) { return false; } } @@ -2045,8 +2294,8 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " - + operationToRemove); + final String errorMessage = "unable to find pending row for " + operationToRemove; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); @@ -2065,13 +2314,14 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(operation.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " + operation); + final String errorMessage = "unable to find pending row for " + operation; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } - public void clear(String account, String authority) { + public void clear(Account account, String authority) { Iterator> entries = mOpsByKey.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); @@ -2087,8 +2337,8 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " - + syncOperation); + final String errorMessage = "unable to find pending row for " + syncOperation; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java index f3260f3ee977c7a1fa29db83043d5b93c9dc64e7..57161b662c8f546790590b624b0e50c0910c9132 100644 --- a/core/java/android/content/SyncResult.java +++ b/core/java/android/content/SyncResult.java @@ -5,8 +5,6 @@ import android.os.Parcelable; /** * This class is used to store information about the result of a sync - * - * @hide */ public final class SyncResult implements Parcelable { public final boolean syncAlreadyInProgress; @@ -113,14 +111,19 @@ public final class SyncResult implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(" syncAlreadyInProgress: ").append(syncAlreadyInProgress); - sb.append(" tooManyDeletions: ").append(tooManyDeletions); - sb.append(" tooManyRetries: ").append(tooManyRetries); - sb.append(" databaseError: ").append(databaseError); - sb.append(" fullSyncRequested: ").append(fullSyncRequested); - sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable); - sb.append(" moreRecordsToGet: ").append(moreRecordsToGet); - sb.append(" stats: ").append(stats); + sb.append("SyncResult:"); + if (syncAlreadyInProgress) { + sb.append(" syncAlreadyInProgress: ").append(syncAlreadyInProgress); + } + if (tooManyDeletions) sb.append(" tooManyDeletions: ").append(tooManyDeletions); + if (tooManyRetries) sb.append(" tooManyRetries: ").append(tooManyRetries); + if (databaseError) sb.append(" databaseError: ").append(databaseError); + if (fullSyncRequested) sb.append(" fullSyncRequested: ").append(fullSyncRequested); + if (partialSyncUnavailable) { + sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable); + } + if (moreRecordsToGet) sb.append(" moreRecordsToGet: ").append(moreRecordsToGet); + sb.append(stats); return sb.toString(); } diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java index f503e6f3521db61c084c4cfa0dc9dcc85976e168..64bbe25cefb0e1d760c66f3741f6556a3bd66666 100644 --- a/core/java/android/content/SyncStateContentProviderHelper.java +++ b/core/java/android/content/SyncStateContentProviderHelper.java @@ -23,6 +23,7 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.accounts.Account; /** * Extends the schema of a ContentProvider to include the _sync_state table @@ -43,14 +44,15 @@ public class SyncStateContentProviderHelper { private static final Uri CONTENT_URI = Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); - private static final String ACCOUNT_WHERE = "_sync_account = ?"; + private static final String ACCOUNT_WHERE = "_sync_account = ? AND _sync_account_type = ?"; private final Provider mInternalProviderInterface; private static final String SYNC_STATE_TABLE = "_sync_state"; - private static long DB_VERSION = 2; + private static long DB_VERSION = 3; - private static final String[] ACCOUNT_PROJECTION = new String[]{"_sync_account"}; + private static final String[] ACCOUNT_PROJECTION = + new String[]{"_sync_account", "_sync_account_type"}; static { sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); @@ -70,8 +72,9 @@ public class SyncStateContentProviderHelper { db.execSQL("CREATE TABLE _sync_state (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + + "_sync_account_type TEXT," + "data TEXT," + - "UNIQUE(_sync_account)" + + "UNIQUE(_sync_account, _sync_account_type)" + ");"); db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); @@ -168,15 +171,17 @@ public class SyncStateContentProviderHelper { * @param account the account of the row that should be copied over. */ public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest, - String account) { - final String[] whereArgs = new String[]{account}; - Cursor c = dbSrc.query(SYNC_STATE_TABLE, new String[]{"_sync_account", "data"}, + Account account) { + final String[] whereArgs = new String[]{account.name, account.type}; + Cursor c = dbSrc.query(SYNC_STATE_TABLE, + new String[]{"_sync_account", "_sync_account_type", "data"}, ACCOUNT_WHERE, whereArgs, null, null, null); try { if (c.moveToNext()) { ContentValues values = new ContentValues(); values.put("_sync_account", c.getString(0)); - values.put("data", c.getBlob(1)); + values.put("_sync_account_type", c.getString(1)); + values.put("data", c.getBlob(2)); dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); } } finally { @@ -184,14 +189,17 @@ public class SyncStateContentProviderHelper { } } - public void onAccountsChanged(String[] accounts) { + public void onAccountsChanged(Account[] accounts) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null); try { while (c.moveToNext()) { - final String account = c.getString(0); + final String accountName = c.getString(0); + final String accountType = c.getString(1); + Account account = new Account(accountName, accountType); if (!ArrayUtils.contains(accounts, account)) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, + new String[]{accountName, accountType}); } } } finally { @@ -199,9 +207,9 @@ public class SyncStateContentProviderHelper { } } - public void discardSyncData(SQLiteDatabase db, String account) { + public void discardSyncData(SQLiteDatabase db, Account account) { if (account != null) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account.name, account.type}); } else { db.delete(SYNC_STATE_TABLE, null, null); } @@ -210,9 +218,9 @@ public class SyncStateContentProviderHelper { /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(SQLiteDatabase db, String account) { + public byte[] readSyncDataBytes(SQLiteDatabase db, Account account) { Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, - new String[]{account}, null, null, null); + new String[]{account.name, account.type}, null, null, null); try { if (c.moveToFirst()) { return c.getBlob(c.getColumnIndexOrThrow("data")); @@ -226,9 +234,10 @@ public class SyncStateContentProviderHelper { /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public void writeSyncDataBytes(SQLiteDatabase db, String account, byte[] data) { + public void writeSyncDataBytes(SQLiteDatabase db, Account account, byte[] data) { ContentValues values = new ContentValues(); values.put("data", data); - db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, new String[]{account}); + db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, + new String[]{account.name, account.type}); } } diff --git a/core/java/android/content/SyncStats.java b/core/java/android/content/SyncStats.java index b561b05f0c9de33e50b6ba976009b078e6d716eb..cc544c00e1f14d1779a0e8920caaaed6b341cd89 100644 --- a/core/java/android/content/SyncStats.java +++ b/core/java/android/content/SyncStats.java @@ -60,15 +60,18 @@ public class SyncStats implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("numAuthExceptions: ").append(numAuthExceptions); - sb.append(" numIoExceptions: ").append(numIoExceptions); - sb.append(" numParseExceptions: ").append(numParseExceptions); - sb.append(" numConflictDetectedExceptions: ").append(numConflictDetectedExceptions); - sb.append(" numInserts: ").append(numInserts); - sb.append(" numUpdates: ").append(numUpdates); - sb.append(" numDeletes: ").append(numDeletes); - sb.append(" numEntries: ").append(numEntries); - sb.append(" numSkippedEntries: ").append(numSkippedEntries); + sb.append(" stats ["); + if (numAuthExceptions > 0) sb.append(" numAuthExceptions: ").append(numAuthExceptions); + if (numIoExceptions > 0) sb.append(" numIoExceptions: ").append(numIoExceptions); + if (numParseExceptions > 0) sb.append(" numParseExceptions: ").append(numParseExceptions); + if (numConflictDetectedExceptions > 0) + sb.append(" numConflictDetectedExceptions: ").append(numConflictDetectedExceptions); + if (numInserts > 0) sb.append(" numInserts: ").append(numInserts); + if (numUpdates > 0) sb.append(" numUpdates: ").append(numUpdates); + if (numDeletes > 0) sb.append(" numDeletes: ").append(numDeletes); + if (numEntries > 0) sb.append(" numEntries: ").append(numEntries); + if (numSkippedEntries > 0) sb.append(" numSkippedEntries: ").append(numSkippedEntries); + sb.append("]"); return sb.toString(); } diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java index 6687fcb4f8ab8c948643c5312a46d14bff316c30..b8fda030b11c30ef91bda019fec0ea80f6db7eba 100644 --- a/core/java/android/content/SyncStatusInfo.java +++ b/core/java/android/content/SyncStatusInfo.java @@ -38,6 +38,7 @@ public class SyncStatusInfo implements Parcelable { public String lastFailureMesg; public long initialFailureTime; public boolean pending; + public boolean initialize; SyncStatusInfo(int authorityId) { this.authorityId = authorityId; @@ -73,6 +74,7 @@ public class SyncStatusInfo implements Parcelable { parcel.writeString(lastFailureMesg); parcel.writeLong(initialFailureTime); parcel.writeInt(pending ? 1 : 0); + parcel.writeInt(initialize ? 1 : 0); } SyncStatusInfo(Parcel parcel) { @@ -94,6 +96,7 @@ public class SyncStatusInfo implements Parcelable { lastFailureMesg = parcel.readString(); initialFailureTime = parcel.readLong(); pending = parcel.readInt() != 0; + initialize = parcel.readInt() != 0; } public static final Creator CREATOR = new Creator() { diff --git a/keystore/java/android/security/Reply.java b/core/java/android/content/SyncStatusObserver.java similarity index 77% rename from keystore/java/android/security/Reply.java rename to core/java/android/content/SyncStatusObserver.java index 15a0dde61e832b2afdb2a59e92313274a296a619..663378a7bedefba65d972bbe865e353e6784440d 100644 --- a/keystore/java/android/security/Reply.java +++ b/core/java/android/content/SyncStatusObserver.java @@ -14,13 +14,8 @@ * limitations under the License. */ -package android.security; +package android.content; -/* - * {@hide} - */ -public class Reply { - public int len; - public int returnCode; - public byte[] data = new byte[ServiceCommand.BUFFER_LENGTH]; +public interface SyncStatusObserver { + void onStatusChanged(int which); } diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index 756f35cca6553cb4c91c2552e997831a4c20cae5..be7090999bc12fc3cb5771ab5d4469bbfe69febd 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -24,6 +24,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import android.accounts.Account; import android.backup.IBackupManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -53,14 +54,14 @@ import java.util.TimeZone; /** * Singleton that tracks the sync data and overall sync * history on the device. - * + * * @hide */ public class SyncStorageEngine extends Handler { private static final String TAG = "SyncManager"; private static final boolean DEBUG = false; private static final boolean DEBUG_FILE = false; - + // @VisibleForTesting static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; @@ -88,6 +89,9 @@ public class SyncStorageEngine extends Handler { /** Enum value for a user-initiated sync. */ public static final int SOURCE_USER = 3; + private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT = + new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED"); + // TODO: i18n -- grab these out of resources. /** String names for the sync source types. */ public static final String[] SOURCES = { "SERVER", @@ -95,44 +99,30 @@ public class SyncStorageEngine extends Handler { "POLL", "USER" }; - // Error types - public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; - public static final int ERROR_AUTHENTICATION = 2; - public static final int ERROR_IO = 3; - public static final int ERROR_PARSE = 4; - public static final int ERROR_CONFLICT = 5; - public static final int ERROR_TOO_MANY_DELETIONS = 6; - public static final int ERROR_TOO_MANY_RETRIES = 7; - public static final int ERROR_INTERNAL = 8; - // The MESG column will contain one of these or one of the Error types. public static final String MESG_SUCCESS = "success"; public static final String MESG_CANCELED = "canceled"; - public static final int CHANGE_SETTINGS = 1<<0; - public static final int CHANGE_PENDING = 1<<1; - public static final int CHANGE_ACTIVE = 1<<2; - public static final int CHANGE_STATUS = 1<<3; - public static final int CHANGE_ALL = 0x7fffffff; - - public static final int MAX_HISTORY = 15; - + public static final int MAX_HISTORY = 100; + private static final int MSG_WRITE_STATUS = 1; private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes - + private static final int MSG_WRITE_STATISTICS = 2; private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour - + + private static final boolean SYNC_ENABLED_DEFAULT = false; + public static class PendingOperation { - final String account; + final Account account; final int syncSource; final String authority; final Bundle extras; // note: read-only. - + int authorityId; byte[] flatExtras; - - PendingOperation(String account, int source, + + PendingOperation(Account account, int source, String authority, Bundle extras) { this.account = account; this.syncSource = source; @@ -149,31 +139,33 @@ public class SyncStorageEngine extends Handler { this.authorityId = other.authorityId; } } - + static class AccountInfo { - final String account; + final Account account; final HashMap authorities = new HashMap(); - - AccountInfo(String account) { + + AccountInfo(Account account) { this.account = account; } } - + public static class AuthorityInfo { - final String account; + final Account account; final String authority; final int ident; boolean enabled; - - AuthorityInfo(String account, String authority, int ident) { + int syncable; + + AuthorityInfo(Account account, String authority, int ident) { this.account = account; this.authority = authority; this.ident = ident; - enabled = true; + enabled = SYNC_ENABLED_DEFAULT; + syncable = -1; // default to "unknown" } } - + public static class SyncHistoryItem { int authorityId; int historyId; @@ -185,69 +177,69 @@ public class SyncStorageEngine extends Handler { long downstreamActivity; String mesg; } - + public static class DayStats { public final int day; public int successCount; public long successTime; public int failureCount; public long failureTime; - + public DayStats(int day) { this.day = day; } } - + // Primary list of all syncable authorities. Also our global lock. private final SparseArray mAuthorities = new SparseArray(); - - private final HashMap mAccounts = - new HashMap(); + + private final HashMap mAccounts = + new HashMap(); private final ArrayList mPendingOperations = new ArrayList(); - + private ActiveSyncInfo mActiveSync; - + private final SparseArray mSyncStatus = new SparseArray(); - + private final ArrayList mSyncHistory = new ArrayList(); - + private final RemoteCallbackList mChangeListeners = new RemoteCallbackList(); - + // We keep 4 weeks of stats. private final DayStats[] mDayStats = new DayStats[7*4]; private final Calendar mCal; private int mYear; private int mYearInDays; - + private final Context mContext; private static volatile SyncStorageEngine sSyncStorageEngine = null; - + /** * This file contains the core engine state: all accounts and the * settings for them. It must never be lost, and should be changed * infrequently, so it is stored as an XML file. */ private final AtomicFile mAccountInfoFile; - + /** * This file contains the current sync status. We would like to retain * it across boots, but its loss is not the end of the world, so we store * this information as binary data. */ private final AtomicFile mStatusFile; - + /** * This file contains sync statistics. This is purely debugging information * so is written infrequently and can be thrown away at any time. */ private final AtomicFile mStatisticsFile; - + /** * This file contains the pending sync operations. It is a binary file, * which must be updated every time an operation is added or removed, @@ -256,16 +248,16 @@ public class SyncStorageEngine extends Handler { private final AtomicFile mPendingFile; private static final int PENDING_FINISH_TO_WRITE = 4; private int mNumPendingFinished = 0; - + private int mNextHistoryId = 0; - private boolean mListenForTickles = true; - + private boolean mMasterSyncAutomatically = true; + private SyncStorageEngine(Context context) { mContext = context; sSyncStorageEngine = this; - + mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - + File dataDir = Environment.getDataDirectory(); File systemDir = new File(dataDir, "system"); File syncDir = new File(systemDir, "sync"); @@ -273,7 +265,7 @@ public class SyncStorageEngine extends Handler { mStatusFile = new AtomicFile(new File(syncDir, "status.bin")); mPendingFile = new AtomicFile(new File(syncDir, "pending.bin")); mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin")); - + readAccountInfoLocked(); readStatusLocked(); readPendingOperationsLocked(); @@ -310,19 +302,19 @@ public class SyncStorageEngine extends Handler { } } } - + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { synchronized (mAuthorities) { mChangeListeners.register(callback, mask); } } - + public void removeStatusChangeListener(ISyncStatusObserver callback) { synchronized (mAuthorities) { mChangeListeners.unregister(callback); } } - + private void reportChange(int which) { ArrayList reports = null; synchronized (mAuthorities) { @@ -340,9 +332,9 @@ public class SyncStorageEngine extends Handler { } mChangeListeners.finishBroadcast(); } - + if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports); - + if (reports != null) { int i = reports.size(); while (i > 0) { @@ -354,30 +346,20 @@ public class SyncStorageEngine extends Handler { } } } - // Inform the backup manager about a data change - IBackupManager ibm = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); - if (ibm != null) { - try { - ibm.dataChanged("com.android.providers.settings"); - } catch (RemoteException e) { - // Try again later - } - } } - public boolean getSyncProviderAutomatically(String account, String providerName) { + public boolean getSyncAutomatically(Account account, String providerName) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, - "getSyncProviderAutomatically"); - return authority != null ? authority.enabled : false; + "getSyncAutomatically"); + return authority != null && authority.enabled; } - + int i = mAuthorities.size(); while (i > 0) { i--; - AuthorityInfo authority = mAuthorities.get(i); + AuthorityInfo authority = mAuthorities.valueAt(i); if (authority.authority.equals(providerName) && authority.enabled) { return true; @@ -387,61 +369,102 @@ public class SyncStorageEngine extends Handler { } } - public void setSyncProviderAutomatically(String account, String providerName, boolean sync) { + public void setSyncAutomatically(Account account, String providerName, boolean sync) { + boolean wasEnabled; + synchronized (mAuthorities) { + AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + wasEnabled = authority.enabled; + authority.enabled = sync; + writeAccountInfoLocked(); + } + + if (!wasEnabled && sync) { + mContext.getContentResolver().requestSync(account, providerName, new Bundle()); + } + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + } + + public int getIsSyncable(Account account, String providerName) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, - "setSyncProviderAutomatically"); - if (authority != null) { - authority.enabled = sync; + "getIsSyncable"); + if (authority == null) { + return -1; } - } else { - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.get(i); - if (authority.authority.equals(providerName)) { - authority.enabled = sync; - } + return authority.syncable; + } + + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.valueAt(i); + if (authority.authority.equals(providerName)) { + return authority.syncable; } } + return -1; + } + } + + public void setIsSyncable(Account account, String providerName, int syncable) { + int oldState; + if (syncable > 1) { + syncable = 1; + } else if (syncable < -1) { + syncable = -1; + } + Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName + " -> " + syncable); + synchronized (mAuthorities) { + AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + oldState = authority.syncable; + authority.syncable = syncable; writeAccountInfoLocked(); } - - reportChange(CHANGE_SETTINGS); + + if (oldState <= 0 && syncable > 0) { + mContext.getContentResolver().requestSync(account, providerName, new Bundle()); + } + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); } - public void setListenForNetworkTickles(boolean flag) { + public void setMasterSyncAutomatically(boolean flag) { + boolean old; synchronized (mAuthorities) { - mListenForTickles = flag; + old = mMasterSyncAutomatically; + mMasterSyncAutomatically = flag; writeAccountInfoLocked(); } - reportChange(CHANGE_SETTINGS); + if (!old && flag) { + mContext.getContentResolver().requestSync(null, null, new Bundle()); + } + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT); } - public boolean getListenForNetworkTickles() { + public boolean getMasterSyncAutomatically() { synchronized (mAuthorities) { - return mListenForTickles; + return mMasterSyncAutomatically; } } - - public AuthorityInfo getAuthority(String account, String authority) { + + public AuthorityInfo getAuthority(Account account, String authority) { synchronized (mAuthorities) { return getAuthorityLocked(account, authority, null); } } - + public AuthorityInfo getAuthority(int authorityId) { synchronized (mAuthorities) { return mAuthorities.get(authorityId); } } - + /** * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { synchronized (mAuthorities) { int i = mPendingOperations.size(); while (i > 0) { @@ -453,7 +476,7 @@ public class SyncStorageEngine extends Handler { return true; } } - + if (mActiveSync != null) { AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId); if (ainfo != null && ainfo.account.equals(account) @@ -462,17 +485,17 @@ public class SyncStorageEngine extends Handler { } } } - + return false; } - + public PendingOperation insertIntoPending(PendingOperation op) { synchronized (mAuthorities) { if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account + " auth=" + op.authority + " src=" + op.syncSource + " extras=" + op.extras); - + AuthorityInfo authority = getOrCreateAuthorityLocked(op.account, op.authority, -1 /* desired identifier */, @@ -480,17 +503,20 @@ public class SyncStorageEngine extends Handler { if (authority == null) { return null; } - + op = new PendingOperation(op); op.authorityId = authority.ident; mPendingOperations.add(op); appendPendingOperationLocked(op); - + SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = true; + status.initialize = op.extras != null && + op.extras.containsKey(ContentResolver.SYNC_EXTRAS_INITIALIZE) && + op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE); } - - reportChange(CHANGE_PENDING); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return op; } @@ -509,7 +535,7 @@ public class SyncStorageEngine extends Handler { } else { mNumPendingFinished++; } - + AuthorityInfo authority = getAuthorityLocked(op.account, op.authority, "deleteFromPending"); if (authority != null) { @@ -524,19 +550,19 @@ public class SyncStorageEngine extends Handler { break; } } - + if (!morePending) { if (DEBUG) Log.v(TAG, "no more pending!"); SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = false; } } - + res = true; } } - - reportChange(CHANGE_PENDING); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return res; } @@ -548,11 +574,11 @@ public class SyncStorageEngine extends Handler { mPendingOperations.clear(); final int N = mSyncStatus.size(); for (int i=0; i(mPendingOperations); } } - + /** * Return the number of currently pending operations. */ @@ -575,12 +601,12 @@ public class SyncStorageEngine extends Handler { return mPendingOperations.size(); } } - + /** * Called when the set of account has changed, given the new array of * active accounts. */ - public void doDatabaseCleanup(String[] accounts) { + public void doDatabaseCleanup(Account[] accounts) { synchronized (mAuthorities) { if (DEBUG) Log.w(TAG, "Updating for new accounts..."); SparseArray removing = new SparseArray(); @@ -596,7 +622,7 @@ public class SyncStorageEngine extends Handler { accIt.remove(); } } - + // Clean out all data structures. int i = removing.size(); if (i > 0) { @@ -658,21 +684,21 @@ public class SyncStorageEngine extends Handler { mActiveSync = null; } } - - reportChange(CHANGE_ACTIVE); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE); } /** * To allow others to send active change reports, to poke clients. */ public void reportActiveChange() { - reportChange(CHANGE_ACTIVE); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE); } - + /** * Note that sync has started for the given account and authority. */ - public long insertStartSyncEvent(String accountName, String authorityName, + public long insertStartSyncEvent(Account accountName, String authorityName, long now, int source) { long id; synchronized (mAuthorities) { @@ -697,8 +723,8 @@ public class SyncStorageEngine extends Handler { id = item.historyId; if (DEBUG) Log.v(TAG, "returning historyId " + id); } - - reportChange(CHANGE_STATUS); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); return id; } @@ -716,20 +742,20 @@ public class SyncStorageEngine extends Handler { } item = null; } - + if (item == null) { Log.w(TAG, "stopSyncEvent: no history for id " + historyId); return; } - + item.elapsedTime = elapsedTime; item.event = EVENT_STOP; item.mesg = resultMessage; item.downstreamActivity = downstreamActivity; item.upstreamActivity = upstreamActivity; - + SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId); - + status.numSyncs++; status.totalElapsedTime += elapsedTime; switch (item.source) { @@ -746,7 +772,7 @@ public class SyncStorageEngine extends Handler { status.numSourceServer++; break; } - + boolean writeStatisticsNow = false; int day = getCurrentDayLocked(); if (mDayStats[0] == null) { @@ -758,7 +784,7 @@ public class SyncStorageEngine extends Handler { } else if (mDayStats[0] == null) { } final DayStats ds = mDayStats[0]; - + final long lastSyncTime = (item.eventTime + elapsedTime); boolean writeStatusNow = false; if (MESG_SUCCESS.equals(resultMessage)) { @@ -787,7 +813,7 @@ public class SyncStorageEngine extends Handler { ds.failureCount++; ds.failureTime += elapsedTime; } - + if (writeStatusNow) { writeStatusLocked(); } else if (!hasMessages(MSG_WRITE_STATUS)) { @@ -799,10 +825,10 @@ public class SyncStorageEngine extends Handler { } else if (!hasMessages(MSG_WRITE_STATISTICS)) { sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS), WRITE_STATISTICS_DELAY); - } + } } - - reportChange(CHANGE_STATUS); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); } /** @@ -815,7 +841,7 @@ public class SyncStorageEngine extends Handler { return mActiveSync; } } - + /** * Return an array of the current sync status for all authorities. Note * that the objects inside the array are the real, live status objects, @@ -831,40 +857,41 @@ public class SyncStorageEngine extends Handler { return ops; } } - + /** - * Returns the status that matches the authority. If there are multiples accounts for - * the authority, the one with the latest "lastSuccessTime" status is returned. + * Returns the status that matches the authority and account. + * + * @param account the account we want to check * @param authority the authority whose row should be selected * @return the SyncStatusInfo for the authority, or null if none exists */ - public SyncStatusInfo getStatusByAuthority(String authority) { + public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) { + if (account == null || authority == null) { + throw new IllegalArgumentException(); + } synchronized (mAuthorities) { - SyncStatusInfo best = null; final int N = mSyncStatus.size(); for (int i=0; i cur.lastSuccessTime) { - best = cur; - } + + if (ainfo != null && ainfo.authority.equals(authority) && + account.equals(ainfo.account)) { + return cur; } } - return best; + return null; } } - + /** * Return true if the pending status is true of any matching authorities. */ - public boolean isAuthorityPending(String account, String authority) { + public boolean isSyncPending(Account account, String authority) { synchronized (mAuthorities) { final int N = mSyncStatus.size(); for (int i=0; i 0) { @@ -932,11 +959,11 @@ public class SyncStorageEngine extends Handler { } } } - + return oldest; } } - + private int getCurrentDayLocked() { mCal.setTimeInMillis(System.currentTimeMillis()); final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR); @@ -948,36 +975,40 @@ public class SyncStorageEngine extends Handler { } return dayOfYear + mYearInDays; } - + /** * Retrieve an authority, returning null if one does not exist. - * + * * @param accountName The name of the account for the authority. * @param authorityName The name of the authority itself. * @param tag If non-null, this will be used in a log message if the * requested authority does not exist. */ - private AuthorityInfo getAuthorityLocked(String accountName, String authorityName, + private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName, String tag) { AccountInfo account = mAccounts.get(accountName); if (account == null) { if (tag != null) { - Log.w(TAG, tag + ": unknown account " + accountName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, tag + ": unknown account " + accountName); + } } return null; } AuthorityInfo authority = account.authorities.get(authorityName); if (authority == null) { if (tag != null) { - Log.w(TAG, tag + ": unknown authority " + authorityName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, tag + ": unknown authority " + authorityName); + } } return null; } - + return authority; } - - private AuthorityInfo getOrCreateAuthorityLocked(String accountName, + + private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, String authorityName, int ident, boolean doWrite) { AccountInfo account = mAccounts.get(accountName); if (account == null) { @@ -997,6 +1028,8 @@ public class SyncStorageEngine extends Handler { ident++; } } + if (DEBUG) Log.v(TAG, "created a new AuthorityInfo for " + accountName + + ", provider " + authorityName); authority = new AuthorityInfo(accountName, authorityName, ident); account.authorities.put(authorityName, authority); mAuthorities.put(ident, authority); @@ -1004,10 +1037,10 @@ public class SyncStorageEngine extends Handler { writeAccountInfoLocked(); } } - + return authority; } - + private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) { SyncStatusInfo status = mSyncStatus.get(authorityId); if (status == null) { @@ -1016,22 +1049,22 @@ public class SyncStorageEngine extends Handler { } return status; } - + public void writeAllState() { synchronized (mAuthorities) { // Account info is always written so no need to do it here. - + if (mNumPendingFinished > 0) { // Only write these if they are out of date. writePendingOperationsLocked(); } - + // Just always write these... they are likely out of date. writeStatusLocked(); writeStatisticsLocked(); } } - + /** * Read all account information back in to the initial engine state. */ @@ -1050,7 +1083,7 @@ public class SyncStorageEngine extends Handler { if ("accounts".equals(tagName)) { String listen = parser.getAttributeValue( null, "listen-for-tickles"); - mListenForTickles = listen == null + mMasterSyncAutomatically = listen == null || Boolean.parseBoolean(listen); eventType = parser.next(); do { @@ -1068,26 +1101,43 @@ public class SyncStorageEngine extends Handler { if (id >= 0) { String accountName = parser.getAttributeValue( null, "account"); + String accountType = parser.getAttributeValue( + null, "type"); + if (accountType == null) { + accountType = "com.google"; + } String authorityName = parser.getAttributeValue( null, "authority"); String enabled = parser.getAttributeValue( null, "enabled"); - AuthorityInfo authority = mAuthorities.get(id); + String syncable = parser.getAttributeValue(null, "syncable"); + AuthorityInfo authority = mAuthorities.get(id); if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" + accountName + " auth=" + authorityName - + " enabled=" + enabled); + + " enabled=" + enabled + + " syncable=" + syncable); if (authority == null) { if (DEBUG_FILE) Log.v(TAG, "Creating entry"); authority = getOrCreateAuthorityLocked( - accountName, authorityName, id, false); + new Account(accountName, accountType), + authorityName, id, false); } if (authority != null) { authority.enabled = enabled == null || Boolean.parseBoolean(enabled); + if ("unknown".equals(syncable)) { + authority.syncable = -1; + } else { + authority.syncable = + (syncable == null || Boolean.parseBoolean(enabled)) + ? 1 + : 0; + } } else { Log.w(TAG, "Failure adding authority: account=" + accountName + " auth=" + authorityName - + " enabled=" + enabled); + + " enabled=" + enabled + + " syncable=" + syncable); } } } @@ -1109,43 +1159,49 @@ public class SyncStorageEngine extends Handler { } } } - + /** * Write all account information to the account file. */ private void writeAccountInfoLocked() { if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile()); FileOutputStream fos = null; - + try { fos = mAccountInfoFile.startWrite(); 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, "accounts"); - if (!mListenForTickles) { + if (!mMasterSyncAutomatically) { out.attribute(null, "listen-for-tickles", "false"); } - + final int N = mAuthorities.size(); for (int i=0; i= 11; + // Copy in all of the status information, as well as accounts. if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db"); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); @@ -1190,6 +1248,9 @@ public class SyncStorageEngine extends Handler { HashMap map = new HashMap(); map.put("_id", "status._id as _id"); map.put("account", "stats.account as account"); + if (hasType) { + map.put("account_type", "stats.account_type as account_type"); + } map.put("authority", "stats.authority as authority"); map.put("totalElapsedTime", "totalElapsedTime"); map.put("numSyncs", "numSyncs"); @@ -1208,16 +1269,22 @@ public class SyncStorageEngine extends Handler { Cursor c = qb.query(db, null, null, null, null, null, null); while (c.moveToNext()) { String accountName = c.getString(c.getColumnIndex("account")); + String accountType = hasType + ? c.getString(c.getColumnIndex("account_type")) : null; + if (accountType == null) { + accountType = "com.google"; + } String authorityName = c.getString(c.getColumnIndex("authority")); AuthorityInfo authority = this.getOrCreateAuthorityLocked( - accountName, authorityName, -1, false); + new Account(accountName, accountType), + authorityName, -1, false); if (authority != null) { int i = mSyncStatus.size(); boolean found = false; SyncStatusInfo st = null; while (i > 0) { i--; - st = mSyncStatus.get(i); + st = mSyncStatus.valueAt(i); if (st.authorityId == authority.ident) { found = true; break; @@ -1241,9 +1308,9 @@ public class SyncStorageEngine extends Handler { st.pending = getIntColumn(c, "pending") != 0; } } - + c.close(); - + // Retrieve the settings. qb = new SQLiteQueryBuilder(); qb.setTables("settings"); @@ -1253,29 +1320,35 @@ public class SyncStorageEngine extends Handler { String value = c.getString(c.getColumnIndex("value")); if (name == null) continue; if (name.equals("listen_for_tickles")) { - setListenForNetworkTickles(value == null - || Boolean.parseBoolean(value)); + setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value)); } else if (name.startsWith("sync_provider_")) { String provider = name.substring("sync_provider_".length(), name.length()); - setSyncProviderAutomatically(null, provider, - value == null || Boolean.parseBoolean(value)); + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.valueAt(i); + if (authority.authority.equals(provider)) { + authority.enabled = value == null || Boolean.parseBoolean(value); + authority.syncable = 1; + } + } } } - + c.close(); - + db.close(); - + writeAccountInfoLocked(); writeStatusLocked(); (new File(path)).delete(); } } - + public static final int STATUS_FILE_END = 0; public static final int STATUS_FILE_ITEM = 100; - + /** * Read all sync status back in to the initial engine state. */ @@ -1306,17 +1379,17 @@ public class SyncStorageEngine extends Handler { Log.i(TAG, "No initial status"); } } - + /** * Write all sync status to the sync status file. */ private void writeStatusLocked() { if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile()); - + // The file is being written, so we don't need to have a scheduled // write until the next change. removeMessages(MSG_WRITE_STATUS); - + FileOutputStream fos = null; try { fos = mStatusFile.startWrite(); @@ -1330,7 +1403,7 @@ public class SyncStorageEngine extends Handler { out.writeInt(STATUS_FILE_END); fos.write(out.marshall()); out.recycle(); - + mStatusFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing status", e1); @@ -1339,9 +1412,9 @@ public class SyncStorageEngine extends Handler { } } } - + public static final int PENDING_OPERATION_VERSION = 1; - + /** * Read all pending operations back in to the initial engine state. */ @@ -1385,7 +1458,7 @@ public class SyncStorageEngine extends Handler { Log.i(TAG, "No initial pending operations"); } } - + private void writePendingOperationLocked(PendingOperation op, Parcel out) { out.writeInt(PENDING_OPERATION_VERSION); out.writeInt(op.authorityId); @@ -1395,7 +1468,7 @@ public class SyncStorageEngine extends Handler { } out.writeByteArray(op.flatExtras); } - + /** * Write all currently pending ops to the pending ops file. */ @@ -1408,10 +1481,10 @@ public class SyncStorageEngine extends Handler { mPendingFile.truncate(); return; } - + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); fos = mPendingFile.startWrite(); - + Parcel out = Parcel.obtain(); for (int i=0; iAll of the onXXX callback methods here are called from a handler - * on the thread this object was created in. - * - *

      This interface is unused. It should be removed. - * - * @hide - */ -@Deprecated -public interface SyncUIContext { - - void setStatusText(String text); - - Context context(); -} diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java index e0cd78643be6420ae5d752da82516e05bca43480..ab4e91cf5c9306d3023e3bb367db261f44614422 100644 --- a/core/java/android/content/SyncableContentProvider.java +++ b/core/java/android/content/SyncableContentProvider.java @@ -19,6 +19,7 @@ package android.content; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.accounts.Account; import java.util.Map; @@ -32,6 +33,16 @@ import java.util.Map; public abstract class SyncableContentProvider extends ContentProvider { protected abstract boolean isTemporary(); + private volatile TempProviderSyncAdapter mTempProviderSyncAdapter; + + public void setTempProviderSyncAdapter(TempProviderSyncAdapter syncAdapter) { + mTempProviderSyncAdapter = syncAdapter; + } + + public TempProviderSyncAdapter getTempProviderSyncAdapter() { + return mTempProviderSyncAdapter; + } + /** * Close resources that must be closed. You must call this to properly release * the resources used by the SyncableContentProvider. @@ -110,7 +121,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * @param context the sync context for the operation * @param account */ - public abstract void onSyncStart(SyncContext context, String account); + public abstract void onSyncStart(SyncContext context, Account account); /** * Called right after a sync is completed @@ -124,7 +135,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * The account of the most recent call to onSyncStart() * @return the account */ - public abstract String getSyncingAccount(); + public abstract Account getSyncingAccount(); /** * Merge diffs from a sync source with this content provider. @@ -194,7 +205,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected abstract void onAccountsChanged(String[] accountsArray); + protected abstract void onAccountsChanged(Account[] accountsArray); /** * A helper method to delete all rows whose account is not in the accounts @@ -203,26 +214,24 @@ public abstract class SyncableContentProvider extends ContentProvider { * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected abstract void deleteRowsForRemovedAccounts(Map accounts, - String table, String accountColumnName); + protected abstract void deleteRowsForRemovedAccounts(Map accounts, + String table); /** * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public abstract void wipeAccount(String account); + public abstract void wipeAccount(Account account); /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public abstract byte[] readSyncDataBytes(String account); + public abstract byte[] readSyncDataBytes(Account account); /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public abstract void writeSyncDataBytes(String account, byte[] data); + public abstract void writeSyncDataBytes(Account account, byte[] data); } diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java index eb3a5da4a5b12e4c300fb6abc38dd39d44c14855..b46c5454ac2f03bd6384c8e86dda1528c7492184 100644 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -12,6 +12,11 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.TimingLogger; +import android.accounts.Account; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; + +import java.io.IOException; /** * @hide @@ -62,12 +67,10 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * * @param context allows you to publish status and interact with the * @param account the account to sync - * @param forced if true then the sync was forced + * @param manualSync true if this sync was requested manually by the user * @param result information to track what happened during this sync attempt - * @return true, if the sync was successfully started. One reason it can - * fail to start is if there is no user configured on the device. */ - public abstract void onSyncStarting(SyncContext context, String account, boolean forced, + public abstract void onSyncStarting(SyncContext context, Account account, boolean manualSync, SyncResult result); /** @@ -85,6 +88,9 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { */ public abstract boolean isReadOnly(); + public abstract boolean getIsSyncable(Account account) + throws IOException, AuthenticatorException, OperationCanceledException; + /** * Get diffs from the server since the last completed sync and put them * into a temporary provider. @@ -168,12 +174,13 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * exist. * @param accounts the list of accounts */ - public abstract void onAccountsChanged(String[] accounts); + public abstract void onAccountsChanged(Account[] accounts); private Context mContext; private class SyncThread extends Thread { - private final String mAccount; + private final Account mAccount; + private final String mAuthority; private final Bundle mExtras; private final SyncContext mSyncContext; private volatile boolean mIsCanceled = false; @@ -181,9 +188,10 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { private long mInitialRxBytes; private final SyncResult mResult; - SyncThread(SyncContext syncContext, String account, Bundle extras) { + SyncThread(SyncContext syncContext, Account account, String authority, Bundle extras) { super("SyncThread"); mAccount = account; + mAuthority = authority; mExtras = extras; mSyncContext = syncContext; mResult = new SyncResult(); @@ -207,7 +215,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { mInitialTxBytes = NetStat.getUidTxBytes(uid); mInitialRxBytes = NetStat.getUidRxBytes(uid); try { - sync(mSyncContext, mAccount, mExtras); + sync(mSyncContext, mAccount, mAuthority, mExtras); } catch (SQLException e) { Log.e(TAG, "Sync failed", e); mResult.databaseError = true; @@ -221,19 +229,45 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void sync(SyncContext syncContext, String account, Bundle extras) { + private void sync(SyncContext syncContext, Account account, String authority, + Bundle extras) { mIsCanceled = false; mProviderSyncStarted = false; mAdapterSyncStarted = false; String message = null; - boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + // always attempt to initialize if the isSyncable state isn't set yet + int isSyncable = ContentResolver.getIsSyncable(account, authority); + if (isSyncable < 0) { + try { + isSyncable = (getIsSyncable(account)) ? 1 : 0; + ContentResolver.setIsSyncable(account, authority, isSyncable); + } catch (IOException e) { + ++mResult.stats.numIoExceptions; + } catch (AuthenticatorException e) { + ++mResult.stats.numParseExceptions; + } catch (OperationCanceledException e) { + // do nothing + } + } + + // if this is an initialization request then our work is done here + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + return; + } + + // if we aren't syncable then get out + if (isSyncable <= 0) { + return; + } + + boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); try { mProvider.onSyncStart(syncContext, account); mProviderSyncStarted = true; - onSyncStarting(syncContext, account, syncForced, mResult); + onSyncStarting(syncContext, account, manualSync, mResult); if (mResult.hasError()) { message = "SyncAdapter failed while trying to start sync"; return; @@ -273,7 +307,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) { + private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) { TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); syncTimer.addSplit("start"); int loopCount = 0; @@ -518,13 +552,14 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); } - public void startSync(SyncContext syncContext, String account, Bundle extras) { + public void startSync(SyncContext syncContext, Account account, String authority, + Bundle extras) { if (mSyncThread != null) { syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); return; } - mSyncThread = new SyncThread(syncContext, account, extras); + mSyncThread = new SyncThread(syncContext, account, authority, extras); mSyncThread.start(); } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 27783efea9dff42301e48885fc25b1a61cabf38d..87da55f8a814596a548cc55f99a5ced0906a10b5 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -126,13 +126,21 @@ public class ActivityInfo extends ComponentInfo * {@link android.R.attr#noHistory} attribute. */ public static final int FLAG_NO_HISTORY = 0x0080; + /** + * Bit in {@link #flags} indicating that, when a request to close system + * windows happens, this activity is finished. + * Set from the + * {@link android.R.attr#finishOnCloseSystemDialogs} attribute. + */ + public static final int FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS = 0x0100; /** * Options that have been set in the activity declaration in the * manifest: {@link #FLAG_MULTIPROCESS}, * {@link #FLAG_FINISH_ON_TASK_LAUNCH}, {@link #FLAG_CLEAR_TASK_ON_LAUNCH}, * {@link #FLAG_ALWAYS_RETAIN_TASK_STATE}, * {@link #FLAG_STATE_NOT_NEEDED}, {@link #FLAG_EXCLUDE_FROM_RECENTS}, - * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}. + * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}, + * {@link #FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS}. */ public int flags; @@ -217,7 +225,9 @@ public class ActivityInfo extends ComponentInfo public static final int CONFIG_KEYBOARD = 0x0010; /** * Bit in {@link #configChanges} that indicates that the activity - * can itself handle changes to the keyboard being hidden/exposed. + * can itself handle changes to the keyboard or navigation being hidden/exposed. + * Note that inspite of the name, this applies to the changes to any + * hidden states: keyboard or navigation. * Set from the {@link android.R.attr#configChanges} attribute. */ public static final int CONFIG_KEYBOARD_HIDDEN = 0x0020; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 0a42a6faf9f4b53d32051b846999d2de2a55bfcb..7a65af8a9a154b41b28e2ed228ac29dbf3eb7ec0 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -184,7 +184,29 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@hide} */ public static final int FLAG_ALLOW_BACKUP = 1<<14; - + + /** + * Value for {@link #flags}: this is false if the application has set + * its android:killAfterRestore to false, true otherwise. + * + *

      If android:allowBackup is set to false or no android:backupAgent + * is specified, this flag will be ignored. + * + * {@hide} + */ + public static final int FLAG_KILL_AFTER_RESTORE = 1<<15; + + /** + * Value for {@link #flags}: this is true if the application has set + * its android:restoreNeedsApplication to true, false otherwise. + * + *

      If android:allowBackup is set to false or no android:backupAgent + * is specified, this flag will be ignored. + * + * {@hide} + */ + public static final int FLAG_RESTORE_NEEDS_APPLICATION = 1<<16; + /** * Flags associated with the application. Any combination of * {@link #FLAG_SYSTEM}, {@link #FLAG_DEBUGGABLE}, {@link #FLAG_HAS_CODE}, @@ -193,7 +215,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@link #FLAG_ALLOW_CLEAR_USER_DATA}, {@link #FLAG_UPDATED_SYSTEM_APP}, * {@link #FLAG_TEST_ONLY}, {@link #FLAG_SUPPORTS_SMALL_SCREENS}, * {@link #FLAG_SUPPORTS_NORMAL_SCREENS}, - * {@link #FLAG_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_RESIZEABLE_FOR_SCREENS}. + * {@link #FLAG_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_RESIZEABLE_FOR_SCREENS}, + * {@link #FLAG_SUPPORTS_SCREEN_DENSITIES}. */ public int flags = 0; @@ -230,7 +253,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public int uid; /** - * The minimum SDK version this application targets. It may run on earilier + * The minimum SDK version this application targets. It may run on earlier * versions, but it knows how to work with any new behavior added at this * version. Will be {@link android.os.Build.VERSION_CODES#CUR_DEVELOPMENT} * if this is a development build and the app is targeting that. You should diff --git a/core/java/android/content/pm/ConfigurationInfo.java b/core/java/android/content/pm/ConfigurationInfo.java index fb7a47fbd1bd3438345df93f61fcc5c94ec91eb9..8edd436fa8ee29b4a2ab954b8743919a2eef925d 100755 --- a/core/java/android/content/pm/ConfigurationInfo.java +++ b/core/java/android/content/pm/ConfigurationInfo.java @@ -22,9 +22,9 @@ import android.os.Parcelable; /** * Information you can retrieve about hardware configuration preferences * declared by an application. This corresponds to information collected from the - * AndroidManifest.xml's <uses-configuration> and the <uses-feature>tags. + * AndroidManifest.xml's <uses-configuration> and <uses-feature> tags. */ -public class ConfigurationInfo implements Parcelable { +public class ConfigurationInfo implements Parcelable { /** * The kind of touch screen attached to the device. * One of: {@link android.content.res.Configuration#TOUCHSCREEN_NOTOUCH}, @@ -92,13 +92,13 @@ public class ConfigurationInfo implements Parcelable { } public String toString() { - return "ApplicationHardwarePreferences{" + return "ConfigurationInfo{" + Integer.toHexString(System.identityHashCode(this)) - + ", touchscreen = " + reqTouchScreen + "}" - + ", inputMethod = " + reqKeyboardType + "}" - + ", navigation = " + reqNavigation + "}" - + ", reqInputFeatures = " + reqInputFeatures + "}" - + ", reqGlEsVersion = " + reqGlEsVersion + "}"; + + " touchscreen = " + reqTouchScreen + + " inputMethod = " + reqKeyboardType + + " navigation = " + reqNavigation + + " reqInputFeatures = " + reqInputFeatures + + " reqGlEsVersion = " + reqGlEsVersion + "}"; } public int describeContents() { diff --git a/core/java/android/content/pm/FeatureInfo.aidl b/core/java/android/content/pm/FeatureInfo.aidl new file mode 100755 index 0000000000000000000000000000000000000000..d84a84c3ce73b1cc93fbf4ab18fa2dc416935249 --- /dev/null +++ b/core/java/android/content/pm/FeatureInfo.aidl @@ -0,0 +1,19 @@ +/* +** 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 android.content.pm; + +parcelable FeatureInfo; diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..57d61fda7bb2d882031425e20499cf43e25718ab --- /dev/null +++ b/core/java/android/content/pm/FeatureInfo.java @@ -0,0 +1,101 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Parcelable.Creator; + +/** + * A single feature that can be requested by an application. This corresponds + * to information collected from the + * AndroidManifest.xml's <uses-feature> tag. + */ +public class FeatureInfo implements Parcelable { + /** + * The name of this feature, for example "android.hardware.camera". If + * this is null, then this is an OpenGL ES version feature as described + * in {@link #reqGlEsVersion}. + */ + public String name; + + /** + * Default value for {@link #reqGlEsVersion}; + */ + public static final int GL_ES_VERSION_UNDEFINED = 0; + + /** + * The GLES version used by an application. The upper order 16 bits represent the + * major version and the lower order 16 bits the minor version. Only valid + * if {@link #name} is null. + */ + public int reqGlEsVersion; + + /** + * Set on {@link #flags} if this feature has been required by the application. + */ + public static final int FLAG_REQUIRED = 0x0001; + + /** + * Additional flags. May be zero or more of {@link #FLAG_REQUIRED}. + */ + public int flags; + + public FeatureInfo() { + } + + public FeatureInfo(FeatureInfo orig) { + name = orig.name; + reqGlEsVersion = orig.reqGlEsVersion; + flags = orig.flags; + } + + public String toString() { + if (name != null) { + return "FeatureInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + " fl=0x" + Integer.toHexString(flags) + "}"; + } else { + return "FeatureInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " glEsVers=" + getGlEsVersion() + + " fl=0x" + Integer.toHexString(flags) + "}"; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeString(name); + dest.writeInt(reqGlEsVersion); + dest.writeInt(flags); + } + + public static final Creator CREATOR = + new Creator() { + public FeatureInfo createFromParcel(Parcel source) { + return new FeatureInfo(source); + } + public FeatureInfo[] newArray(int size) { + return new FeatureInfo[size]; + } + }; + + private FeatureInfo(Parcel source) { + name = source.readString(); + reqGlEsVersion = source.readInt(); + flags = source.readInt(); + } + + /** + * This method extracts the major and minor version of reqGLEsVersion attribute + * and returns it as a string. Say reqGlEsVersion value of 0x00010002 is returned + * as 1.2 + * @return String representation of the reqGlEsVersion attribute + */ + public String getGlEsVersion() { + int major = ((reqGlEsVersion & 0xffff0000) >> 16); + int minor = reqGlEsVersion & 0x0000ffff; + return String.valueOf(major)+"."+String.valueOf(minor); + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index e587ca7597601cfe88023081bac2b1deb197a68b..fc6538fbe05d5edf2f5a47bfa9209b7f5dee3904 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.FeatureInfo; import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageDataObserver; @@ -75,6 +76,8 @@ interface IPackageManager { int checkSignatures(String pkg1, String pkg2); + int checkUidSignatures(int uid1, int uid2); + String[] getPackagesForUid(int uid); String getNameForUid(int uid); @@ -274,6 +277,14 @@ interface IPackageManager { */ String[] getSystemSharedLibraryNames(); + /** + * Get a list of features that are available on the + * system. + */ + FeatureInfo[] getSystemAvailableFeatures(); + + boolean hasSystemFeature(String name); + void enterSafeMode(); boolean isSafeMode(); void systemReady(); diff --git a/core/java/android/content/pm/LabeledIntent.java b/core/java/android/content/pm/LabeledIntent.java new file mode 100644 index 0000000000000000000000000000000000000000..d70a698d3b8103be3ff61d44701f4c7532926e18 --- /dev/null +++ b/core/java/android/content/pm/LabeledIntent.java @@ -0,0 +1,177 @@ +package android.content.pm; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.text.TextUtils; + +/** + * A special subclass of Intent that can have a custom label/icon + * associated with it. Primarily for use with {@link Intent#ACTION_CHOOSER}. + */ +public class LabeledIntent extends Intent { + private String mSourcePackage; + private int mLabelRes; + private CharSequence mNonLocalizedLabel; + private int mIcon; + + /** + * Create a labeled intent from the given intent, supplying the label + * and icon resources for it. + * + * @param origIntent The original Intent to copy. + * @param sourcePackage The package in which the label and icon live. + * @param labelRes Resource containing the label, or 0 if none. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(Intent origIntent, String sourcePackage, + int labelRes, int icon) { + super(origIntent); + mSourcePackage = sourcePackage; + mLabelRes = labelRes; + mNonLocalizedLabel = null; + mIcon = icon; + } + + /** + * Create a labeled intent from the given intent, supplying a textual + * label and icon resource for it. + * + * @param origIntent The original Intent to copy. + * @param sourcePackage The package in which the label and icon live. + * @param nonLocalizedLabel Concrete text to use for the label. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(Intent origIntent, String sourcePackage, + CharSequence nonLocalizedLabel, int icon) { + super(origIntent); + mSourcePackage = sourcePackage; + mLabelRes = 0; + mNonLocalizedLabel = nonLocalizedLabel; + mIcon = icon; + } + + /** + * Create a labeled intent with no intent data but supplying the label + * and icon resources for it. + * + * @param sourcePackage The package in which the label and icon live. + * @param labelRes Resource containing the label, or 0 if none. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(String sourcePackage, int labelRes, int icon) { + mSourcePackage = sourcePackage; + mLabelRes = labelRes; + mNonLocalizedLabel = null; + mIcon = icon; + } + + /** + * Create a labeled intent with no intent data but supplying a textual + * label and icon resource for it. + * + * @param sourcePackage The package in which the label and icon live. + * @param nonLocalizedLabel Concrete text to use for the label. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(String sourcePackage, + CharSequence nonLocalizedLabel, int icon) { + mSourcePackage = sourcePackage; + mLabelRes = 0; + mNonLocalizedLabel = nonLocalizedLabel; + mIcon = icon; + } + + /** + * Return the name of the package holding label and icon resources. + */ + public String getSourcePackage() { + return mSourcePackage; + } + + /** + * Return any resource identifier that has been given for the label text. + */ + public int getLabelResource() { + return mLabelRes; + } + + /** + * Return any concrete text that has been given for the label text. + */ + public CharSequence getNonLocalizedLabel() { + return mNonLocalizedLabel; + } + + /** + * Return any resource identifier that has been given for the label icon. + */ + public int getIconResource() { + return mIcon; + } + + /** + * Retrieve the label associated with this object. If the object does + * not have a label, null will be returned, in which case you will probably + * want to load the label from the underlying resolved info for the Intent. + */ + public CharSequence loadLabel(PackageManager pm) { + if (mNonLocalizedLabel != null) { + return mNonLocalizedLabel; + } + if (mLabelRes != 0 && mSourcePackage != null) { + CharSequence label = pm.getText(mSourcePackage, mLabelRes, null); + if (label != null) { + return label; + } + } + return null; + } + + /** + * Retrieve the icon associated with this object. If the object does + * not have a icon, null will be returned, in which case you will probably + * want to load the icon from the underlying resolved info for the Intent. + */ + public Drawable loadIcon(PackageManager pm) { + if (mIcon != 0 && mSourcePackage != null) { + Drawable icon = pm.getDrawable(mSourcePackage, mIcon, null); + if (icon != null) { + return icon; + } + } + return null; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(mSourcePackage); + dest.writeInt(mLabelRes); + TextUtils.writeToParcel(mNonLocalizedLabel, dest, parcelableFlags); + dest.writeInt(mIcon); + } + + /** @hide */ + protected LabeledIntent(Parcel in) { + readFromParcel(in); + } + + public void readFromParcel(Parcel in) { + super.readFromParcel(in); + mSourcePackage = in.readString(); + mLabelRes = in.readInt(); + mNonLocalizedLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readInt(); + } + + public static final Creator CREATOR + = new Creator() { + public LabeledIntent createFromParcel(Parcel source) { + return new LabeledIntent(source); + } + public LabeledIntent[] newArray(int size) { + return new LabeledIntent[size]; + } + }; + +} diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index d9326f211b5ba39d3a454d96f8c401bae07280df..a8ce8890eba1ab172a54c9425b1d872b1994f8a0 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -127,6 +127,11 @@ public class PackageInfo implements Parcelable { */ public ConfigurationInfo[] configPreferences; + /** + * The features that this application has said it requires. + */ + public FeatureInfo[] reqFeatures; + public PackageInfo() { } @@ -162,6 +167,7 @@ public class PackageInfo implements Parcelable { dest.writeStringArray(requestedPermissions); dest.writeTypedArray(signatures, parcelableFlags); dest.writeTypedArray(configPreferences, parcelableFlags); + dest.writeTypedArray(reqFeatures, parcelableFlags); } public static final Parcelable.Creator CREATOR @@ -195,5 +201,6 @@ public class PackageInfo implements Parcelable { requestedPermissions = source.createStringArray(); signatures = source.createTypedArray(Signature.CREATOR); configPreferences = source.createTypedArray(ConfigurationInfo.CREATOR); + reqFeatures = source.createTypedArray(FeatureInfo.CREATOR); } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 67bd1acd989c06fa61de10bc797fb79ae0a0ed71..cd48dcbcba3b5886f01a986ef71cb08412505e46 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -159,8 +159,10 @@ public abstract class PackageManager { /** * {@link PackageInfo} flag: return information about - * hardware preferences - * {@link PackageInfo#configPreferences} + * hardware preferences in + * {@link PackageInfo#configPreferences PackageInfo.configPreferences} and + * requested features in {@link PackageInfo#reqFeatures + * PackageInfo.reqFeatures}. */ public static final int GET_CONFIGURATIONS = 0x00004000; @@ -399,6 +401,14 @@ public abstract class PackageManager { */ public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a feature that is not available. + * @hide + */ + public static final int INSTALL_FAILED_MISSING_FEATURE = -17; + /** * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} @@ -865,6 +875,7 @@ public abstract class PackageManager { * {@link #SIGNATURE_SECOND_NOT_SIGNED}, {@link #SIGNATURE_NO_MATCH}, * or {@link #SIGNATURE_UNKNOWN_PACKAGE}. * + * @see #checkSignatures(int, int) * @see #SIGNATURE_MATCH * @see #SIGNATURE_NEITHER_SIGNED * @see #SIGNATURE_FIRST_NOT_SIGNED @@ -874,6 +885,34 @@ public abstract class PackageManager { */ public abstract int checkSignatures(String pkg1, String pkg2); + /** + * Like {@link #checkSignatures(String, String)}, but takes UIDs of + * the two packages to be checked. This can be useful, for example, + * when doing the check in an IPC, where the UID is the only identity + * available. It is functionally identical to determining the package + * associated with the UIDs and checking their signatures. + * + * @param uid1 First UID whose signature will be compared. + * @param uid2 Second UID whose signature will be compared. + * @return Returns an integer indicating whether there is a matching + * signature: the value is >= 0 if there is a match (or neither package + * is signed), or < 0 if there is not a match. The match result can be + * further distinguished with the success (>= 0) constants + * {@link #SIGNATURE_MATCH}, {@link #SIGNATURE_NEITHER_SIGNED}; or + * failure (< 0) constants {@link #SIGNATURE_FIRST_NOT_SIGNED}, + * {@link #SIGNATURE_SECOND_NOT_SIGNED}, {@link #SIGNATURE_NO_MATCH}, + * or {@link #SIGNATURE_UNKNOWN_PACKAGE}. + * + * @see #checkSignatures(int, int) + * @see #SIGNATURE_MATCH + * @see #SIGNATURE_NEITHER_SIGNED + * @see #SIGNATURE_FIRST_NOT_SIGNED + * @see #SIGNATURE_SECOND_NOT_SIGNED + * @see #SIGNATURE_NO_MATCH + * @see #SIGNATURE_UNKNOWN_PACKAGE + */ + public abstract int checkSignatures(int uid1, int uid2); + /** * Retrieve the names of all packages that are associated with a particular * user id. In most cases, this will be a single package name, the package @@ -950,6 +989,24 @@ public abstract class PackageManager { */ public abstract String[] getSystemSharedLibraryNames(); + /** + * Get a list of features that are available on the + * system. + * + * @return An array of FeatureInfo classes describing the features + * that are available on the system, or null if there are none(!!). + */ + public abstract FeatureInfo[] getSystemAvailableFeatures(); + + /** + * Check whether the given feature name is one of the available + * features as returned by {@link #getSystemAvailableFeatures()}. + * + * @return Returns true if the devices supports the feature, else + * false. + */ + public abstract boolean hasSystemFeature(String name); + /** * Determine the best action to perform for a given Intent. This is how * {@link Intent#resolveActivity} finds an activity if a class has not @@ -1429,8 +1486,6 @@ public abstract class PackageManager { * which market the package came from. * * @param packageName The name of the package to query - * - * @hide */ public abstract String getInstallerPackageName(String packageName); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 33f4b52e2f94ab3593d05b24f7e4f4a061ddbac6..b798bde023386903e403a1ce945b4570683ac18e 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -27,6 +27,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; import android.util.AttributeSet; @@ -84,8 +85,9 @@ public class PackageParser { private String mArchiveSourcePath; private String[] mSeparateProcesses; - private int mSdkVersion; - private String mSdkCodename; + private static final int SDK_VERSION = Build.VERSION.SDK_INT; + private static final String SDK_CODENAME = "REL".equals(Build.VERSION.CODENAME) + ? null : Build.VERSION.CODENAME; private int mParseError = PackageManager.INSTALL_SUCCEEDED; @@ -152,11 +154,6 @@ public class PackageParser { mSeparateProcesses = procs; } - public void setSdkVersion(int sdkVersion, String codename) { - mSdkVersion = sdkVersion; - mSdkCodename = codename; - } - private static final boolean isPackageFilename(String name) { return name.endsWith(".apk"); } @@ -184,20 +181,31 @@ public class PackageParser { int N = p.configPreferences.size(); if (N > 0) { pi.configPreferences = new ConfigurationInfo[N]; - for (int i=0; i 0) { + pi.reqFeatures = new FeatureInfo[N]; + p.reqFeatures.toArray(pi.reqFeatures); } } if ((flags&PackageManager.GET_ACTIVITIES) != 0) { int N = p.activities.size(); if (N > 0) { - pi.activities = new ActivityInfo[N]; - for (int i=0; i 0) { - pi.receivers = new ActivityInfo[N]; - for (int i=0; i 0) { - pi.services = new ServiceInfo[N]; - for (int i=0; i 0) { - pi.providers = new ProviderInfo[N]; - for (int i=0; i 0) { + int N = (p.mSignatures != null) ? p.mSignatures.length : 0; + if (N > 0) { pi.signatures = new Signature[N]; System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N); } @@ -758,19 +790,37 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("uses-feature")) { - ConfigurationInfo cPref = new ConfigurationInfo(); + FeatureInfo fi = new FeatureInfo(); sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestUsesFeature); - cPref.reqGlEsVersion = sa.getInt( - com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, - ConfigurationInfo.GL_ES_VERSION_UNDEFINED); + fi.name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesFeature_name); + if (fi.name == null) { + fi.reqGlEsVersion = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, + FeatureInfo.GL_ES_VERSION_UNDEFINED); + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesFeature_required, + true)) { + fi.flags |= FeatureInfo.FLAG_REQUIRED; + } sa.recycle(); - pkg.configPreferences.add(cPref); + if (pkg.reqFeatures == null) { + pkg.reqFeatures = new ArrayList(); + } + pkg.reqFeatures.add(fi); + + if (fi.name == null) { + ConfigurationInfo cPref = new ConfigurationInfo(); + cPref.reqGlEsVersion = fi.reqGlEsVersion; + pkg.configPreferences.add(cPref); + } XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("uses-sdk")) { - if (mSdkVersion > 0) { + if (SDK_VERSION > 0) { sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestUsesSdk); @@ -803,15 +853,15 @@ public class PackageParser { int maxVers = sa.getInt( com.android.internal.R.styleable.AndroidManifestUsesSdk_maxSdkVersion, - mSdkVersion); + SDK_VERSION); sa.recycle(); if (minCode != null) { - if (!minCode.equals(mSdkCodename)) { - if (mSdkCodename != null) { + if (!minCode.equals(SDK_CODENAME)) { + if (SDK_CODENAME != null) { outError[0] = "Requires development platform " + minCode - + " (current platform is " + mSdkCodename + ")"; + + " (current platform is " + SDK_CODENAME + ")"; } else { outError[0] = "Requires development platform " + minCode + " but this is a release platform."; @@ -819,18 +869,18 @@ public class PackageParser { mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } - } else if (minVers > mSdkVersion) { + } else if (minVers > SDK_VERSION) { outError[0] = "Requires newer sdk version #" + minVers - + " (current version is #" + mSdkVersion + ")"; + + " (current version is #" + SDK_VERSION + ")"; mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } if (targetCode != null) { - if (!targetCode.equals(mSdkCodename)) { - if (mSdkCodename != null) { + if (!targetCode.equals(SDK_CODENAME)) { + if (SDK_CODENAME != null) { outError[0] = "Requires development platform " + targetCode - + " (current platform is " + mSdkCodename + ")"; + + " (current platform is " + SDK_CODENAME + ")"; } else { outError[0] = "Requires development platform " + targetCode + " but this is a release platform."; @@ -845,9 +895,9 @@ public class PackageParser { pkg.applicationInfo.targetSdkVersion = targetVers; } - if (maxVers < mSdkVersion) { + if (maxVers < SDK_VERSION) { outError[0] = "Requires older sdk version #" + maxVers - + " (current version is #" + mSdkVersion + ")"; + + " (current version is #" + SDK_VERSION + ")"; mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } @@ -918,8 +968,9 @@ public class PackageParser { return null; } else { - Log.w(TAG, "Bad element under : " - + parser.getName()); + Log.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -931,6 +982,7 @@ public class PackageParser { } final int NP = PackageParser.NEW_PERMISSIONS.length; + StringBuilder implicitPerms = null; for (int ip=0; ip 0) { - pkg.usesLibraryFiles = new String[pkg.usesLibraries.size()]; - pkg.usesLibraries.toArray(pkg.usesLibraryFiles); + if (implicitPerms != null) { + Log.i(TAG, implicitPerms.toString()); } if (supportsSmallScreens < 0 || (supportsSmallScreens > 0 @@ -1278,12 +1334,28 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestApplication_allowBackup, true); if (allowBackup) { ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; + + // backupAgent, killAfterRestore, and restoreNeedsApplication are only relevant + // if backup is possible for the given application. String backupAgent = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestApplication_backupAgent); if (backupAgent != null) { ai.backupAgentName = buildClassName(pkgName, backupAgent, outError); - Log.v(TAG, "android:backupAgent = " + ai.backupAgentName - + " from " + pkgName + "+" + backupAgent); + if (false) { + Log.v(TAG, "android:backupAgent = " + ai.backupAgentName + + " from " + pkgName + "+" + backupAgent); + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_killAfterRestore, + true)) { + ai.flags |= ApplicationInfo.FLAG_KILL_AFTER_RESTORE; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_restoreNeedsApplication, + false)) { + ai.flags |= ApplicationInfo.FLAG_RESTORE_NEEDS_APPLICATION; + } } } @@ -1434,19 +1506,37 @@ public class PackageParser { String lname = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + boolean req = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_required, + true); sa.recycle(); - if (lname != null && !owner.usesLibraries.contains(lname)) { - owner.usesLibraries.add(lname.intern()); + if (lname != null) { + if (req) { + if (owner.usesLibraries == null) { + owner.usesLibraries = new ArrayList(); + } + if (!owner.usesLibraries.contains(lname)) { + owner.usesLibraries.add(lname.intern()); + } + } else { + if (owner.usesOptionalLibraries == null) { + owner.usesOptionalLibraries = new ArrayList(); + } + if (!owner.usesOptionalLibraries.contains(lname)) { + owner.usesOptionalLibraries.add(lname.intern()); + } + } } XmlUtils.skipCurrentTag(parser); } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under : " + tagName); + Log.w(TAG, "Unknown element under : " + tagName + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } else { @@ -1491,25 +1581,6 @@ public class PackageParser { return true; } - private boolean parseComponentInfo(Package owner, int flags, - ComponentInfo outInfo, String[] outError, String tag, TypedArray sa, - int nameRes, int labelRes, int iconRes, int processRes, - int enabledRes) { - if (!parsePackageItemInfo(owner, outInfo, outError, tag, sa, - nameRes, labelRes, iconRes)) { - return false; - } - - if (processRes != 0) { - outInfo.processName = buildProcessName(owner.applicationInfo.packageName, - owner.applicationInfo.processName, sa.getNonResourceString(processRes), - flags, mSeparateProcesses, outError); - } - outInfo.enabled = sa.getBoolean(enabledRes, true); - - return outError[0] == null; - } - private Activity parseActivity(Package owner, Resources res, XmlPullParser parser, AttributeSet attrs, int flags, String[] outError, boolean receiver) throws XmlPullParserException, IOException { @@ -1609,6 +1680,12 @@ public class PackageParser { a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING; } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_finishOnCloseSystemDialogs, + false)) { + a.info.flags |= ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS; + } + if (!receiver) { a.info.launchMode = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_launchMode, @@ -1648,8 +1725,9 @@ public class PackageParser { return null; } if (intent.countActions() == 0) { - Log.w(TAG, "Intent filter for activity " + intent - + " defines no actions"); + Log.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { a.intents.add(intent); } @@ -1662,9 +1740,13 @@ public class PackageParser { if (!RIGID_PARSER) { Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); if (receiver) { - Log.w(TAG, "Unknown element under : " + parser.getName()); + Log.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { - Log.w(TAG, "Unknown element under : " + parser.getName()); + Log.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } XmlUtils.skipCurrentTag(parser); continue; @@ -1792,8 +1874,9 @@ public class PackageParser { return null; } if (intent.countActions() == 0) { - Log.w(TAG, "Intent filter for activity alias " + intent - + " defines no actions"); + Log.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { a.intents.add(intent); } @@ -1804,8 +1887,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under : " + parser.getName()); + Log.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -1968,8 +2052,9 @@ public class PackageParser { outInfo.info.grantUriPermissions = true; } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No path, pathPrefix, or pathPattern for "); + Log.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2009,8 +2094,9 @@ public class PackageParser { if (!havePerm) { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No readPermission or writePermssion for "); + Log.w(TAG, "No readPermission or writePermssion for : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2054,8 +2140,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No path, pathPrefix, or pathPattern for "); + Log.w(TAG, "No path, pathPrefix, or pathPattern for : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2066,9 +2153,9 @@ public class PackageParser { } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under : " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2146,9 +2233,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under : " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2185,9 +2272,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under " + tag + ": " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2243,8 +2330,9 @@ public class PackageParser { data.putFloat(name, v.getFloat()); } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, " only supports string, integer, float, color, boolean, and resource reference types"); + Log.w(TAG, " only supports string, integer, float, color, boolean, and resource reference types: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { outError[0] = " only supports string, integer, float, color, boolean, and resource reference types"; data = null; @@ -2278,6 +2366,7 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestIntentFilter_priority, 0); if (priority > 0 && isActivity && (flags&PARSE_IS_SYSTEM) == 0) { Log.w(TAG, "Activity with priority > 0, forcing to 0 at " + + mArchiveSourcePath + " " + parser.getPositionDescription()); priority = 0; } @@ -2375,8 +2464,9 @@ public class PackageParser { sa.recycle(); XmlUtils.skipCurrentTag(parser); } else if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under : " + parser.getName()); + Log.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); } else { outError[0] = "Bad element under : " + parser.getName(); @@ -2416,7 +2506,8 @@ public class PackageParser { public ArrayList protectedBroadcasts; - public final ArrayList usesLibraries = new ArrayList(); + public ArrayList usesLibraries = null; + public ArrayList usesOptionalLibraries = null; public String[] usesLibraryFiles = null; // We store the application meta-data independently to avoid multiple unwanted references @@ -2464,6 +2555,11 @@ public class PackageParser { public final ArrayList configPreferences = new ArrayList(); + /* + * Applications requested features + */ + public ArrayList reqFeatures = null; + public Package(String _name) { packageName = _name; applicationInfo.packageName = _name; diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index d01460e4216e95ee7aa17c3b59086d29c2ff6b91..ec017754fc1064fb3c0839293911f2ccec9a25b4 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -74,7 +74,12 @@ public final class ProviderInfo extends ComponentInfo * running in the same process. Higher goes first. */ public int initOrder = 0; - /** Whether or not this provider is syncable. */ + /** + * Whether or not this provider is syncable. + * @deprecated This flag is now being ignored. The current way to make a provider + * syncable is to provide a SyncAdapter service for a given provider/account type. + */ + @Deprecated public boolean isSyncable = false; public ProviderInfo() { diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java new file mode 100644 index 0000000000000000000000000000000000000000..b39a67de5f8b36c3d5c57b39254374a519a0f3eb --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ComponentName; +import android.content.res.XmlResourceParser; +import android.os.Environment; +import android.os.Handler; +import android.util.Log; +import android.util.AttributeSet; +import android.util.Xml; + +import java.util.Map; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.IOException; +import java.io.FileInputStream; + +import com.android.internal.os.AtomicFile; +import com.android.internal.util.FastXmlSerializer; + +import com.google.android.collect.Maps; +import com.google.android.collect.Lists; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +/** + * A cache of registered services. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The services are referred to by type V and + * are made available via the {@link #getServiceInfo} method. + * @hide + */ +public abstract class RegisteredServicesCache { + private static final String TAG = "PackageManager"; + + public final Context mContext; + private final String mInterfaceName; + private final String mMetaDataName; + private final String mAttributesName; + private final XmlSerializerAndParser mSerializerAndParser; + private final AtomicReference mReceiver; + + private final Object mServicesLock = new Object(); + // synchronized on mServicesLock + private HashMap mPersistentServices; + // synchronized on mServicesLock + private Map> mServices; + // synchronized on mServicesLock + private boolean mPersistentServicesFileDidNotExist; + + /** + * This file contains the list of known services. We would like to maintain this forever + * so we store it as an XML file. + */ + private final AtomicFile mPersistentServicesFile; + + // the listener and handler are synchronized on "this" and must be updated together + private RegisteredServicesCacheListener mListener; + private Handler mHandler; + + public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, + String attributeName, XmlSerializerAndParser serializerAndParser) { + mContext = context; + mInterfaceName = interfaceName; + mMetaDataName = metaDataName; + mAttributesName = attributeName; + mSerializerAndParser = serializerAndParser; + + File dataDir = Environment.getDataDirectory(); + File systemDir = new File(dataDir, "system"); + File syncDir = new File(systemDir, "registered_services"); + mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml")); + + generateServicesMap(); + + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context1, Intent intent) { + generateServicesMap(); + } + }; + mReceiver = new AtomicReference(receiver); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + mContext.registerReceiver(receiver, intentFilter); + } + + public void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + Map> services; + synchronized (mServicesLock) { + services = mServices; + } + fout.println("RegisteredServicesCache: " + services.size() + " services"); + for (ServiceInfo info : services.values()) { + fout.println(" " + info); + } + } + + public RegisteredServicesCacheListener getListener() { + synchronized (this) { + return mListener; + } + } + + public void setListener(RegisteredServicesCacheListener listener, Handler handler) { + if (handler == null) { + handler = new Handler(mContext.getMainLooper()); + } + synchronized (this) { + mHandler = handler; + mListener = listener; + } + } + + private void notifyListener(final V type, final boolean removed) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "notifyListener: " + type + " is " + (removed ? "removed" : "added")); + } + RegisteredServicesCacheListener listener; + Handler handler; + synchronized (this) { + listener = mListener; + handler = mHandler; + } + if (listener == null) { + return; + } + + final RegisteredServicesCacheListener listener2 = listener; + handler.post(new Runnable() { + public void run() { + listener2.onServiceChanged(type, removed); + } + }); + } + + /** + * Value type that describes a Service. The information within can be used + * to bind to the service. + */ + public static class ServiceInfo { + public final V type; + public final ComponentName componentName; + public final int uid; + + private ServiceInfo(V type, ComponentName componentName, int uid) { + this.type = type; + this.componentName = componentName; + this.uid = uid; + } + + @Override + public String toString() { + return "ServiceInfo: " + type + ", " + componentName + ", uid " + uid; + } + } + + /** + * Accessor for the registered authenticators. + * @param type the account type of the authenticator + * @return the AuthenticatorInfo that matches the account type or null if none is present + */ + public ServiceInfo getServiceInfo(V type) { + synchronized (mServicesLock) { + return mServices.get(type); + } + } + + /** + * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all + * registered authenticators. + */ + public Collection> getAllServices() { + synchronized (mServicesLock) { + return Collections.unmodifiableCollection(mServices.values()); + } + } + + /** + * Stops the monitoring of package additions, removals and changes. + */ + public void close() { + final BroadcastReceiver receiver = mReceiver.getAndSet(null); + if (receiver != null) { + mContext.unregisterReceiver(receiver); + } + } + + @Override + protected void finalize() throws Throwable { + if (mReceiver.get() != null) { + Log.e(TAG, "RegisteredServicesCache finalized without being closed"); + } + close(); + super.finalize(); + } + + private boolean inSystemImage(int callerUid) { + String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); + for (String name : packages) { + try { + PackageInfo packageInfo = + mContext.getPackageManager().getPackageInfo(name, 0 /* flags */); + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + return false; + } + + void generateServicesMap() { + PackageManager pm = mContext.getPackageManager(); + ArrayList> serviceInfos = new ArrayList>(); + List resolveInfos = pm.queryIntentServices(new Intent(mInterfaceName), + PackageManager.GET_META_DATA); + for (ResolveInfo resolveInfo : resolveInfos) { + try { + ServiceInfo info = parseServiceInfo(resolveInfo); + if (info == null) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString()); + continue; + } + serviceInfos.add(info); + } catch (XmlPullParserException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); + } catch (IOException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); + } + } + + synchronized (mServicesLock) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "generateServicesMap: " + mInterfaceName); + } + if (mPersistentServices == null) { + readPersistentServicesLocked(); + } + mServices = Maps.newHashMap(); + boolean changed = false; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "found " + serviceInfos.size() + " services"); + } + for (ServiceInfo info : serviceInfos) { + // four cases: + // - doesn't exist yet + // - add, notify user that it was added + // - exists and the UID is the same + // - replace, don't notify user + // - exists, the UID is different, and the new one is not a system package + // - ignore + // - exists, the UID is different, and the new one is a system package + // - add, notify user that it was added + Integer previousUid = mPersistentServices.get(info.type); + if (previousUid == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered new type: " + info); + } + changed = true; + mServices.put(info.type, info); + mPersistentServices.put(info.type, info.uid); + if (!mPersistentServicesFileDidNotExist) { + notifyListener(info.type, false /* removed */); + } + } else if (previousUid == info.uid) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered existing type with the same uid: " + info); + } + mServices.put(info.type, info); + } else if (inSystemImage(info.uid) + || !containsTypeAndUid(serviceInfos, info.type, previousUid)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + if (inSystemImage(info.uid)) { + Log.d(TAG, "encountered existing type with a new uid but from" + + " the system: " + info); + } else { + Log.d(TAG, "encountered existing type with a new uid but existing was" + + " removed: " + info); + } + } + changed = true; + mServices.put(info.type, info); + mPersistentServices.put(info.type, info.uid); + notifyListener(info.type, false /* removed */); + } else { + // ignore + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered existing type with a new uid, ignoring: " + info); + } + } + } + + ArrayList toBeRemoved = Lists.newArrayList(); + for (V v1 : mPersistentServices.keySet()) { + if (!containsType(serviceInfos, v1)) { + toBeRemoved.add(v1); + } + } + for (V v1 : toBeRemoved) { + mPersistentServices.remove(v1); + changed = true; + notifyListener(v1, true /* removed */); + } + if (changed) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "writing updated list of persistent services"); + } + writePersistentServicesLocked(); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "persistent services did not change, so not writing anything"); + } + } + mPersistentServicesFileDidNotExist = false; + } + } + + private boolean containsType(ArrayList> serviceInfos, V type) { + for (int i = 0, N = serviceInfos.size(); i < N; i++) { + if (serviceInfos.get(i).type.equals(type)) { + return true; + } + } + + return false; + } + + private boolean containsTypeAndUid(ArrayList> serviceInfos, V type, int uid) { + for (int i = 0, N = serviceInfos.size(); i < N; i++) { + final ServiceInfo serviceInfo = serviceInfos.get(i); + if (serviceInfo.type.equals(type) && serviceInfo.uid == uid) { + return true; + } + } + + return false; + } + + private ServiceInfo parseServiceInfo(ResolveInfo service) + throws XmlPullParserException, IOException { + android.content.pm.ServiceInfo si = service.serviceInfo; + ComponentName componentName = new ComponentName(si.packageName, si.name); + + PackageManager pm = mContext.getPackageManager(); + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, mMetaDataName); + if (parser == null) { + throw new XmlPullParserException("No " + mMetaDataName + " meta-data"); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!mAttributesName.equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with " + mAttributesName + " tag"); + } + + V v = parseServiceAttributes(si.packageName, attrs); + if (v == null) { + return null; + } + final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo; + final ApplicationInfo applicationInfo = serviceInfo.applicationInfo; + final int uid = applicationInfo.uid; + return new ServiceInfo(v, componentName, uid); + } finally { + if (parser != null) parser.close(); + } + } + + /** + * Read all sync status back in to the initial engine state. + */ + private void readPersistentServicesLocked() { + mPersistentServices = Maps.newHashMap(); + if (mSerializerAndParser == null) { + return; + } + FileInputStream fis = null; + try { + mPersistentServicesFileDidNotExist = !mPersistentServicesFile.getBaseFile().exists(); + if (mPersistentServicesFileDidNotExist) { + return; + } + fis = mPersistentServicesFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG) { + eventType = parser.next(); + } + String tagName = parser.getName(); + if ("services".equals(tagName)) { + eventType = parser.next(); + do { + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) { + tagName = parser.getName(); + if ("service".equals(tagName)) { + V service = mSerializerAndParser.createFromXml(parser); + if (service == null) { + break; + } + String uidString = parser.getAttributeValue(null, "uid"); + int uid = Integer.parseInt(uidString); + mPersistentServices.put(service, uid); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + } + } catch (Exception e) { + Log.w(TAG, "Error reading persistent services, starting from scratch", e); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (java.io.IOException e1) { + } + } + } + } + + /** + * Write all sync status to the sync status file. + */ + private void writePersistentServicesLocked() { + if (mSerializerAndParser == null) { + return; + } + FileOutputStream fos = null; + try { + fos = mPersistentServicesFile.startWrite(); + 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, "services"); + for (Map.Entry service : mPersistentServices.entrySet()) { + out.startTag(null, "service"); + out.attribute(null, "uid", Integer.toString(service.getValue())); + mSerializerAndParser.writeAsXml(service.getKey(), out); + out.endTag(null, "service"); + } + out.endTag(null, "services"); + out.endDocument(); + mPersistentServicesFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing accounts", e1); + if (fos != null) { + mPersistentServicesFile.failWrite(fos); + } + } + } + + public abstract V parseServiceAttributes(String packageName, AttributeSet attrs); +} diff --git a/core/java/android/content/pm/RegisteredServicesCacheListener.java b/core/java/android/content/pm/RegisteredServicesCacheListener.java new file mode 100644 index 0000000000000000000000000000000000000000..2bc094276dce58f618cd6078bd7edce455372952 --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCacheListener.java @@ -0,0 +1,16 @@ +package android.content.pm; + +import android.os.Parcelable; + +/** + * Listener for changes to the set of registered services managed by a RegisteredServicesCache. + * @hide + */ +public interface RegisteredServicesCacheListener { + /** + * Invoked when a service is registered or changed. + * @param type the type of registered service + * @param removed true if the service was removed + */ + void onServiceChanged(V type, boolean removed); +} diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index ee49c02223865a15ab27ce7a2d81185f706dba4b..380db6512987535465918e60162ed4e3e2fa165f 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -91,6 +91,13 @@ public class ResolveInfo implements Parcelable { */ public int icon; + /** + * Optional -- if non-null, the {@link #labelRes} and {@link #icon} + * resources will be loaded from this package, rather than the one + * containing the resolved component. + */ + public String resolvePackageName; + /** * Retrieve the current textual label associated with this resolution. This * will call back on the given PackageManager to load the label from @@ -106,9 +113,15 @@ public class ResolveInfo implements Parcelable { if (nonLocalizedLabel != null) { return nonLocalizedLabel; } + CharSequence label; + if (resolvePackageName != null && labelRes != 0) { + label = pm.getText(resolvePackageName, labelRes, null); + if (label != null) { + return label; + } + } ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; ApplicationInfo ai = ci.applicationInfo; - CharSequence label; if (labelRes != 0) { label = pm.getText(ci.packageName, labelRes, ai); if (label != null) { @@ -133,6 +146,12 @@ public class ResolveInfo implements Parcelable { ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; ApplicationInfo ai = ci.applicationInfo; Drawable dr; + if (resolvePackageName != null && icon != 0) { + dr = pm.getDrawable(resolvePackageName, icon, null); + if (dr != null) { + return dr; + } + } if (icon != 0) { dr = pm.getDrawable(ci.packageName, icon, ai); if (dr != null) { @@ -160,24 +179,26 @@ public class ResolveInfo implements Parcelable { if (filter != null) { pw.println(prefix + "Filter:"); filter.dump(pw, prefix + " "); - } else { - pw.println(prefix + "Filter: null"); } pw.println(prefix + "priority=" + priority + " preferredOrder=" + preferredOrder + " match=0x" + Integer.toHexString(match) + " specificIndex=" + specificIndex + " isDefault=" + isDefault); - pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes) - + " nonLocalizedLabel=" + nonLocalizedLabel - + " icon=0x" + Integer.toHexString(icon)); + if (resolvePackageName != null) { + pw.println(prefix + "resolvePackageName=" + resolvePackageName); + } + if (labelRes != 0 || nonLocalizedLabel != null || icon != 0) { + pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes) + + " nonLocalizedLabel=" + nonLocalizedLabel + + " icon=0x" + Integer.toHexString(icon)); + } if (activityInfo != null) { pw.println(prefix + "ActivityInfo:"); activityInfo.dump(pw, prefix + " "); } else if (serviceInfo != null) { pw.println(prefix + "ServiceInfo:"); - // TODO - //serviceInfo.dump(pw, prefix + " "); + serviceInfo.dump(pw, prefix + " "); } } @@ -219,6 +240,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(labelRes); TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags); dest.writeInt(icon); + dest.writeString(resolvePackageName); } public static final Creator CREATOR @@ -257,6 +279,7 @@ public class ResolveInfo implements Parcelable { nonLocalizedLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); icon = source.readInt(); + resolvePackageName = source.readString(); } public static class DisplayNameComparator diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index b60650c01bfe01081ff09edcae35dd646f8dfa89..51d2a4decf8c109e24743c40a21ad464949ba356 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -2,6 +2,7 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; +import android.util.Printer; /** * Information you can retrieve about a particular application @@ -24,6 +25,11 @@ public class ServiceInfo extends ComponentInfo permission = orig.permission; } + public void dump(Printer pw, String prefix) { + super.dumpFront(pw, prefix); + pw.println(prefix + "permission=" + permission); + } + public String toString() { return "ServiceInfo{" + Integer.toHexString(System.identityHashCode(this)) diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java new file mode 100644 index 0000000000000000000000000000000000000000..33598f0b389051415b249c3f8230f95ee60c2221 --- /dev/null +++ b/core/java/android/content/pm/XmlSerializerAndParser.java @@ -0,0 +1,14 @@ +package android.content.pm; + +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import android.os.Parcel; + +import java.io.IOException; + +/** @hide */ +public interface XmlSerializerAndParser { + void writeAsXml(T item, XmlSerializer out) throws IOException; + T createFromXml(XmlPullParser parser) throws IOException, XmlPullParserException; +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 0bc8a9d2e4827f2f2371fe72dc7131024d124cfc..0d43b2a83c72fb2d1715f2d8096622317bcaf93a 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -24,7 +24,6 @@ import android.util.TypedValue; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Locale; /** * Provides access to an application's raw asset files; see {@link Resources} diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java index 50faf57a096f73595298053e55f0479ef8958fa6..11c67cc18110727e2989b293a784e9518ebcecdb 100644 --- a/core/java/android/content/res/CompatibilityInfo.java +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -274,6 +274,25 @@ public class CompatibilityInfo { * Apply translation to the canvas that is necessary to draw the content. */ public void translateCanvas(Canvas canvas) { + if (applicationScale == 1.5f) { + /* When we scale for compatibility, we can put our stretched + bitmaps and ninepatches on exacty 1/2 pixel boundaries, + which can give us inconsistent drawing due to imperfect + float precision in the graphics engine's inverse matrix. + + As a work-around, we translate by a tiny amount to avoid + landing on exact pixel centers and boundaries, giving us + the slop we need to draw consistently. + + This constant is meant to resolve to 1/255 after it is + scaled by 1.5 (applicationScale). Note, this is just a guess + as to what is small enough not to create its own artifacts, + and big enough to avoid the precision problems. Feel free + to experiment with smaller values as you choose. + */ + final float tinyOffset = 2.0f / (3 * 255); + canvas.translate(tinyOffset, tinyOffset); + } canvas.scale(applicationScale, applicationScale); } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 5f44cc9580673ce9f36b9e971e5455f418e2d0a6..1fe34b543f7f59a8668338305fb6a760c684b62d 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -93,7 +93,8 @@ public final class Configuration implements Parcelable, Comparable(); private static boolean mPreloaded; - private final LongSparseArray mPreloadedDrawables; - /*package*/ final TypedValue mTmpValue = new TypedValue(); // These are protected by the mTmpValue lock. @@ -157,11 +157,6 @@ public class Resources { } updateConfiguration(config, metrics); assets.ensureStringBlocks(); - if (mCompatibilityInfo.isScalingRequired()) { - mPreloadedDrawables = emptySparseArray(); - } else { - mPreloadedDrawables = sPreloadedDrawables; - } } /** @@ -1668,9 +1663,9 @@ public class Resources { return dr; } - Drawable.ConstantState cs = mPreloadedDrawables.get(key); + Drawable.ConstantState cs = sPreloadedDrawables.get(key); if (cs != null) { - dr = cs.newDrawable(); + dr = cs.newDrawable(this); } else { if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { @@ -1705,9 +1700,10 @@ public class Resources { } else { try { InputStream is = mAssets.openNonAsset( - value.assetCookie, file, AssetManager.ACCESS_BUFFER); + value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); - dr = Drawable.createFromResourceStream(this, value, is, file); + dr = Drawable.createFromResourceStream(this, value, is, + file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { @@ -1750,7 +1746,7 @@ public class Resources { //Log.i(TAG, "Returning cached drawable @ #" + // Integer.toHexString(((Integer)key).intValue()) // + " in " + this + ": " + entry); - return entry.newDrawable(); + return entry.newDrawable(this); } else { // our entry has been purged mDrawableCache.delete(key); @@ -1974,7 +1970,6 @@ public class Resources { mMetrics.setToDefaults(); updateConfiguration(null, null); mAssets.ensureStringBlocks(); - mPreloadedDrawables = sPreloadedDrawables; mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; } } diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index e684cb8018df9bc1df77d741394496c388e8c539..8fb82be3388004adb0b668f4e7672db0f63a0af3 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -202,7 +202,7 @@ final class StringBlock { sub = subtag(tag, ";size="); if (sub != null) { int size = Integer.parseInt(sub); - buffer.setSpan(new AbsoluteSizeSpan(size), + buffer.setSpan(new AbsoluteSizeSpan(size, true), style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -310,7 +310,7 @@ final class StringBlock { * the ascent if possible, or the descent if shrinking the ascent further * will make the text unreadable. */ - private static class Height implements LineHeightSpan { + private static class Height implements LineHeightSpan.WithDensity { private int mSize; private static float sProportion = 0; @@ -321,9 +321,21 @@ final class StringBlock { public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) { - if (fm.bottom - fm.top < mSize) { - fm.top = fm.bottom - mSize; - fm.ascent = fm.ascent - mSize; + // Should not get called, at least not by StaticLayout. + chooseHeight(text, start, end, spanstartv, v, fm, null); + } + + public void chooseHeight(CharSequence text, int start, int end, + int spanstartv, int v, + Paint.FontMetricsInt fm, TextPaint paint) { + int size = mSize; + if (paint != null) { + size *= paint.density; + } + + if (fm.bottom - fm.top < size) { + fm.top = fm.bottom - size; + fm.ascent = fm.ascent - size; } else { if (sProportion == 0) { /* @@ -343,27 +355,27 @@ final class StringBlock { int need = (int) Math.ceil(-fm.top * sProportion); - if (mSize - fm.descent >= need) { + if (size - fm.descent >= need) { /* * It is safe to shrink the ascent this much. */ - fm.top = fm.bottom - mSize; - fm.ascent = fm.descent - mSize; - } else if (mSize >= need) { + fm.top = fm.bottom - size; + fm.ascent = fm.descent - size; + } else if (size >= need) { /* * We can't show all the descent, but we can at least * show all the ascent. */ fm.top = fm.ascent = -need; - fm.bottom = fm.descent = fm.top + mSize; + fm.bottom = fm.descent = fm.top + size; } else { /* * Show as much of the ascent as we can, and no descent. */ - fm.top = fm.ascent = -mSize; + fm.top = fm.ascent = -size; fm.bottom = fm.descent = 0; } } diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index 4ac0aef81ea3825c1af14389d3ef374bde8fcd9a..27a02e260f52a0a7b01523c4e955f596529b4b52 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -166,6 +166,48 @@ public abstract class AbstractWindowedCursor extends AbstractCursor return mWindow.isBlob(mPos, columnIndex); } + public boolean isString(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object == null || object instanceof String; + } + } + + return mWindow.isString(mPos, columnIndex); + } + + public boolean isLong(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object != null && (object instanceof Integer || object instanceof Long); + } + } + + return mWindow.isLong(mPos, columnIndex); + } + + public boolean isFloat(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object != null && (object instanceof Float || object instanceof Double); + } + } + + return mWindow.isFloat(mPos, columnIndex); + } + @Override protected void checkPosition() { diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 8e2673036e063197cc7eeff621679826b3c8b1d0..99db81b561754d91290de7a7a34ac318a2a68d75 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -263,7 +263,58 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { } } + /** + * Checks if a field contains a long + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is a long + */ + public boolean isLong(int row, int col) { + acquireReference(); + try { + return isInteger_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + + /** + * Checks if a field contains a float. + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is a float + */ + public boolean isFloat(int row, int col) { + acquireReference(); + try { + return isFloat_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + + /** + * Checks if a field contains either a String or is null. + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is {@code NULL} or a String + */ + public boolean isString(int row, int col) { + acquireReference(); + try { + return isString_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + private native boolean isBlob_native(int row, int col); + private native boolean isString_native(int row, int col); + private native boolean isInteger_native(int row, int col); + private native boolean isFloat_native(int row, int col); /** * Returns a String for the given field. diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 10f3806d0ec9ddf81d62ecd7444102915ff0afbc..4ca660198cfec5d33b23b13ef17a15a7003706ca 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.binary.Hex; import android.content.ContentValues; import android.content.Context; +import android.content.OperationApplicationException; import android.database.sqlite.SQLiteAbortException; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; @@ -82,6 +83,8 @@ public class DatabaseUtils { code = 8; } else if (e instanceof SQLiteException) { code = 9; + } else if (e instanceof OperationApplicationException) { + code = 10; } else { reply.writeException(e); Log.e(TAG, "Writing exception to parcel", e); @@ -123,6 +126,18 @@ public class DatabaseUtils { } } + public static void readExceptionWithOperationApplicationExceptionFromParcel( + Parcel reply) throws OperationApplicationException { + int code = reply.readInt(); + if (code == 0) return; + String msg = reply.readString(); + if (code == 10) { + throw new OperationApplicationException(msg); + } else { + DatabaseUtils.readExceptionFromParcel(reply, msg, code); + } + } + private static final void readExceptionFromParcel(Parcel reply, String msg, int code) { switch (code) { case 2: @@ -211,7 +226,7 @@ public class DatabaseUtils { sb.append(sqlString); sb.append('\''); } - + /** * SQL-escape a string. */ @@ -240,7 +255,7 @@ public class DatabaseUtils { appendEscapedSQLString(sql, value.toString()); } } - + /** * Concatenates two SQL WHERE clauses, handling empty or null values. * @hide @@ -252,12 +267,12 @@ public class DatabaseUtils { if (TextUtils.isEmpty(b)) { return a; } - + return "(" + a + ") AND (" + b + ")"; } - + /** - * return the collation key + * return the collation key * @param name * @return the collation key */ @@ -269,7 +284,7 @@ public class DatabaseUtils { return ""; } } - + /** * return the collation key in hex format * @param name @@ -280,7 +295,7 @@ public class DatabaseUtils { char[] keys = Hex.encodeHex(arr); return new String(keys, 0, getKeyLen(arr) * 2); } - + private static int getKeyLen(byte[] arr) { if (arr[arr.length - 1] != 0) { return arr.length; @@ -289,16 +304,16 @@ public class DatabaseUtils { return arr.length-1; } } - + private static byte[] getCollationKeyInBytes(String name) { if (mColl == null) { mColl = Collator.getInstance(); mColl.setStrength(Collator.PRIMARY); } - return mColl.getCollationKey(name).toByteArray(); + return mColl.getCollationKey(name).toByteArray(); } - - private static Collator mColl = null; + + private static Collator mColl = null; /** * Prints the contents of a Cursor to System.out. The position is restored * after printing. @@ -591,10 +606,12 @@ public class DatabaseUtils { public static long queryNumEntries(SQLiteDatabase db, String table) { Cursor cursor = db.query(table, countProjection, null, null, null, null, null); - cursor.moveToFirst(); - long count = cursor.getLong(0); - cursor.deactivate(); - return count; + try { + cursor.moveToFirst(); + return cursor.getLong(0); + } finally { + cursor.close(); + } } /** diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 184d6dced8c7077703ee90d9c1ee3d15baa458b7..f621483f6df61cae033b044c672410cdf128bb56 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -22,10 +22,11 @@ import android.database.DatabaseUtils; import android.database.SQLException; import android.os.Debug; import android.os.SystemClock; +import android.os.SystemProperties; import android.text.TextUtils; import android.util.Config; -import android.util.Log; import android.util.EventLog; +import android.util.Log; import java.io.File; import java.util.HashMap; @@ -53,7 +54,8 @@ import java.util.concurrent.locks.ReentrantLock; */ public class SQLiteDatabase extends SQLiteClosable { private static final String TAG = "Database"; - private static final int DB_OPERATION_EVENT = 52000; + private static final int EVENT_DB_OPERATION = 52000; + private static final int EVENT_DB_CORRUPT = 75004; /** * Algorithms used in ON CONFLICT clause @@ -62,53 +64,53 @@ public class SQLiteDatabase extends SQLiteClosable { */ public enum ConflictAlgorithm { /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, - * thus ending the current transaction, and the command aborts with a - * return code of SQLITE_CONSTRAINT. If no transaction is active + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active * (other than the implied transaction that is created on every command) * then this algorithm works the same as ABORT. */ ROLLBACK("ROLLBACK"), - + /** - * When a constraint violation occurs,no ROLLBACK is executed - * so changes from prior commands within the same transaction + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction * are preserved. This is the default behavior. */ ABORT("ABORT"), - + /** - * When a constraint violation occurs, the command aborts with a return - * code SQLITE_CONSTRAINT. But any changes to the database that - * the command made prior to encountering the constraint violation + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation * are preserved and are not backed out. */ FAIL("FAIL"), - + /** - * When a constraint violation occurs, the one row that contains - * the constraint violation is not inserted or changed. - * But the command continues executing normally. Other rows before and - * after the row that contained the constraint violation continue to be + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be * inserted or updated normally. No error is returned. */ IGNORE("IGNORE"), - + /** * When a UNIQUE constraint violation occurs, the pre-existing rows that - * are causing the constraint violation are removed prior to inserting + * are causing the constraint violation are removed prior to inserting * or updating the current row. Thus the insert or update always occurs. - * The command continues executing normally. No error is returned. + * The command continues executing normally. No error is returned. * If a NOT NULL constraint violation occurs, the NULL value is replaced - * by the default value for that column. If the column has no default - * value, then the ABORT algorithm is used. If a CHECK constraint - * violation occurs then the IGNORE algorithm is used. When this conflict - * resolution strategy deletes rows in order to satisfy a constraint, + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, * it does not invoke delete triggers on those rows. * This behavior might change in a future release. */ REPLACE("REPLACE"); - + private final String mValue; ConflictAlgorithm(String value) { mValue = value; @@ -117,7 +119,7 @@ public class SQLiteDatabase extends SQLiteClosable { return mValue; } } - + /** * Maximum Length Of A LIKE Or GLOB Pattern * The pattern matching algorithm used in the default LIKE and GLOB implementation @@ -175,22 +177,29 @@ public class SQLiteDatabase extends SQLiteClosable { */ private boolean mTransactionIsSuccessful; + /** + * Valid during the life of a transaction. + */ + private SQLiteTransactionListener mTransactionListener; + /** Synchronize on this when accessing the database */ private final ReentrantLock mLock = new ReentrantLock(true); private long mLockAcquiredWallTime = 0L; private long mLockAcquiredThreadTime = 0L; - + // limit the frequency of complaints about each database to one within 20 sec - // unless run command adb shell setprop log.tag.Database VERBOSE + // unless run command adb shell setprop log.tag.Database VERBOSE private static final int LOCK_WARNING_WINDOW_IN_MS = 20000; /** If the lock is held this long then a warning will be printed when it is released. */ private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300; private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100; private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000; + private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; + private long mLastLockMessageTime = 0L; - + /** Used by native code, do not rename */ /* package */ int mNativeHandle = 0; @@ -205,15 +214,19 @@ public class SQLiteDatabase extends SQLiteClosable { /** The optional factory to use when creating new Cursors */ private CursorFactory mFactory; - + private WeakHashMap mPrograms; - + private final RuntimeException mLeakedException; // package visible, since callers will access directly to minimize overhead in the case // that logging is not enabled. /* package */ final boolean mLogStats; - + + // System property that enables logging of slow queries. Specify the threshold in ms. + private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; + private final int mSlowQueryThreshold; + /** * @param closable */ @@ -225,7 +238,7 @@ public class SQLiteDatabase extends SQLiteClosable { unlock(); } } - + void removeSQLiteClosable(SQLiteClosable closable) { lock(); try { @@ -233,8 +246,8 @@ public class SQLiteDatabase extends SQLiteClosable { } finally { unlock(); } - } - + } + @Override protected void onAllReferencesReleased() { if (isOpen()) { @@ -245,10 +258,10 @@ public class SQLiteDatabase extends SQLiteClosable { /** * Attempts to release memory that SQLite holds but does not require to * operate properly. Typically this memory will come from the page cache. - * + * * @return the number of bytes actually released */ - static public native int releaseMemory(); + static public native int releaseMemory(); /** * Control whether or not the SQLiteDatabase is made thread-safe by using locks @@ -284,7 +297,7 @@ public class SQLiteDatabase extends SQLiteClosable { * touch the native sqlite3* object since it is single threaded and uses * a polling lock contention algorithm. The lock is recursive, and may be acquired * multiple times by the same thread. This is a no-op if mLockingEnabled is false. - * + * * @see #unlock() */ /* package */ void lock() { @@ -320,7 +333,7 @@ public class SQLiteDatabase extends SQLiteClosable { /** * Releases the database lock. This is a no-op if mLockingEnabled is false. - * + * * @see #unlock() */ /* package */ void unlock() { @@ -350,7 +363,7 @@ public class SQLiteDatabase extends SQLiteClosable { private void checkLockHoldTime() { // Use elapsed real-time since the CPU may sleep when waiting for IO long elapsedTime = SystemClock.elapsedRealtime(); - long lockedTime = elapsedTime - mLockAcquiredWallTime; + long lockedTime = elapsedTime - mLockAcquiredWallTime; if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT && !Log.isLoggable(TAG, Log.VERBOSE) && (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) { @@ -392,6 +405,31 @@ public class SQLiteDatabase extends SQLiteClosable { * */ public void beginTransaction() { + beginTransactionWithListener(null /* transactionStatusCallback */); + } + + /** + * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of + * the work done in that transaction and all of the nested transactions will be committed or + * rolled back. The changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. + * + *

      Here is the standard idiom for transactions: + * + *

      +     *   db.beginTransactionWithListener(listener);
      +     *   try {
      +     *     ...
      +     *     db.setTransactionSuccessful();
      +     *   } finally {
      +     *     db.endTransaction();
      +     *   }
      +     * 
      + * @param transactionListener listener that should be notified when the transaction begins, + * commits, or is rolled back, either explicitly or by a call to + * {@link #yieldIfContendedSafely}. + */ + public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { lockForced(); boolean ok = false; try { @@ -411,8 +449,17 @@ public class SQLiteDatabase extends SQLiteClosable { // This thread didn't already have the lock, so begin a database // transaction now. execSQL("BEGIN EXCLUSIVE;"); + mTransactionListener = transactionListener; mTransactionIsSuccessful = true; mInnerTransactionIsSuccessful = false; + if (transactionListener != null) { + try { + transactionListener.onBegin(); + } catch (RuntimeException e) { + execSQL("ROLLBACK;"); + throw e; + } + } ok = true; } finally { if (!ok) { @@ -440,11 +487,27 @@ public class SQLiteDatabase extends SQLiteClosable { if (mLock.getHoldCount() != 1) { return; } + RuntimeException savedException = null; + if (mTransactionListener != null) { + try { + if (mTransactionIsSuccessful) { + mTransactionListener.onCommit(); + } else { + mTransactionListener.onRollback(); + } + } catch (RuntimeException e) { + savedException = e; + mTransactionIsSuccessful = false; + } + } if (mTransactionIsSuccessful) { execSQL("COMMIT;"); } else { try { execSQL("ROLLBACK;"); + if (savedException != null) { + throw savedException; + } } catch (SQLException e) { if (Config.LOGD) { Log.d(TAG, "exception during rollback, maybe the DB previously " @@ -453,6 +516,7 @@ public class SQLiteDatabase extends SQLiteClosable { } } } finally { + mTransactionListener = null; unlockForced(); if (Config.LOGV) { Log.v(TAG, "unlocked " + Thread.currentThread() @@ -517,8 +581,10 @@ public class SQLiteDatabase extends SQLiteClosable { * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock * will not be yielded. Use yieldIfContendedSafely instead. */ + @Deprecated public boolean yieldIfContended() { - return yieldIfContendedHelper(false /* do not check yielding */); + return yieldIfContendedHelper(false /* do not check yielding */, + -1 /* sleepAfterYieldDelay */); } /** @@ -526,14 +592,29 @@ public class SQLiteDatabase extends SQLiteClosable { * successful so far. Do not call setTransactionSuccessful before calling this. When this * returns a new transaction will have been created but not marked as successful. This assumes * that there are no nested transactions (beginTransaction has only been called once) and will - * through an exception if that is not the case. + * throw an exception if that is not the case. * @return true if the transaction was yielded */ public boolean yieldIfContendedSafely() { - return yieldIfContendedHelper(true /* check yielding */); + return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/); } - private boolean yieldIfContendedHelper(boolean checkFullyYielded) { + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * throw an exception if that is not the case. + * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if + * the lock was actually yielded. This will allow other background threads to make some + * more progress than they would if we started the transaction immediately. + * @return true if the transaction was yielded + */ + public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { + return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); + } + + private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) { if (mLock.getQueueLength() == 0) { // Reset the lock acquire time since we know that the thread was willing to yield // the lock at this time. @@ -542,6 +623,7 @@ public class SQLiteDatabase extends SQLiteClosable { return false; } setTransactionSuccessful(); + SQLiteTransactionListener transactionListener = mTransactionListener; endTransaction(); if (checkFullyYielded) { if (this.isDbLockedByCurrentThread()) { @@ -549,7 +631,25 @@ public class SQLiteDatabase extends SQLiteClosable { "Db locked more than once. yielfIfContended cannot yield"); } } - beginTransaction(); + if (sleepAfterYieldDelay > 0) { + // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to + // check if anyone is using the database. If the database is not contended, + // retake the lock and return. + long remainingDelay = sleepAfterYieldDelay; + while (remainingDelay > 0) { + try { + Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? + remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); + } catch (InterruptedException e) { + Thread.interrupted(); + } + remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; + if (mLock.getQueueLength() == 0) { + break; + } + } + } + beginTransactionWithListener(transactionListener); return true; } @@ -640,6 +740,7 @@ public class SQLiteDatabase extends SQLiteClosable { // Try to recover from this, if we can. // TODO: should we do this for other open failures? Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); + EventLog.writeEvent(EVENT_DB_CORRUPT, path); new File(path).delete(); return new SQLiteDatabase(path, factory, flags); } @@ -696,9 +797,9 @@ public class SQLiteDatabase extends SQLiteClosable { if (program != null) { program.onAllReferencesReleasedFromContainer(); } - } + } } - + /** * Native call to close the database. */ @@ -1108,33 +1209,44 @@ public class SQLiteDatabase extends SQLiteClosable { String editTable) { long timeStart = 0; - if (Config.LOGV) { + if (Config.LOGV || mSlowQueryThreshold != -1) { timeStart = System.currentTimeMillis(); } SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable); + Cursor cursor = null; try { - return driver.query( + cursor = driver.query( cursorFactory != null ? cursorFactory : mFactory, selectionArgs); } finally { - if (Config.LOGV) { + if (Config.LOGV || mSlowQueryThreshold != -1) { + + // Force query execution + if (cursor != null) { + cursor.moveToFirst(); + cursor.moveToPosition(-1); + } + long duration = System.currentTimeMillis() - timeStart; - Log.v(SQLiteCursor.TAG, - "query (" + duration + " ms): " + driver.toString() + ", args are " - + (selectionArgs != null - ? TextUtils.join(",", selectionArgs) - : "")); + if (Config.LOGV || duration >= mSlowQueryThreshold) { + Log.v(SQLiteCursor.TAG, + "query (" + duration + " ms): " + driver.toString() + ", args are " + + (selectionArgs != null + ? TextUtils.join(",", selectionArgs) + : "")); + } } } + return cursor; } /** * Runs the provided SQL and returns a cursor over the result set. - * The cursor will read an initial set of rows and the return to the caller. - * It will continue to read in batches and send data changed notifications + * The cursor will read an initial set of rows and the return to the caller. + * It will continue to read in batches and send data changed notifications * when the later batches are ready. * @param sql the SQL query. The SQL string must not be ; terminated * @param selectionArgs You may include ?s in where clause in the query, @@ -1143,19 +1255,19 @@ public class SQLiteDatabase extends SQLiteClosable { * @param initialRead set the initial count of items to read from the cursor * @param maxRead set the count of items to read on each iteration after the first * @return A {@link Cursor} object, which is positioned before the first entry - * + * * This work is incomplete and not fully tested or reviewed, so currently * hidden. * @hide */ - public Cursor rawQuery(String sql, String[] selectionArgs, + public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) { SQLiteCursor c = (SQLiteCursor)rawQueryWithFactory( null, sql, selectionArgs, null); c.setLoadStyle(initialRead, maxRead); return c; } - + /** * Convenience method for inserting a row into the database. * @@ -1208,7 +1320,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long replace(String table, String nullColumnHack, ContentValues initialValues) { try { - return insertWithOnConflict(table, nullColumnHack, initialValues, + return insertWithOnConflict(table, nullColumnHack, initialValues, ConflictAlgorithm.REPLACE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + initialValues, e); @@ -1230,7 +1342,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException { - return insertWithOnConflict(table, nullColumnHack, initialValues, + return insertWithOnConflict(table, nullColumnHack, initialValues, ConflictAlgorithm.REPLACE); } @@ -1386,7 +1498,7 @@ public class SQLiteDatabase extends SQLiteClosable { public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { return updateWithOnConflict(table, values, whereClause, whereArgs, null); } - + /** * Convenience method for updating rows in the database. * @@ -1399,7 +1511,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the number of rows affected * @hide */ - public int updateWithOnConflict(String table, ContentValues values, + public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, ConflictAlgorithm algorithm) { if (!isOpen()) { throw new IllegalStateException("database not open"); @@ -1416,7 +1528,7 @@ public class SQLiteDatabase extends SQLiteClosable { sql.append(algorithm.value()); sql.append(" "); } - + sql.append(table); sql.append(" SET "); @@ -1577,7 +1689,8 @@ public class SQLiteDatabase extends SQLiteClosable { mFlags = flags; mPath = path; mLogStats = "1".equals(android.os.SystemProperties.get("db.logstats")); - + mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); + mLeakedException = new IllegalStateException(path + " SQLiteDatabase created and never closed"); mFactory = factory; @@ -1621,7 +1734,7 @@ public class SQLiteDatabase extends SQLiteClosable { } /* package */ void logTimeStat(boolean read, long begin, long end) { - EventLog.writeEvent(DB_OPERATION_EVENT, mPath, read ? 0 : 1, end - begin); + EventLog.writeEvent(EVENT_DB_OPERATION, mPath, read ? 0 : 1, end - begin); } /** diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index d04afb01c3e342e3897d612d6e59f5186b5e3f99..84d88793747332ea66dc0bcf59020e652eac71e5 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -17,6 +17,7 @@ package android.database.sqlite; import android.util.Config; +import android.util.Log; /** * Provides debugging info about all SQLite databases running in the current process. @@ -27,23 +28,27 @@ public final class SQLiteDebug { /** * Controls the printing of SQL statements as they are executed. */ - public static final boolean DEBUG_SQL_STATEMENTS = Config.LOGV; + public static final boolean DEBUG_SQL_STATEMENTS = + Log.isLoggable("SQLiteStatements", Log.VERBOSE); /** * Controls the stack trace reporting of active cursors being * finalized. */ - public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = Config.LOGV; + public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = + Log.isLoggable("SQLiteCursorClosing", Log.VERBOSE); /** * Controls the tracking of time spent holding the database lock. */ - public static final boolean DEBUG_LOCK_TIME_TRACKING = false; + public static final boolean DEBUG_LOCK_TIME_TRACKING = + Log.isLoggable("SQLiteLockTime", Log.VERBOSE); /** * Controls the printing of stack traces when tracking the time spent holding the database lock. */ - public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE = false; + public static final boolean DEBUG_LOCK_TIME_TRACKING_STACK_TRACE = + Log.isLoggable("SQLiteLockStackTrace", Log.VERBOSE); /** * Contains statistics about the active pagers in the current process. diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 8a639196ee7a414ad93af7571cf8b6f0747ee5b1..af54a71169ab04fea25c93108e8dbbb5e1a5095b 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -355,23 +355,26 @@ public class SQLiteQueryBuilder String groupBy, String having, String sortOrder, String limit) { String[] projection = computeProjection(projectionIn); + StringBuilder where = new StringBuilder(); + if (mWhereClause.length() > 0) { - mWhereClause.append(')'); + where.append(mWhereClause.toString()); + where.append(')'); } // Tack on the user's selection, if present. if (selection != null && selection.length() > 0) { if (mWhereClause.length() > 0) { - mWhereClause.append(" AND "); + where.append(" AND "); } - mWhereClause.append('('); - mWhereClause.append(selection); - mWhereClause.append(')'); + where.append('('); + where.append(selection); + where.append(')'); } return buildQueryString( - mDistinct, mTables, projection, mWhereClause.toString(), + mDistinct, mTables, projection, where.toString(), groupBy, having, sortOrder, limit); } diff --git a/core/java/android/database/sqlite/SQLiteTransactionListener.java b/core/java/android/database/sqlite/SQLiteTransactionListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e97ece8f7faccccfab080d2179e3040bbaa03f85 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteTransactionListener.java @@ -0,0 +1,21 @@ +package android.database.sqlite; + +/** + * A listener for transaction events. + */ +public interface SQLiteTransactionListener { + /** + * Called immediately after the transaction begins. + */ + void onBegin(); + + /** + * Called immediately before commiting the transaction. + */ + void onCommit(); + + /** + * Called if the transaction is about to be rolled back. + */ + void onRollback(); +} diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java index 5bfdcc4a5578c6abe4f409838aa937578ddc3a0c..30ecf5a81a4edd8a25782a606d55c6862745aa15 100755 --- a/core/java/android/gesture/GestureOverlayView.java +++ b/core/java/android/gesture/GestureOverlayView.java @@ -29,6 +29,7 @@ import android.view.animation.AnimationUtils; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; import android.os.SystemClock; +import android.annotation.Widget; import com.android.internal.R; import java.util.ArrayList; @@ -50,6 +51,7 @@ import java.util.ArrayList; * @attr ref android.R.styleable#GestureOverlayView_orientation * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor */ +@Widget public class GestureOverlayView extends FrameLayout { public static final int GESTURE_STROKE_TYPE_SINGLE = 0; public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1; diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 091bc1700988e5e66517622c139bdd4f6cd4a9b3..4b733ef334dbda1511ca8a10f7424207384e7d9a 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -17,7 +17,9 @@ package android.hardware; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.StringTokenizer; import java.io.IOException; @@ -35,20 +37,38 @@ import android.os.Message; * frames for encoding for video. *

      There is no default constructor for this class. Use {@link #open()} to * get a Camera object.

      + * + *

      In order to use the device camera, you must declare the + * {@link android.Manifest.permission#CAMERA} permission in your Android + * Manifest. Also be sure to include the + * <uses-feature> + * manifest element in order to declare camera features used by your application. + * For example, if you use the camera and auto-focus feature, your Manifest + * should include the following:

      + *
       <uses-permission android:name="android.permission.CAMERA" />
      + * <uses-feature android:name="android.hardware.camera" />
      + * <uses-feature android:name="android.hardware.camera.autofocus" />
      + * + *

      Caution: Different Android-powered devices + * may have different hardware specifications, such as megapixel ratings and + * auto-focus capabilities. In order for your application to be compatible with + * more devices, you should not make assumptions about the device camera + * specifications.

      */ public class Camera { private static final String TAG = "Camera"; - + // These match the enums in frameworks/base/include/ui/Camera.h - private static final int CAMERA_MSG_ERROR = 0; - private static final int CAMERA_MSG_SHUTTER = 1; - private static final int CAMERA_MSG_FOCUS = 2; - private static final int CAMERA_MSG_ZOOM = 3; - private static final int CAMERA_MSG_PREVIEW_FRAME = 4; - private static final int CAMERA_MSG_VIDEO_FRAME = 5; - private static final int CAMERA_MSG_POSTVIEW_FRAME = 6; - private static final int CAMERA_MSG_RAW_IMAGE = 7; - private static final int CAMERA_MSG_COMPRESSED_IMAGE = 8; + private static final int CAMERA_MSG_ERROR = 0x001; + private static final int CAMERA_MSG_SHUTTER = 0x002; + private static final int CAMERA_MSG_FOCUS = 0x004; + private static final int CAMERA_MSG_ZOOM = 0x008; + private static final int CAMERA_MSG_PREVIEW_FRAME = 0x010; + private static final int CAMERA_MSG_VIDEO_FRAME = 0x020; + private static final int CAMERA_MSG_POSTVIEW_FRAME = 0x040; + private static final int CAMERA_MSG_RAW_IMAGE = 0x080; + private static final int CAMERA_MSG_COMPRESSED_IMAGE = 0x100; + private static final int CAMERA_MSG_ALL_MSGS = 0x1FF; private int mNativeContext; // accessed by native methods private EventHandler mEventHandler; @@ -56,15 +76,18 @@ public class Camera { private PictureCallback mRawImageCallback; private PictureCallback mJpegCallback; private PreviewCallback mPreviewCallback; + private PictureCallback mPostviewCallback; private AutoFocusCallback mAutoFocusCallback; + private ZoomCallback mZoomCallback; private ErrorCallback mErrorCallback; private boolean mOneShot; - + private boolean mWithBuffer; + /** * Returns a new Camera object. */ - public static Camera open() { - return new Camera(); + public static Camera open() { + return new Camera(); } Camera() { @@ -72,6 +95,8 @@ public class Camera { mRawImageCallback = null; mJpegCallback = null; mPreviewCallback = null; + mPostviewCallback = null; + mZoomCallback = null; Looper looper; if ((looper = Looper.myLooper()) != null) { @@ -84,40 +109,39 @@ public class Camera { native_setup(new WeakReference(this)); } - - protected void finalize() { - native_release(); + + protected void finalize() { + native_release(); } - + private native final void native_setup(Object camera_this); private native final void native_release(); - + /** * Disconnects and releases the Camera object resources. - *

      It is recommended that you call this as soon as you're done with the + *

      It is recommended that you call this as soon as you're done with the * Camera object.

      */ - public final void release() { + public final void release() { native_release(); } /** * Reconnect to the camera after passing it to MediaRecorder. To save * setup/teardown time, a client of Camera can pass an initialized Camera - * object to a MediaRecorder to use for video recording. Once the + * object to a MediaRecorder to use for video recording. Once the * MediaRecorder is done with the Camera, this method can be used to * re-establish a connection with the camera hardware. NOTE: The Camera * object must first be unlocked by the process that owns it before it - * can be connected to another proces. + * can be connected to another process. * * @throws IOException if the method fails. * - * FIXME: Unhide after approval * @hide */ public native final void reconnect() throws IOException; - + /** * Lock the camera to prevent other processes from accessing it. To save * setup/teardown time, a client of Camera can pass an initialized Camera @@ -126,31 +150,26 @@ public class Camera { * Camera object is locked. Locking it again from the same process will * have no effect. Attempting to lock it from another process if it has * not been unlocked will fail. - * Returns 0 if lock was successful. * - * FIXME: Unhide after approval - * @hide + * @throws RuntimeException if the method fails. */ - public native final int lock(); - + public native final void lock(); + /** - * Unlock the camera to allow aother process to access it. To save + * Unlock the camera to allow another process to access it. To save * setup/teardown time, a client of Camera can pass an initialized Camera * object to another process. This method is used to unlock the Camera * object before handing off the Camera object to the other process. - - * Returns 0 if unlock was successful. * - * FIXME: Unhide after approval - * @hide + * @throws RuntimeException if the method fails. */ - public native final int unlock(); - + public native final void unlock(); + /** * Sets the SurfaceHolder to be used for a picture preview. If the surface * changed since the last call, the screen will blank. Nothing happens * if the same surface is re-set. - * + * * @param holder the SurfaceHolder upon which to place the picture preview * @throws IOException if the method fails. */ @@ -172,23 +191,27 @@ public class Camera { /** * The callback that delivers the preview frames. * - * @param data The contents of the preview frame in getPreviewFormat() - * format. + * @param data The contents of the preview frame in the format defined + * by {@link android.graphics.PixelFormat}, which can be queried + * with {@link android.hardware.Camera.Parameters#getPreviewFormat()}. + * If {@link android.hardware.Camera.Parameters#setPreviewFormat(int)} + * is never called, the default will be the YCbCr_420_SP + * (NV21) format. * @param camera The Camera service object. */ void onPreviewFrame(byte[] data, Camera camera); }; - + /** * Start drawing preview frames to the surface. */ public native final void startPreview(); - + /** * Stop drawing preview frames to the surface. */ public native final void stopPreview(); - + /** * Return current preview state. * @@ -196,7 +219,7 @@ public class Camera { * @hide */ public native final boolean previewEnabled(); - + /** * Can be called at any time to instruct the camera to use a callback for * each preview frame in addition to displaying it. @@ -207,6 +230,9 @@ public class Camera { public final void setPreviewCallback(PreviewCallback cb) { mPreviewCallback = cb; mOneShot = false; + mWithBuffer = false; + // Always use one-shot mode. We fake camera preview mode by + // doing one-shot preview continuously. setHasPreviewCallback(cb != null, false); } @@ -217,14 +243,48 @@ public class Camera { * @param cb A callback object that receives a copy of the preview frame. */ public final void setOneShotPreviewCallback(PreviewCallback cb) { - if (cb != null) { - mPreviewCallback = cb; - mOneShot = true; - setHasPreviewCallback(true, true); - } + mPreviewCallback = cb; + mOneShot = true; + mWithBuffer = false; + setHasPreviewCallback(cb != null, false); + } + + private native final void setHasPreviewCallback(boolean installed, boolean manualBuffer); + + /** + * Installs a callback which will get called as long as there are buffers in the + * preview buffer queue, which minimizes dynamic allocation of preview buffers. + * + * Apps must call addCallbackBuffer to explicitly register the buffers to use, or no callbacks + * will be received. addCallbackBuffer may be safely called before or after + * a call to setPreviewCallbackWithBuffer with a non-null callback parameter. + * + * The buffer queue will be cleared upon any calls to setOneShotPreviewCallback, + * setPreviewCallback, or to this method with a null callback parameter. + * + * @param cb A callback object that receives a copy of the preview frame. A null value will clear the queue. + * @hide + */ + public final void setPreviewCallbackWithBuffer(PreviewCallback cb) { + mPreviewCallback = cb; + mOneShot = false; + mWithBuffer = true; + setHasPreviewCallback(cb != null, true); } - private native final void setHasPreviewCallback(boolean installed, boolean oneshot); + /** + * Adds a pre-allocated buffer to the callback buffer queue. + * Preview width and height can be determined from getPreviewSize, and bitsPerPixel can be + * found from from {@link android.hardware.Camera.Parameters#getPreviewFormat()} and + * {@link android.graphics.PixelFormat#getPixelFormatInfo(int, PixelFormat)} + * + * Alternatively, a buffer from a previous callback may be passed in or used + * to determine the size of new preview frame buffers. + * + * @param callbackBuffer The buffer to register. Size should be width * height * bitsPerPixel / 8. + * @hide + */ + public native final void addCallbackBuffer(byte[] callbackBuffer); private class EventHandler extends Handler { @@ -245,33 +305,58 @@ public class Camera { return; case CAMERA_MSG_RAW_IMAGE: - if (mRawImageCallback != null) + if (mRawImageCallback != null) { mRawImageCallback.onPictureTaken((byte[])msg.obj, mCamera); + } return; case CAMERA_MSG_COMPRESSED_IMAGE: - if (mJpegCallback != null) + if (mJpegCallback != null) { mJpegCallback.onPictureTaken((byte[])msg.obj, mCamera); + } return; - + case CAMERA_MSG_PREVIEW_FRAME: if (mPreviewCallback != null) { - mPreviewCallback.onPreviewFrame((byte[])msg.obj, mCamera); + PreviewCallback cb = mPreviewCallback; if (mOneShot) { + // Clear the callback variable before the callback + // in case the app calls setPreviewCallback from + // the callback function mPreviewCallback = null; + } else if (!mWithBuffer) { + // We're faking the camera preview mode to prevent + // the app from being flooded with preview frames. + // Set to oneshot mode again. + setHasPreviewCallback(true, false); } + cb.onPreviewFrame((byte[])msg.obj, mCamera); + } + return; + + case CAMERA_MSG_POSTVIEW_FRAME: + if (mPostviewCallback != null) { + mPostviewCallback.onPictureTaken((byte[])msg.obj, mCamera); } return; case CAMERA_MSG_FOCUS: - if (mAutoFocusCallback != null) + if (mAutoFocusCallback != null) { mAutoFocusCallback.onAutoFocus(msg.arg1 == 0 ? false : true, mCamera); + } + return; + + case CAMERA_MSG_ZOOM: + if (mZoomCallback != null) { + mZoomCallback.onZoomUpdate(msg.arg1, msg.arg2 != 0, mCamera); + } return; case CAMERA_MSG_ERROR : Log.e(TAG, "Error " + msg.arg1); - if (mErrorCallback != null) + if (mErrorCallback != null) { mErrorCallback.onError(msg.arg1, mCamera); + } return; default: @@ -296,12 +381,21 @@ public class Camera { /** * Handles the callback for the camera auto focus. + *

      Devices that do not support auto-focus will receive a "fake" + * callback to this interface. If your application needs auto-focus and + * should not be installed on devices without auto-focus, you must + * declare that your app uses the + * {@code android.hardware.camera.autofocus} feature, in the + * <uses-feature> + * manifest element.

      */ public interface AutoFocusCallback { /** - * Callback for the camera auto focus. - * + * Callback for the camera auto focus. If the camera does not support + * auto-focus and autoFocus is called, onAutoFocus will be called + * immediately with success. + * * @param success true if focus was successful, false if otherwise * @param camera the Camera service object */ @@ -309,10 +403,22 @@ public class Camera { }; /** - * Starts auto-focus function and registers a callback function to - * run when camera is focused. Only valid after startPreview() has - * been called. - * + * Starts auto-focus function and registers a callback function to run when + * camera is focused. Only valid after startPreview() has been called. + * Applications should call {@link + * android.hardware.Camera.Parameters#getFocusMode()} to determine if this + * method should be called. If the camera does not support auto-focus, it is + * a no-op and {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} + * callback will be called immediately. + *

      If your application should not be installed + * on devices without auto-focus, you must declare that your application + * uses auto-focus with the + * <uses-feature> + * manifest element.

      + *

      If the current flash mode is not + * {@link android.hardware.Camera.Parameters#FLASH_MODE_OFF}, flash may be + * fired during auto-focus depending on the driver.

      + * * @param cb the callback to run */ public final void autoFocus(AutoFocusCallback cb) @@ -322,6 +428,19 @@ public class Camera { } private native final void native_autoFocus(); + /** + * Cancels auto-focus function. If the auto-focus is still in progress, + * this function will cancel it. Whether the auto-focus is in progress + * or not, this function will return the focus position to the default. + * If the camera does not support auto-focus, this is a no-op. + */ + public final void cancelAutoFocus() + { + mAutoFocusCallback = null; + native_cancelAutoFocus(); + } + private native final void native_cancelAutoFocus(); + /** * An interface which contains a callback for the shutter closing after taking a picture. */ @@ -340,7 +459,7 @@ public class Camera { public interface PictureCallback { /** * Callback for when a picture is taken. - * + * * @param data a byte array of the picture data * @param camera the Camera service object */ @@ -348,28 +467,104 @@ public class Camera { }; /** - * Triggers an asynchronous image capture. The camera service - * will initiate a series of callbacks to the application as the - * image capture progresses. The shutter callback occurs after - * the image is captured. This can be used to trigger a sound - * to let the user know that image has been captured. The raw - * callback occurs when the raw image data is available. The jpeg - * callback occurs when the compressed image is available. If the - * application does not need a particular callback, a null can be - * passed instead of a callback method. - * + * Triggers an asynchronous image capture. The camera service will initiate + * a series of callbacks to the application as the image capture progresses. + * The shutter callback occurs after the image is captured. This can be used + * to trigger a sound to let the user know that image has been captured. The + * raw callback occurs when the raw image data is available (NOTE: the data + * may be null if the hardware does not have enough memory to make a copy). + * The jpeg callback occurs when the compressed image is available. If the + * application does not need a particular callback, a null can be passed + * instead of a callback method. + * * @param shutter callback after the image is captured, may be null * @param raw callback with raw image data, may be null * @param jpeg callback with jpeg image data, may be null */ public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) { + takePicture(shutter, raw, null, jpeg); + } + private native final void native_takePicture(); + + /** + * Triggers an asynchronous image capture. The camera service will initiate + * a series of callbacks to the application as the image capture progresses. + * The shutter callback occurs after the image is captured. This can be used + * to trigger a sound to let the user know that image has been captured. The + * raw callback occurs when the raw image data is available (NOTE: the data + * may be null if the hardware does not have enough memory to make a copy). + * The postview callback occurs when a scaled, fully processed postview + * image is available (NOTE: not all hardware supports this). The jpeg + * callback occurs when the compressed image is available. If the + * application does not need a particular callback, a null can be passed + * instead of a callback method. + * + * @param shutter callback after the image is captured, may be null + * @param raw callback with raw image data, may be null + * @param postview callback with postview image data, may be null + * @param jpeg callback with jpeg image data, may be null + */ + public final void takePicture(ShutterCallback shutter, PictureCallback raw, + PictureCallback postview, PictureCallback jpeg) { mShutterCallback = shutter; mRawImageCallback = raw; + mPostviewCallback = postview; mJpegCallback = jpeg; native_takePicture(); } - private native final void native_takePicture(); + + /** + * Zooms to the requested value smoothly. Driver will generate {@link + * #ZoomCallback} for the current zoom value and whether zoom is stopped. + * The applications can call {@link #stopSmoothZoom} to stop the zoom + * earlier. The applications should not call startSmoothZoom again or {@link + * android.hardware.Camera.Parameters#setZoom(int)} before the zoom stops. + * + * @param value zoom value. The valid range is 0 to {@link + * android.hardware.Camera.Parameters#getMaxZoom}. + * @hide + */ + public native final void startSmoothZoom(int value); + + /** + * Stops the smooth zoom. The applications should wait for the {@link + * #ZoomCallback} to know when the zoom is actually stopped. + * @hide + */ + public native final void stopSmoothZoom(); + + /** + * Handles the zoom callback. + * + * @hide + */ + public interface ZoomCallback + { + /** + * Callback for zoom updates + * + * @param zoomValue the current zoom value. In smooth zoom mode, camera + * generates this callback for every new zoom value. + * @param stopped whether smooth zoom is stopped. If the value is true, + * this is the last zoom update for the application. + * + * @param camera the Camera service object + * @see android.hardware.Camera.Parameters#startSmoothZoom + */ + void onZoomUpdate(int zoomValue, boolean stopped, Camera camera); + }; + + /** + * Registers a callback to be invoked when the zoom value is updated by the + * camera driver during smooth zoom. + * @param cb the callback to run + * @hide + */ + public final void setZoomCallback(ZoomCallback cb) + { + mZoomCallback = cb; + } // These match the enum in include/ui/Camera.h /** Unspecified camerar error. @see #ErrorCallback */ @@ -377,7 +572,7 @@ public class Camera { /** Media server died. In this case, the application must release the * Camera object and instantiate a new one. @see #ErrorCallback */ public static final int CAMERA_ERROR_SERVER_DIED = 100; - + /** * Handles the camera error callback. */ @@ -403,13 +598,13 @@ public class Camera { { mErrorCallback = cb; } - + private native final void native_setParameters(String params); private native final String native_getParameters(); /** * Sets the Parameters for pictures from this Camera service. - * + * * @param params the Parameters to use for this Camera service */ public void setParameters(Parameters params) { @@ -432,7 +627,7 @@ public class Camera { public class Size { /** * Sets the dimensions for pictures. - * + * * @param w the photo width (pixels) * @param h the photo height (pixels) */ @@ -448,8 +643,144 @@ public class Camera { /** * Handles the parameters for pictures created by a Camera service. + * + *

      To make camera parameters take effect, applications have to call + * Camera.setParameters. For example, after setWhiteBalance is called, white + * balance is not changed until Camera.setParameters() is called. + * + *

      Different devices may have different camera capabilities, such as + * picture size or flash modes. The application should query the camera + * capabilities before setting parameters. For example, the application + * should call getSupportedColorEffects before calling setEffect. If the + * camera does not support color effects, getSupportedColorEffects will + * return null. */ public class Parameters { + // Parameter keys to communicate with the camera driver. + private static final String KEY_PREVIEW_SIZE = "preview-size"; + private static final String KEY_PREVIEW_FORMAT = "preview-format"; + private static final String KEY_PREVIEW_FRAME_RATE = "preview-frame-rate"; + private static final String KEY_PICTURE_SIZE = "picture-size"; + private static final String KEY_PICTURE_FORMAT = "picture-format"; + private static final String KEY_JPEG_THUMBNAIL_WIDTH = "jpeg-thumbnail-width"; + private static final String KEY_JPEG_THUMBNAIL_HEIGHT = "jpeg-thumbnail-height"; + private static final String KEY_JPEG_THUMBNAIL_QUALITY = "jpeg-thumbnail-quality"; + private static final String KEY_JPEG_QUALITY = "jpeg-quality"; + private static final String KEY_ROTATION = "rotation"; + private static final String KEY_GPS_LATITUDE = "gps-latitude"; + private static final String KEY_GPS_LONGITUDE = "gps-longitude"; + private static final String KEY_GPS_ALTITUDE = "gps-altitude"; + private static final String KEY_GPS_TIMESTAMP = "gps-timestamp"; + private static final String KEY_WHITE_BALANCE = "whitebalance"; + private static final String KEY_EFFECT = "effect"; + private static final String KEY_ANTIBANDING = "antibanding"; + private static final String KEY_SCENE_MODE = "scene-mode"; + private static final String KEY_FLASH_MODE = "flash-mode"; + private static final String KEY_FOCUS_MODE = "focus-mode"; + // Parameter key suffix for supported values. + private static final String SUPPORTED_VALUES_SUFFIX = "-values"; + + // Values for white balance settings. + public static final String WHITE_BALANCE_AUTO = "auto"; + public static final String WHITE_BALANCE_INCANDESCENT = "incandescent"; + public static final String WHITE_BALANCE_FLUORESCENT = "fluorescent"; + public static final String WHITE_BALANCE_WARM_FLUORESCENT = "warm-fluorescent"; + public static final String WHITE_BALANCE_DAYLIGHT = "daylight"; + public static final String WHITE_BALANCE_CLOUDY_DAYLIGHT = "cloudy-daylight"; + public static final String WHITE_BALANCE_TWILIGHT = "twilight"; + public static final String WHITE_BALANCE_SHADE = "shade"; + + // Values for color effect settings. + public static final String EFFECT_NONE = "none"; + public static final String EFFECT_MONO = "mono"; + public static final String EFFECT_NEGATIVE = "negative"; + public static final String EFFECT_SOLARIZE = "solarize"; + public static final String EFFECT_SEPIA = "sepia"; + public static final String EFFECT_POSTERIZE = "posterize"; + public static final String EFFECT_WHITEBOARD = "whiteboard"; + public static final String EFFECT_BLACKBOARD = "blackboard"; + public static final String EFFECT_AQUA = "aqua"; + + // Values for antibanding settings. + public static final String ANTIBANDING_AUTO = "auto"; + public static final String ANTIBANDING_50HZ = "50hz"; + public static final String ANTIBANDING_60HZ = "60hz"; + public static final String ANTIBANDING_OFF = "off"; + + // Values for flash mode settings. + /** + * Flash will not be fired. + */ + public static final String FLASH_MODE_OFF = "off"; + + /** + * Flash will be fired automatically when required. The flash may be fired + * during preview, auto-focus, or snapshot depending on the driver. + */ + public static final String FLASH_MODE_AUTO = "auto"; + + /** + * Flash will always be fired during snapshot. The flash may also be + * fired during preview or auto-focus depending on the driver. + */ + public static final String FLASH_MODE_ON = "on"; + + /** + * Flash will be fired in red-eye reduction mode. + */ + public static final String FLASH_MODE_RED_EYE = "red-eye"; + + /** + * Constant emission of light during preview, auto-focus and snapshot. + * This can also be used for video recording. + */ + public static final String FLASH_MODE_TORCH = "torch"; + + // Values for scene mode settings. + public static final String SCENE_MODE_AUTO = "auto"; + public static final String SCENE_MODE_ACTION = "action"; + public static final String SCENE_MODE_PORTRAIT = "portrait"; + public static final String SCENE_MODE_LANDSCAPE = "landscape"; + public static final String SCENE_MODE_NIGHT = "night"; + public static final String SCENE_MODE_NIGHT_PORTRAIT = "night-portrait"; + public static final String SCENE_MODE_THEATRE = "theatre"; + public static final String SCENE_MODE_BEACH = "beach"; + public static final String SCENE_MODE_SNOW = "snow"; + public static final String SCENE_MODE_SUNSET = "sunset"; + public static final String SCENE_MODE_STEADYPHOTO = "steadyphoto"; + public static final String SCENE_MODE_FIREWORKS = "fireworks"; + public static final String SCENE_MODE_SPORTS = "sports"; + public static final String SCENE_MODE_PARTY = "party"; + public static final String SCENE_MODE_CANDLELIGHT = "candlelight"; + + // Values for focus mode settings. + /** + * Auto-focus mode. + */ + public static final String FOCUS_MODE_AUTO = "auto"; + + /** + * Focus is set at infinity. Applications should not call + * {@link #autoFocus(AutoFocusCallback)} in this mode. + */ + public static final String FOCUS_MODE_INFINITY = "infinity"; + public static final String FOCUS_MODE_MACRO = "macro"; + + /** + * Focus is fixed. The camera is always in this mode if the focus is not + * adjustable. If the camera has auto-focus, this mode can fix the + * focus, which is usually at hyperfocal distance. Applications should + * not call {@link #autoFocus(AutoFocusCallback)} in this mode. + */ + public static final String FOCUS_MODE_FIXED = "fixed"; + + // Formats for setPreviewFormat and setPictureFormat. + private static final String PIXEL_FORMAT_YUV422SP = "yuv422sp"; + private static final String PIXEL_FORMAT_YUV420SP = "yuv420sp"; + private static final String PIXEL_FORMAT_YUV422I = "yuv422i-yuyv"; + private static final String PIXEL_FORMAT_RGB565 = "rgb565"; + private static final String PIXEL_FORMAT_JPEG = "jpeg"; + private HashMap mMap; private Parameters() { @@ -472,7 +803,7 @@ public class Camera { * Creates a single string with all the parameters set in * this Parameters object. *

      The {@link #unflatten(String)} method does the reverse.

      - * + * * @return a String with all values from this Parameters object, in * semi-colon delimited key-value pairs */ @@ -490,16 +821,16 @@ public class Camera { } /** - * Takes a flattened string of parameters and adds each one to + * Takes a flattened string of parameters and adds each one to * this Parameters object. *

      The {@link #flatten()} method does the reverse.

      - * - * @param flattened a String of parameters (key-value paired) that + * + * @param flattened a String of parameters (key-value paired) that * are semi-colon delimited */ public void unflatten(String flattened) { mMap.clear(); - + StringTokenizer tokenizer = new StringTokenizer(flattened, ";"); while (tokenizer.hasMoreElements()) { String kv = tokenizer.nextToken(); @@ -512,14 +843,14 @@ public class Camera { mMap.put(k, v); } } - + public void remove(String key) { mMap.remove(key); } /** * Sets a String parameter. - * + * * @param key the key name for the parameter * @param value the String value of the parameter */ @@ -538,7 +869,7 @@ public class Camera { /** * Sets an integer parameter. - * + * * @param key the key name for the parameter * @param value the int value of the parameter */ @@ -548,7 +879,7 @@ public class Camera { /** * Returns the value of a String parameter. - * + * * @param key the key name for the parameter * @return the String value of the parameter */ @@ -558,7 +889,7 @@ public class Camera { /** * Returns the value of an integer parameter. - * + * * @param key the key name for the parameter * @return the int value of the parameter */ @@ -568,110 +899,136 @@ public class Camera { /** * Sets the dimensions for preview pictures. - * + * * @param width the width of the pictures, in pixels * @param height the height of the pictures, in pixels */ public void setPreviewSize(int width, int height) { String v = Integer.toString(width) + "x" + Integer.toString(height); - set("preview-size", v); + set(KEY_PREVIEW_SIZE, v); } /** * Returns the dimensions setting for preview pictures. - * - * @return a Size object with the height and width setting + * + * @return a Size object with the height and width setting * for the preview picture */ public Size getPreviewSize() { - String pair = get("preview-size"); - if (pair == null) - return null; - String[] dims = pair.split("x"); - if (dims.length != 2) - return null; - - return new Size(Integer.parseInt(dims[0]), - Integer.parseInt(dims[1])); + String pair = get(KEY_PREVIEW_SIZE); + return strToSize(pair); + } + /** + * Gets the supported preview sizes. + * + * @return a List of Size object. null if preview size setting is not + * supported. + */ + public List getSupportedPreviewSizes() { + String str = get(KEY_PREVIEW_SIZE + SUPPORTED_VALUES_SUFFIX); + return splitSize(str); } /** - * Sets the dimensions for EXIF thumbnails. - * + * Sets the dimensions for EXIF thumbnail in Jpeg picture. + * * @param width the width of the thumbnail, in pixels * @param height the height of the thumbnail, in pixels + */ + public void setJpegThumbnailSize(int width, int height) { + set(KEY_JPEG_THUMBNAIL_WIDTH, width); + set(KEY_JPEG_THUMBNAIL_HEIGHT, height); + } + + /** + * Returns the dimensions for EXIF thumbnail in Jpeg picture. * - * FIXME: unhide before release - * @hide + * @return a Size object with the height and width setting for the EXIF + * thumbnails */ - public void setThumbnailSize(int width, int height) { - set("jpeg-thumbnail-width", width); - set("jpeg-thumbnail-height", height); + public Size getJpegThumbnailSize() { + return new Size(getInt(KEY_JPEG_THUMBNAIL_WIDTH), + getInt(KEY_JPEG_THUMBNAIL_HEIGHT)); } /** - * Returns the dimensions for EXIF thumbnail - * - * @return a Size object with the height and width setting - * for the EXIF thumbnails + * Sets the quality of the EXIF thumbnail in Jpeg picture. * - * FIXME: unhide before release - * @hide + * @param quality the JPEG quality of the EXIF thumbnail. The range is 1 + * to 100, with 100 being the best. */ - public Size getThumbnailSize() { - return new Size(getInt("jpeg-thumbnail-width"), - getInt("jpeg-thumbnail-height")); + public void setJpegThumbnailQuality(int quality) { + set(KEY_JPEG_THUMBNAIL_QUALITY, quality); } /** - * Sets the quality of the EXIF thumbnail - * - * @param quality the JPEG quality of the EXIT thumbnail + * Returns the quality setting for the EXIF thumbnail in Jpeg picture. * - * FIXME: unhide before release - * @hide + * @return the JPEG quality setting of the EXIF thumbnail. */ - public void setThumbnailQuality(int quality) { - set("jpeg-thumbnail-quality", quality); + public int getJpegThumbnailQuality() { + return getInt(KEY_JPEG_THUMBNAIL_QUALITY); } /** - * Returns the quality setting for the EXIF thumbnail - * - * @return the JPEG quality setting of the EXIF thumbnail + * Sets Jpeg quality of captured picture. * - * FIXME: unhide before release - * @hide + * @param quality the JPEG quality of captured picture. The range is 1 + * to 100, with 100 being the best. */ - public int getThumbnailQuality() { - return getInt("jpeg-thumbnail-quality"); + public void setJpegQuality(int quality) { + set(KEY_JPEG_QUALITY, quality); + } + + /** + * Returns the quality setting for the JPEG picture. + * + * @return the JPEG picture quality setting. + */ + public int getJpegQuality() { + return getInt(KEY_JPEG_QUALITY); } /** * Sets the rate at which preview frames are received. - * + * * @param fps the frame rate (frames per second) */ public void setPreviewFrameRate(int fps) { - set("preview-frame-rate", fps); + set(KEY_PREVIEW_FRAME_RATE, fps); } /** * Returns the setting for the rate at which preview frames * are received. - * + * * @return the frame rate setting (frames per second) */ public int getPreviewFrameRate() { - return getInt("preview-frame-rate"); + return getInt(KEY_PREVIEW_FRAME_RATE); + } + + /** + * Gets the supported preview frame rates. + * + * @return a List of Integer objects (preview frame rates). null if + * preview frame rate setting is not supported. + */ + public List getSupportedPreviewFrameRates() { + String str = get(KEY_PREVIEW_FRAME_RATE + SUPPORTED_VALUES_SUFFIX); + return splitInt(str); } /** * Sets the image format for preview pictures. - * - * @param pixel_format the desired preview picture format - * (PixelFormat.YCbCr_420_SP, + *

      If this is never called, the default format will be + * {@link android.graphics.PixelFormat#YCbCr_420_SP}, which + * uses the NV21 encoding format.

      + * + * @param pixel_format the desired preview picture format, defined + * by one of the {@link android.graphics.PixelFormat} constants. + * (E.g., PixelFormat.YCbCr_420_SP (default), * PixelFormat.RGB_565, or * PixelFormat.JPEG) * @see android.graphics.PixelFormat @@ -679,56 +1036,79 @@ public class Camera { public void setPreviewFormat(int pixel_format) { String s = cameraFormatForPixelFormat(pixel_format); if (s == null) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException( + "Invalid pixel_format=" + pixel_format); } - set("preview-format", s); + set(KEY_PREVIEW_FORMAT, s); } /** - * Returns the image format for preview pictures. - * - * @return the PixelFormat int representing the preview picture format + * Returns the image format for preview pictures got from + * {@link PreviewCallback}. + * + * @return the {@link android.graphics.PixelFormat} int representing + * the preview picture format. */ public int getPreviewFormat() { - return pixelFormatForCameraFormat(get("preview-format")); + return pixelFormatForCameraFormat(get(KEY_PREVIEW_FORMAT)); + } + + /** + * Gets the supported preview formats. + * + * @return a List of Integer objects. null if preview format setting is + * not supported. + */ + public List getSupportedPreviewFormats() { + String str = get(KEY_PREVIEW_FORMAT + SUPPORTED_VALUES_SUFFIX); + ArrayList formats = new ArrayList(); + for (String s : split(str)) { + int f = pixelFormatForCameraFormat(s); + if (f == PixelFormat.UNKNOWN) continue; + formats.add(f); + } + return formats; } /** * Sets the dimensions for pictures. - * + * * @param width the width for pictures, in pixels * @param height the height for pictures, in pixels */ public void setPictureSize(int width, int height) { String v = Integer.toString(width) + "x" + Integer.toString(height); - set("picture-size", v); + set(KEY_PICTURE_SIZE, v); } /** * Returns the dimension setting for pictures. - * - * @return a Size object with the height and width setting + * + * @return a Size object with the height and width setting * for pictures */ public Size getPictureSize() { - String pair = get("picture-size"); - if (pair == null) - return null; - String[] dims = pair.split("x"); - if (dims.length != 2) - return null; - - return new Size(Integer.parseInt(dims[0]), - Integer.parseInt(dims[1])); + String pair = get(KEY_PICTURE_SIZE); + return strToSize(pair); + } + /** + * Gets the supported picture sizes. + * + * @return a List of Size objects. null if picture size setting is not + * supported. + */ + public List getSupportedPictureSizes() { + String str = get(KEY_PICTURE_SIZE + SUPPORTED_VALUES_SUFFIX); + return splitSize(str); } /** * Sets the image format for pictures. - * - * @param pixel_format the desired picture format - * (PixelFormat.YCbCr_420_SP, + * + * @param pixel_format the desired picture format + * (PixelFormat.YCbCr_420_SP (NV21), * PixelFormat.RGB_565, or * PixelFormat.JPEG) * @see android.graphics.PixelFormat @@ -736,27 +1116,40 @@ public class Camera { public void setPictureFormat(int pixel_format) { String s = cameraFormatForPixelFormat(pixel_format); if (s == null) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException( + "Invalid pixel_format=" + pixel_format); } - set("picture-format", s); + set(KEY_PICTURE_FORMAT, s); } /** * Returns the image format for pictures. - * + * * @return the PixelFormat int representing the picture format */ public int getPictureFormat() { - return pixelFormatForCameraFormat(get("picture-format")); + return pixelFormatForCameraFormat(get(KEY_PICTURE_FORMAT)); + } + + /** + * Gets the supported picture formats. + * + * @return a List of Integer objects (values are PixelFormat.XXX). null + * if picture setting is not supported. + */ + public List getSupportedPictureFormats() { + String str = get(KEY_PICTURE_SIZE + SUPPORTED_VALUES_SUFFIX); + return splitInt(str); } private String cameraFormatForPixelFormat(int pixel_format) { switch(pixel_format) { - case PixelFormat.YCbCr_422_SP: return "yuv422sp"; - case PixelFormat.YCbCr_420_SP: return "yuv420sp"; - case PixelFormat.RGB_565: return "rgb565"; - case PixelFormat.JPEG: return "jpeg"; + case PixelFormat.YCbCr_422_SP: return PIXEL_FORMAT_YUV422SP; + case PixelFormat.YCbCr_420_SP: return PIXEL_FORMAT_YUV420SP; + case PixelFormat.YCbCr_422_I: return PIXEL_FORMAT_YUV422I; + case PixelFormat.RGB_565: return PIXEL_FORMAT_RGB565; + case PixelFormat.JPEG: return PIXEL_FORMAT_JPEG; default: return null; } } @@ -765,22 +1158,423 @@ public class Camera { if (format == null) return PixelFormat.UNKNOWN; - if (format.equals("yuv422sp")) + if (format.equals(PIXEL_FORMAT_YUV422SP)) return PixelFormat.YCbCr_422_SP; - if (format.equals("yuv420sp")) + if (format.equals(PIXEL_FORMAT_YUV420SP)) return PixelFormat.YCbCr_420_SP; - if (format.equals("rgb565")) + if (format.equals(PIXEL_FORMAT_YUV422I)) + return PixelFormat.YCbCr_422_I; + + if (format.equals(PIXEL_FORMAT_RGB565)) return PixelFormat.RGB_565; - if (format.equals("jpeg")) + if (format.equals(PIXEL_FORMAT_JPEG)) return PixelFormat.JPEG; return PixelFormat.UNKNOWN; } - }; -} + /** + * Sets the orientation of the device in degrees. For example, suppose + * the natural position of the device is landscape. If the user takes a + * picture in landscape mode in 2048x1536 resolution, the rotation + * should be set to 0. If the user rotates the phone 90 degrees + * clockwise, the rotation should be set to 90. Applications can use + * {@link android.view.OrientationEventListener} to set this parameter. + * + * The camera driver may set orientation in the EXIF header without + * rotating the picture. Or the driver may rotate the picture and + * the EXIF thumbnail. If the Jpeg picture is rotated, the orientation + * in the EXIF header will be missing or 1 (row #0 is top and column #0 + * is left side). + * + * @param rotation The orientation of the device in degrees. Rotation + * can only be 0, 90, 180 or 270. + * @throws IllegalArgumentException if rotation value is invalid. + * @see android.view.OrientationEventListener + */ + public void setRotation(int rotation) { + if (rotation == 0 || rotation == 90 || rotation == 180 + || rotation == 270) { + set(KEY_ROTATION, Integer.toString(rotation)); + } else { + throw new IllegalArgumentException( + "Invalid rotation=" + rotation); + } + } + + /** + * Sets GPS latitude coordinate. This will be stored in JPEG EXIF + * header. + * + * @param latitude GPS latitude coordinate. + */ + public void setGpsLatitude(double latitude) { + set(KEY_GPS_LATITUDE, Double.toString(latitude)); + } + + /** + * Sets GPS longitude coordinate. This will be stored in JPEG EXIF + * header. + * + * @param longitude GPS longitude coordinate. + */ + public void setGpsLongitude(double longitude) { + set(KEY_GPS_LONGITUDE, Double.toString(longitude)); + } + + /** + * Sets GPS altitude. This will be stored in JPEG EXIF header. + * + * @param altitude GPS altitude in meters. + */ + public void setGpsAltitude(double altitude) { + set(KEY_GPS_ALTITUDE, Double.toString(altitude)); + } + + /** + * Sets GPS timestamp. This will be stored in JPEG EXIF header. + * + * @param timestamp GPS timestamp (UTC in seconds since January 1, + * 1970). + */ + public void setGpsTimestamp(long timestamp) { + set(KEY_GPS_TIMESTAMP, Long.toString(timestamp)); + } + + /** + * Removes GPS latitude, longitude, altitude, and timestamp from the + * parameters. + */ + public void removeGpsData() { + remove(KEY_GPS_LATITUDE); + remove(KEY_GPS_LONGITUDE); + remove(KEY_GPS_ALTITUDE); + remove(KEY_GPS_TIMESTAMP); + } + + /** + * Gets the current white balance setting. + * + * @return one of WHITE_BALANCE_XXX string constant. null if white + * balance setting is not supported. + */ + public String getWhiteBalance() { + return get(KEY_WHITE_BALANCE); + } + + /** + * Sets the white balance. + * + * @param value WHITE_BALANCE_XXX string constant. + */ + public void setWhiteBalance(String value) { + set(KEY_WHITE_BALANCE, value); + } + + /** + * Gets the supported white balance. + * + * @return a List of WHITE_BALANCE_XXX string constants. null if white + * balance setting is not supported. + */ + public List getSupportedWhiteBalance() { + String str = get(KEY_WHITE_BALANCE + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + /** + * Gets the current color effect setting. + * + * @return one of EFFECT_XXX string constant. null if color effect + * setting is not supported. + */ + public String getColorEffect() { + return get(KEY_EFFECT); + } + + /** + * Sets the current color effect setting. + * + * @param value EFFECT_XXX string constants. + */ + public void setColorEffect(String value) { + set(KEY_EFFECT, value); + } + + /** + * Gets the supported color effects. + * + * @return a List of EFFECT_XXX string constants. null if color effect + * setting is not supported. + */ + public List getSupportedColorEffects() { + String str = get(KEY_EFFECT + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + + /** + * Gets the current antibanding setting. + * + * @return one of ANTIBANDING_XXX string constant. null if antibanding + * setting is not supported. + */ + public String getAntibanding() { + return get(KEY_ANTIBANDING); + } + + /** + * Sets the antibanding. + * + * @param antibanding ANTIBANDING_XXX string constant. + */ + public void setAntibanding(String antibanding) { + set(KEY_ANTIBANDING, antibanding); + } + + /** + * Gets the supported antibanding values. + * + * @return a List of ANTIBANDING_XXX string constants. null if + * antibanding setting is not supported. + */ + public List getSupportedAntibanding() { + String str = get(KEY_ANTIBANDING + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + /** + * Gets the current scene mode setting. + * + * @return one of SCENE_MODE_XXX string constant. null if scene mode + * setting is not supported. + */ + public String getSceneMode() { + return get(KEY_SCENE_MODE); + } + + /** + * Sets the scene mode. Other parameters may be changed after changing + * scene mode. For example, flash and supported flash mode may be + * changed to "off" in night scene mode. After setting scene mode, + * applications should call getParameters to know if some parameters are + * changed. + * + * @param value SCENE_MODE_XXX string constants. + */ + public void setSceneMode(String value) { + set(KEY_SCENE_MODE, value); + } + + /** + * Gets the supported scene modes. + * + * @return a List of SCENE_MODE_XXX string constant. null if scene mode + * setting is not supported. + */ + public List getSupportedSceneModes() { + String str = get(KEY_SCENE_MODE + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + /** + * Gets the current flash mode setting. + * + * @return one of FLASH_MODE_XXX string constant. null if flash mode + * setting is not supported. + */ + public String getFlashMode() { + return get(KEY_FLASH_MODE); + } + + /** + * Sets the flash mode. + * + * @param value FLASH_MODE_XXX string constants. + */ + public void setFlashMode(String value) { + set(KEY_FLASH_MODE, value); + } + + /** + * Gets the supported flash modes. + * + * @return a List of FLASH_MODE_XXX string constants. null if flash mode + * setting is not supported. + */ + public List getSupportedFlashModes() { + String str = get(KEY_FLASH_MODE + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + /** + * Gets the current focus mode setting. + * + * @return one of FOCUS_MODE_XXX string constant. If the camera does not + * support auto-focus, this should return {@link + * #FOCUS_MODE_FIXED}. If the focus mode is not FOCUS_MODE_FIXED + * or {@link #FOCUS_MODE_INFINITY}, applications should call + * {@link #autoFocus(AutoFocusCallback)} to start the focus. + */ + public String getFocusMode() { + return get(KEY_FOCUS_MODE); + } + /** + * Sets the focus mode. + * + * @param value FOCUS_MODE_XXX string constants. + */ + public void setFocusMode(String value) { + set(KEY_FOCUS_MODE, value); + } + /** + * Gets the supported focus modes. + * + * @return a List of FOCUS_MODE_XXX string constants. null if focus mode + * setting is not supported. + */ + public List getSupportedFocusModes() { + String str = get(KEY_FOCUS_MODE + SUPPORTED_VALUES_SUFFIX); + return split(str); + } + + /** + * Gets current zoom value. This also works when smooth zoom is in + * progress. + * + * @return the current zoom value. The range is 0 to {@link + * #getMaxZoom}. + * @hide + */ + public int getZoom() { + return getInt("zoom"); + } + + /** + * Sets current zoom value. If {@link #startSmoothZoom(int)} has been + * called and zoom is not stopped yet, applications should not call this + * method. + * + * @param value zoom value. The valid range is 0 to {@link #getMaxZoom}. + * @hide + */ + public void setZoom(int value) { + set("zoom", value); + } + + /** + * Returns true if zoom is supported. Applications should call this + * before using other zoom methods. + * + * @return true if zoom is supported. + * @hide + */ + public boolean isZoomSupported() { + String str = get("zoom-supported"); + return "true".equals(str); + } + + /** + * Gets the maximum zoom value allowed for snapshot. This is the maximum + * value that applications can set to {@link #setZoom(int)}. + * + * @return the maximum zoom value supported by the camera. + * @hide + */ + public int getMaxZoom() { + return getInt("max-zoom"); + } + + /** + * Gets the zoom factors of all zoom values. + * + * @return the zoom factors in 1/100 increments. Ex: a zoom of 3.2x is + * returned as 320. Accuracy of the value is dependent on the + * hardware implementation. The first element of the list is the + * zoom factor of first zoom value. If the first zoom value is + * 0, the zoom factor should be 100. The last element is the + * zoom factor of zoom value {@link #getMaxZoom}. + * @hide + */ + public List getZoomFactors() { + return splitInt(get("zoom-factors")); + } + + /** + * Returns true if smooth zoom is supported. Applications should call + * this before using other smooth zoom methods. + * + * @return true if smooth zoom is supported. + * @hide + */ + public boolean isSmoothZoomSupported() { + String str = get("smooth-zoom-supported"); + return "true".equals(str); + } + + // Splits a comma delimited string to an ArrayList of String. + // Return null if the passing string is null or the size is 0. + private ArrayList split(String str) { + if (str == null) return null; + + // Use StringTokenizer because it is faster than split. + StringTokenizer tokenizer = new StringTokenizer(str, ","); + ArrayList substrings = new ArrayList(); + while (tokenizer.hasMoreElements()) { + substrings.add(tokenizer.nextToken()); + } + return substrings; + } + + // Splits a comma delimited string to an ArrayList of Integer. + // Return null if the passing string is null or the size is 0. + private ArrayList splitInt(String str) { + if (str == null) return null; + + StringTokenizer tokenizer = new StringTokenizer(str, ","); + ArrayList substrings = new ArrayList(); + while (tokenizer.hasMoreElements()) { + String token = tokenizer.nextToken(); + substrings.add(Integer.parseInt(token)); + } + if (substrings.size() == 0) return null; + return substrings; + } + + // Splits a comma delimited string to an ArrayList of Size. + // Return null if the passing string is null or the size is 0. + private ArrayList splitSize(String str) { + if (str == null) return null; + + StringTokenizer tokenizer = new StringTokenizer(str, ","); + ArrayList sizeList = new ArrayList(); + while (tokenizer.hasMoreElements()) { + Size size = strToSize(tokenizer.nextToken()); + if (size != null) sizeList.add(size); + } + if (sizeList.size() == 0) return null; + return sizeList; + } + + // Parses a string (ex: "480x320") to Size object. + // Return null if the passing string is null. + private Size strToSize(String str) { + if (str == null) return null; + + int pos = str.indexOf('x'); + if (pos != -1) { + String width = str.substring(0, pos); + String height = str.substring(pos + 1); + return new Size(Integer.parseInt(width), + Integer.parseInt(height)); + } + Log.e(TAG, "Invalid size parameter string=" + str); + return null; + } + }; +} diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 0ce2f7bc84784139c02cc541e712cf181eac36fa..f5fed4f9031dd820d7532445363ac72dfd8894c1 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -46,13 +46,21 @@ public class Sensor { /** A constant describing a gyroscope sensor type */ public static final int TYPE_GYROSCOPE = 4; - /** A constant describing a light sensor type */ + /** + * A constant describing an light sensor type. + * See {@link android.hardware.SensorEvent SensorEvent} + * for more details. + */ public static final int TYPE_LIGHT = 5; /** A constant describing a pressure sensor type */ public static final int TYPE_PRESSURE = 6; /** A constant describing a temperature sensor type */ public static final int TYPE_TEMPERATURE = 7; - /** A constant describing a proximity sensor type */ + /** + * A constant describing an proximity sensor type. + * See {@link android.hardware.SensorEvent SensorEvent} + * for more details. + */ public static final int TYPE_PROXIMITY = 8; diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java index cf939c52a5e3ec7ab3602456447e2af1f8ce40c6..32d56910c36592bc60a8d9d378c67797a1802044 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -115,8 +115,19 @@ public class SensorEvent { *

      {@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD Sensor.TYPE_MAGNETIC_FIELD}:

      * All values are in micro-Tesla (uT) and measure the ambient magnetic * field in the X, Y and Z axis. - * - */ + * + *

      {@link android.hardware.Sensor#TYPE_LIGHT Sensor.TYPE_LIGHT}:

      + * + *

      values[0]: Ambient light level in SI lux units + * + *

      {@link android.hardware.Sensor#TYPE_PROXIMITY Sensor.TYPE_PROXIMITY}:

      + * + *

      values[0]: Proximity sensor distance measured in centimeters + * + *

      Note that some proximity sensors only support a binary "close" or "far" measurement. + * In this case, the sensor should report its maxRange value in the "far" state and a value + * less than maxRange in the "near" state. + */ public final float[] values; /** diff --git a/core/java/android/hardware/SensorListener.java b/core/java/android/hardware/SensorListener.java index cfa184bf1784ded2c7bd035ae2f487a868283791..c71e968d27133e519004ab104d45b11cb265bf76 100644 --- a/core/java/android/hardware/SensorListener.java +++ b/core/java/android/hardware/SensorListener.java @@ -20,9 +20,8 @@ package android.hardware; * Used for receiving notifications from the SensorManager when * sensor values have changed. * - * This interface is deprecated, use + * @deprecated Use * {@link android.hardware.SensorEventListener SensorEventListener} instead. - * */ @Deprecated public interface SensorListener { diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index bf945ec7bae33525decaeed3067266719b977303..271f973e97e02fa9b6831becd8386a423cb0d9ae 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -116,47 +116,67 @@ public class SensorManager @Deprecated public static final int SENSOR_ORIENTATION_RAW = 1 << 7; - /** A constant that includes all sensors */ + /** A constant that includes all sensors + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int SENSOR_ALL = 0x7F; - /** Smallest sensor ID */ + /** Smallest sensor ID + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int SENSOR_MIN = SENSOR_ORIENTATION; - /** Largest sensor ID */ + /** Largest sensor ID + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int SENSOR_MAX = ((SENSOR_ALL + 1)>>1); /** Index of the X value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int DATA_X = 0; /** Index of the Y value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int DATA_Y = 1; /** Index of the Z value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int DATA_Z = 2; /** Offset to the untransformed values in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int RAW_DATA_INDEX = 3; /** Index of the untransformed X value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int RAW_DATA_X = 3; /** Index of the untransformed Y value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int RAW_DATA_Y = 4; /** Index of the untransformed Z value in the array returned by - * {@link android.hardware.SensorListener#onSensorChanged} */ + * {@link android.hardware.SensorListener#onSensorChanged} + * @deprecated use {@link android.hardware.Sensor Sensor} instead. + */ @Deprecated public static final int RAW_DATA_Z = 5; diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index eedcc354e61337a931919511095620f58dbbd4f6..36196536f689d3b4d60cb1bfb2e461c1f653d93a 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -45,6 +45,9 @@ public abstract class AbstractInputMethodService extends Service implements KeyEvent.Callback { private InputMethod mInputMethod; + final KeyEvent.DispatcherState mDispatcherState + = new KeyEvent.DispatcherState(); + /** * Base class for derived classes to implement their {@link InputMethod} * interface. This takes care of basic maintenance of the input method, @@ -129,7 +132,8 @@ public abstract class AbstractInputMethodService extends Service * callbacks on the service, and tell the client when this is done. */ public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) { - boolean handled = event.dispatch(AbstractInputMethodService.this); + boolean handled = event.dispatch(AbstractInputMethodService.this, + mDispatcherState, this); if (callback != null) { callback.finishedEvent(seq, handled); } @@ -147,6 +151,16 @@ public abstract class AbstractInputMethodService extends Service } } + /** + * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} + * for used for processing events from the target application. + * Normally you will not need to use this directly, but + * just use the standard high-level event callbacks like {@link #onKeyDown}. + */ + public KeyEvent.DispatcherState getKeyDispatcherState() { + return mDispatcherState; + } + /** * Called by the framework during initialization, when the InputMethod * interface for this service needs to be created. diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 6ee92ce6b019a5b74a0b23711a340bab3f610fdc..5499bbafb0ff91f376e6b5e2e64b46b7c0a59606 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -554,7 +554,7 @@ public class InputMethodService extends AbstractInputMethodService { mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); mInflater = (LayoutInflater)getSystemService( Context.LAYOUT_INFLATER_SERVICE); - mWindow = new SoftInputWindow(this, mTheme); + mWindow = new SoftInputWindow(this, mTheme, mDispatcherState); initViews(); mWindow.getWindow().setLayout(FILL_PARENT, WRAP_CONTENT); } @@ -1557,6 +1557,28 @@ public class InputMethodService extends AbstractInputMethodService { mImm.showSoftInputFromInputMethod(mToken, flags); } + private boolean handleBack(boolean doIt) { + if (mShowInputRequested) { + // If the soft input area is shown, back closes it and we + // consume the back key. + if (doIt) requestHideSelf(0); + return true; + } else if (mWindowVisible) { + if (mCandidatesVisibility == View.VISIBLE) { + // If we are showing candidates even if no input area, then + // hide them. + if (doIt) setCandidatesViewShown(false); + } else { + // If we have the window visible for some other reason -- + // most likely to show candidates -- then just get rid + // of it. This really shouldn't happen, but just in case... + if (doIt) hideWindow(); + } + return true; + } + return false; + } + /** * Override this to intercept key down events before they are processed by the * application. If you return true, the application will not itself @@ -1564,37 +1586,32 @@ public class InputMethodService extends AbstractInputMethodService { * will occur as if the IME had not seen the event at all. * *

      The default implementation intercepts {@link KeyEvent#KEYCODE_BACK - * KeyEvent.KEYCODE_BACK} to hide the current IME UI if it is shown. In - * additional, in fullscreen mode only, it will consume DPAD movement + * KeyEvent.KEYCODE_BACK} if the IME is currently shown, to + * possibly hide it when the key goes up (if not canceled or long pressed). In + * addition, in fullscreen mode only, it will consume DPAD movement * events to move the cursor in the extracted text view, not allowing * them to perform navigation in the underlying application. */ public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK - && event.getRepeatCount() == 0) { - if (mShowInputRequested) { - // If the soft input area is shown, back closes it and we - // consume the back key. - requestHideSelf(0); + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (handleBack(false)) { + event.startTracking(); return true; - } else if (mWindowVisible) { - if (mCandidatesVisibility == View.VISIBLE) { - // If we are showing candidates even if no input area, then - // hide them. - setCandidatesViewShown(false); - return true; - } else { - // If we have the window visible for some other reason -- - // most likely to show candidates -- then just get rid - // of it. This really shouldn't happen, but just in case... - hideWindow(); - return true; - } } + return false; } return doMovementKey(keyCode, event, MOVEMENT_DOWN); } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Override this to intercept special key multiple events before they are * processed by the @@ -1617,12 +1634,18 @@ public class InputMethodService extends AbstractInputMethodService { * process the event. If you return true, the normal application processing * will occur as if the IME had not seen the event at all. * - *

      The default implementation always returns false, except when - * in fullscreen mode, where it will consume DPAD movement + *

      The default implementation intercepts {@link KeyEvent#KEYCODE_BACK + * KeyEvent.KEYCODE_BACK} to hide the current IME UI if it is shown. In + * addition, in fullscreen mode only, it will consume DPAD movement * events to move the cursor in the extracted text view, not allowing * them to perform navigation in the underlying application. */ public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isTracking() + && !event.isCanceled()) { + return handleBack(true); + } + return doMovementKey(keyCode, event, MOVEMENT_UP); } @@ -1992,8 +2015,9 @@ public class InputMethodService extends AbstractInputMethodService { req.flags = InputConnection.GET_TEXT_WITH_STYLES; req.hintMaxLines = 10; req.hintMaxChars = 10000; - mExtractedText = getCurrentInputConnection().getExtractedText(req, - InputConnection.GET_EXTRACTED_TEXT_MONITOR); + InputConnection ic = getCurrentInputConnection(); + mExtractedText = ic == null? null + : ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); final EditorInfo ei = getCurrentInputEditorInfo(); diff --git a/core/java/android/inputmethodservice/Keyboard.java b/core/java/android/inputmethodservice/Keyboard.java index fea63be795b2525ef77d3ccbbe197612a4ca3335..4814b0aa4c3796374ab5fd58740b169958584191 100755 --- a/core/java/android/inputmethodservice/Keyboard.java +++ b/core/java/android/inputmethodservice/Keyboard.java @@ -142,7 +142,7 @@ public class Keyboard { private int[][] mGridNeighbors; private int mProximityThreshold; /** Number of key widths from current touch point to search for nearest keys. */ - private static float SEARCH_DISTANCE = 1.4f; + private static float SEARCH_DISTANCE = 1.8f; /** * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 9c9c14395f15ae6b02f853c7a859dd968b5fdf24..0f7ef22f93fbc0e928257738cb369189698ad2f8 100755 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -30,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.inputmethodservice.Keyboard.Key; import android.os.Handler; import android.os.Message; -import android.os.SystemClock; import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; @@ -38,6 +37,7 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; import android.widget.PopupWindow; import android.widget.TextView; @@ -163,8 +163,8 @@ public class KeyboardView extends View implements View.OnClickListener { private static final int MSG_REPEAT = 3; private static final int MSG_LONGPRESS = 4; - private static final int DELAY_BEFORE_PREVIEW = 40; - private static final int DELAY_AFTER_PREVIEW = 60; + private static final int DELAY_BEFORE_PREVIEW = 0; + private static final int DELAY_AFTER_PREVIEW = 70; private int mVerticalCorrection; private int mProximityThreshold; @@ -191,6 +191,7 @@ public class KeyboardView extends View implements View.OnClickListener { private int mLastCodeX; private int mLastCodeY; private int mCurrentKey = NOT_A_KEY; + private int mDownKey = NOT_A_KEY; private long mLastKeyTime; private long mCurrentKeyTime; private int[] mKeyIndices = new int[12]; @@ -202,13 +203,21 @@ public class KeyboardView extends View implements View.OnClickListener { private boolean mAbortKey; private Key mInvalidatedKey; private Rect mClipRegion = new Rect(0, 0, 0, 0); - + private boolean mPossiblePoly; + private SwipeTracker mSwipeTracker = new SwipeTracker(); + private int mSwipeThreshold; + private boolean mDisambiguateSwipe; + + // Variables for dealing with multiple pointers + private int mOldPointerCount = 1; + private float mOldPointerX; + private float mOldPointerY; + private Drawable mKeyBackground; private static final int REPEAT_INTERVAL = 50; // ~20 keys per second private static final int REPEAT_START_DELAY = 400; - private static final int LONGPRESS_TIMEOUT = 800; - // Deemed to be too short : ViewConfiguration.getLongPressTimeout(); + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); private static int MAX_NEARBY_KEYS = 12; private int[] mDistances = new int[MAX_NEARBY_KEYS]; @@ -227,6 +236,8 @@ public class KeyboardView extends View implements View.OnClickListener { private Rect mDirtyRect = new Rect(); /** The keyboard bitmap for faster updates */ private Bitmap mBuffer; + /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ + private boolean mKeyboardChanged; /** The canvas for the above mutable keyboard bitmap */ private Canvas mCanvas; @@ -340,11 +351,15 @@ public class KeyboardView extends View implements View.OnClickListener { mPaint.setAntiAlias(true); mPaint.setTextSize(keyTextSize); mPaint.setTextAlign(Align.CENTER); + mPaint.setAlpha(255); mPadding = new Rect(0, 0, 0, 0); mMiniKeyboardCache = new HashMap(); mKeyBackground.getPadding(mPadding); - + + mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); + mDisambiguateSwipe = getResources().getBoolean( + com.android.internal.R.bool.config_swipeDisambiguation); resetMultiTap(); initGestureDetector(); } @@ -354,22 +369,49 @@ public class KeyboardView extends View implements View.OnClickListener { @Override public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) { + if (mPossiblePoly) return false; final float absX = Math.abs(velocityX); final float absY = Math.abs(velocityY); - if (velocityX > 500 && absY < absX) { - swipeRight(); - return true; - } else if (velocityX < -500 && absY < absX) { - swipeLeft(); - return true; - } else if (velocityY < -500 && absX < absY) { - swipeUp(); - return true; - } else if (velocityY > 500 && absX < 200) { - swipeDown(); - return true; - } else if (absX > 800 || absY > 800) { - return true; + float deltaX = me2.getX() - me1.getX(); + float deltaY = me2.getY() - me1.getY(); + int travelX = getWidth() / 2; // Half the keyboard width + int travelY = getHeight() / 2; // Half the keyboard height + mSwipeTracker.computeCurrentVelocity(1000); + final float endingVelocityX = mSwipeTracker.getXVelocity(); + final float endingVelocityY = mSwipeTracker.getYVelocity(); + boolean sendDownKey = false; + if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { + if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { + sendDownKey = true; + } else { + swipeRight(); + return true; + } + } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { + if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { + sendDownKey = true; + } else { + swipeLeft(); + return true; + } + } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { + if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { + sendDownKey = true; + } else { + swipeUp(); + return true; + } + } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { + if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { + sendDownKey = true; + } else { + swipeDown(); + return true; + } + } + + if (sendDownKey) { + detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); } return false; } @@ -401,16 +443,20 @@ public class KeyboardView extends View implements View.OnClickListener { if (mKeyboard != null) { showPreview(NOT_A_KEY); } + // Remove any pending messages + removeMessages(); mKeyboard = keyboard; List keys = mKeyboard.getKeys(); mKeys = keys.toArray(new Key[keys.size()]); requestLayout(); - // Release buffer, just in case the new keyboard has a different size. - // It will be reallocated on the next draw. - mBuffer = null; + // Hint to reallocate the buffer if the size changed + mKeyboardChanged = true; invalidateAllKeys(); computeProximityThreshold(keyboard); mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views + // Switching to a different keyboard should abort any pending keys so that the key up + // doesn't get delivered to the old or new keyboard + mAbortKey = true; // Until the next ACTION_DOWN } /** @@ -564,17 +610,21 @@ public class KeyboardView extends View implements View.OnClickListener { @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); - if (mDrawPending || mBuffer == null) { + if (mDrawPending || mBuffer == null || mKeyboardChanged) { onBufferDraw(); } canvas.drawBitmap(mBuffer, 0, 0, null); } - + private void onBufferDraw() { - if (mBuffer == null) { - mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); - mCanvas = new Canvas(mBuffer); + if (mBuffer == null || mKeyboardChanged) { + if (mBuffer == null || mKeyboardChanged && + (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { + mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBuffer); + } invalidateAllKeys(); + mKeyboardChanged = false; } final Canvas canvas = mCanvas; canvas.clipRect(mDirtyRect, Op.REPLACE); @@ -590,7 +640,6 @@ public class KeyboardView extends View implements View.OnClickListener { final Key[] keys = mKeys; final Key invalidKey = mInvalidatedKey; - paint.setAlpha(255); paint.setColor(mKeyTextColor); boolean drawSingleKey = false; if (invalidKey != null && canvas.getClipBounds(clipRegion)) { @@ -611,7 +660,7 @@ public class KeyboardView extends View implements View.OnClickListener { } int[] drawableState = key.getCurrentDrawableState(); keyBackground.setState(drawableState); - + // Switch the character to uppercase if shift is pressed String label = key.label == null? null : adjustCase(key.label).toString(); @@ -680,7 +729,6 @@ public class KeyboardView extends View implements View.OnClickListener { private int getKeyIndices(int x, int y, int[] allKeys) { final Key[] keys = mKeys; - final boolean shifted = mKeyboard.isShifted(); int primaryIndex = NOT_A_KEY; int closestKey = NOT_A_KEY; int closestKeyDist = mProximityThreshold + 1; @@ -730,8 +778,7 @@ public class KeyboardView extends View implements View.OnClickListener { return primaryIndex; } - private void detectAndSendKey(int x, int y, long eventTime) { - int index = mCurrentKey; + private void detectAndSendKey(int index, int x, int y, long eventTime) { if (index != NOT_A_KEY && index < mKeys.length) { final Key key = mKeys[index]; if (key.text != null) { @@ -817,6 +864,7 @@ public class KeyboardView extends View implements View.OnClickListener { private void showKey(final int keyIndex) { final PopupWindow previewPopup = mPreviewPopup; final Key[] keys = mKeys; + if (keyIndex < 0 || keyIndex >= mKeys.length) return; Key key = keys[keyIndex]; if (key.icon != null) { mPreviewText.setCompoundDrawables(null, null, null, @@ -1011,19 +1059,69 @@ public class KeyboardView extends View implements View.OnClickListener { } return false; } - + + private long mOldEventTime; + private boolean mUsedVelocity; + @Override public boolean onTouchEvent(MotionEvent me) { + // Convert multi-pointer up/down events to single up/down events to + // deal with the typical multi-pointer behavior of two-thumb typing + final int pointerCount = me.getPointerCount(); + final int action = me.getAction(); + boolean result = false; + final long now = me.getEventTime(); + + if (pointerCount != mOldPointerCount) { + if (pointerCount == 1) { + // Send a down event for the latest pointer + MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + me.getX(), me.getY(), me.getMetaState()); + result = onModifiedTouchEvent(down, false); + down.recycle(); + // If it's an up action, then deliver the up as well. + if (action == MotionEvent.ACTION_UP) { + result = onModifiedTouchEvent(me, true); + } + } else { + // Send an up event for the last pointer + MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, + mOldPointerX, mOldPointerY, me.getMetaState()); + result = onModifiedTouchEvent(up, true); + up.recycle(); + } + } else { + if (pointerCount == 1) { + result = onModifiedTouchEvent(me, false); + mOldPointerX = me.getX(); + mOldPointerY = me.getY(); + } else { + // Don't do anything when 2 pointers are down and moving. + result = true; + } + } + mOldPointerCount = pointerCount; + + return result; + } + + private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { int touchX = (int) me.getX() - mPaddingLeft; int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; - int action = me.getAction(); - long eventTime = me.getEventTime(); + final int action = me.getAction(); + final long eventTime = me.getEventTime(); + mOldEventTime = eventTime; int keyIndex = getKeyIndices(touchX, touchY, null); - + mPossiblePoly = possiblePoly; + + // Track the last few movements to look for spurious swipes. + if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); + mSwipeTracker.addMovement(me); + if (mGestureDetector.onTouchEvent(me)) { showPreview(NOT_A_KEY); mHandler.removeMessages(MSG_REPEAT); - mHandler.removeMessages(MSG_LONGPRESS); + mHandler.removeMessages(MSG_LONGPRESS); return true; } @@ -1044,6 +1142,7 @@ public class KeyboardView extends View implements View.OnClickListener { mCurrentKeyTime = 0; mLastKey = NOT_A_KEY; mCurrentKey = keyIndex; + mDownKey = keyIndex; mDownTime = me.getEventTime(); mLastMoveTime = mDownTime; checkMultiTap(eventTime, keyIndex); @@ -1072,7 +1171,7 @@ public class KeyboardView extends View implements View.OnClickListener { if (keyIndex == mCurrentKey) { mCurrentKeyTime += eventTime - mLastMoveTime; continueLongPress = true; - } else { + } else if (mRepeatKeyIndex == NOT_A_KEY) { resetMultiTap(); mLastKey = mCurrentKey; mLastCodeX = mLastX; @@ -1083,10 +1182,6 @@ public class KeyboardView extends View implements View.OnClickListener { mCurrentKeyTime = 0; } } - if (keyIndex != mRepeatKeyIndex) { - mHandler.removeMessages(MSG_REPEAT); - mRepeatKeyIndex = NOT_A_KEY; - } } if (!continueLongPress) { // Cancel old longpress @@ -1097,13 +1192,11 @@ public class KeyboardView extends View implements View.OnClickListener { mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); } } - showPreview(keyIndex); + showPreview(mCurrentKey); break; case MotionEvent.ACTION_UP: - mHandler.removeMessages(MSG_SHOW_PREVIEW); - mHandler.removeMessages(MSG_REPEAT); - mHandler.removeMessages(MSG_LONGPRESS); + removeMessages(); if (keyIndex == mCurrentKey) { mCurrentKeyTime += eventTime - mLastMoveTime; } else { @@ -1122,11 +1215,17 @@ public class KeyboardView extends View implements View.OnClickListener { Arrays.fill(mKeyIndices, NOT_A_KEY); // If we're not on a repeating key (which sends on a DOWN event) if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { - detectAndSendKey(touchX, touchY, eventTime); + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); } invalidateKey(keyIndex); mRepeatKeyIndex = NOT_A_KEY; break; + case MotionEvent.ACTION_CANCEL: + removeMessages(); + mAbortKey = true; + showPreview(NOT_A_KEY); + invalidateKey(mCurrentKey); + break; } mLastX = touchX; mLastY = touchY; @@ -1135,7 +1234,7 @@ public class KeyboardView extends View implements View.OnClickListener { private boolean repeatKey() { Key key = mKeys[mRepeatKeyIndex]; - detectAndSendKey(key.x, key.y, mLastTapTime); + detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); return true; } @@ -1159,16 +1258,20 @@ public class KeyboardView extends View implements View.OnClickListener { if (mPreviewPopup.isShowing()) { mPreviewPopup.dismiss(); } - mHandler.removeMessages(MSG_REPEAT); - mHandler.removeMessages(MSG_LONGPRESS); - mHandler.removeMessages(MSG_SHOW_PREVIEW); + removeMessages(); dismissPopupKeyboard(); mBuffer = null; mCanvas = null; mMiniKeyboardCache.clear(); } - + + private void removeMessages() { + mHandler.removeMessages(MSG_REPEAT); + mHandler.removeMessages(MSG_LONGPRESS); + mHandler.removeMessages(MSG_SHOW_PREVIEW); + } + @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); @@ -1216,4 +1319,114 @@ public class KeyboardView extends View implements View.OnClickListener { resetMultiTap(); } } + + private static class SwipeTracker { + + static final int NUM_PAST = 4; + static final int LONGEST_PAST_TIME = 200; + + final float mPastX[] = new float[NUM_PAST]; + final float mPastY[] = new float[NUM_PAST]; + final long mPastTime[] = new long[NUM_PAST]; + + float mYVelocity; + float mXVelocity; + + public void clear() { + mPastTime[0] = 0; + } + + public void addMovement(MotionEvent ev) { + long time = ev.getEventTime(); + final int N = ev.getHistorySize(); + for (int i=0; i= 0) { + final int start = drop+1; + final int count = NUM_PAST-drop-1; + System.arraycopy(pastX, start, pastX, 0, count); + System.arraycopy(pastY, start, pastY, 0, count); + System.arraycopy(pastTime, start, pastTime, 0, count); + i -= (drop+1); + } + pastX[i] = x; + pastY[i] = y; + pastTime[i] = time; + i++; + if (i < NUM_PAST) { + pastTime[i] = 0; + } + } + + public void computeCurrentVelocity(int units) { + computeCurrentVelocity(units, Float.MAX_VALUE); + } + + public void computeCurrentVelocity(int units, float maxVelocity) { + final float[] pastX = mPastX; + final float[] pastY = mPastY; + final long[] pastTime = mPastTime; + + final float oldestX = pastX[0]; + final float oldestY = pastY[0]; + final long oldestTime = pastTime[0]; + float accumX = 0; + float accumY = 0; + int N=0; + while (N < NUM_PAST) { + if (pastTime[N] == 0) { + break; + } + N++; + } + + for (int i=1; i < N; i++) { + final int dur = (int)(pastTime[i] - oldestTime); + if (dur == 0) continue; + float dist = pastX[i] - oldestX; + float vel = (dist/dur) * units; // pixels/frame. + if (accumX == 0) accumX = vel; + else accumX = (accumX + vel) * .5f; + + dist = pastY[i] - oldestY; + vel = (dist/dur) * units; // pixels/frame. + if (accumY == 0) accumY = vel; + else accumY = (accumY + vel) * .5f; + } + mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) + : Math.min(accumX, maxVelocity); + mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) + : Math.min(accumY, maxVelocity); + } + + public float getXVelocity() { + return mXVelocity; + } + + public float getYVelocity() { + return mYVelocity; + } + } } diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index d91ace6b778f6042ef92d7d3638d7c2c66168847..6a54846fddbe8cd0637c4ecae0c10f5f17c0d614 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.os.IBinder; import android.view.Gravity; +import android.view.KeyEvent; import android.view.WindowManager; /** @@ -30,7 +31,8 @@ import android.view.WindowManager; * always visible. */ class SoftInputWindow extends Dialog { - + final KeyEvent.DispatcherState mDispatcherState; + public void setToken(IBinder token) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.token = token; @@ -49,11 +51,19 @@ class SoftInputWindow extends Dialog { * using styles. This theme is applied on top of the current theme in * context. If 0, the default dialog theme will be used. */ - public SoftInputWindow(Context context, int theme) { + public SoftInputWindow(Context context, int theme, + KeyEvent.DispatcherState dispatcherState) { super(context, theme); + mDispatcherState = dispatcherState; initDockWindow(); } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + mDispatcherState.reset(); + } + /** * Get the size of the DockWindow. * diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 1429bc1aff4d1f42411b1e12baeadac7a56156d3..a127df01b8e797c91beb33f03d050087f3403438 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -18,6 +18,7 @@ package android.net; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.os.Binder; import android.os.RemoteException; /** @@ -114,15 +115,64 @@ public class ConnectivityManager public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"; - public static final int TYPE_MOBILE = 0; - public static final int TYPE_WIFI = 1; + /** + * The Default Mobile data connection. When active, all data traffic + * will use this connection by default. Should not coexist with other + * default connections. + */ + public static final int TYPE_MOBILE = 0; + /** + * The Default WIFI data connection. When active, all data traffic + * will use this connection by default. Should not coexist with other + * default connections. + */ + public static final int TYPE_WIFI = 1; + /** + * An MMS-specific Mobile data connection. This connection may be the + * same as {@link #TYPEMOBILE} but it may be different. This is used + * by applications needing to talk to the carrier's Multimedia Messaging + * Service servers. It may coexist with default data connections. + * {@hide} + */ + public static final int TYPE_MOBILE_MMS = 2; + /** + * A SUPL-specific Mobile data connection. This connection may be the + * same as {@link #TYPEMOBILE} but it may be different. This is used + * by applications needing to talk to the carrier's Secure User Plane + * Location servers for help locating the device. It may coexist with + * default data connections. + * {@hide} + */ + public static final int TYPE_MOBILE_SUPL = 3; + /** + * A DUN-specific Mobile data connection. This connection may be the + * same as {@link #TYPEMOBILE} but it may be different. This is used + * by applicaitons performing a Dial Up Networking bridge so that + * the carrier is aware of DUN traffic. It may coexist with default data + * connections. + * {@hide} + */ + public static final int TYPE_MOBILE_DUN = 4; + /** + * A High Priority Mobile data connection. This connection is typically + * the same as {@link #TYPEMOBILE} but the routing setup is different. + * Only requesting processes will have access to the Mobile DNS servers + * and only IP's explicitly requested via {@link #requestRouteToHost} + * will route over this interface. + *{@hide} + */ + public static final int TYPE_MOBILE_HIPRI = 5; + /** {@hide} */ + public static final int MAX_RADIO_TYPE = TYPE_WIFI; + /** {@hide} */ + public static final int MAX_NETWORK_TYPE = TYPE_MOBILE_HIPRI; public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI; private IConnectivityManager mService; static public boolean isNetworkTypeValid(int networkType) { - return networkType == TYPE_WIFI || networkType == TYPE_MOBILE; + return networkType >= 0 && networkType <= MAX_NETWORK_TYPE; } public void setNetworkPreference(int preference) { @@ -195,7 +245,8 @@ public class ConnectivityManager */ public int startUsingNetworkFeature(int networkType, String feature) { try { - return mService.startUsingNetworkFeature(networkType, feature); + return mService.startUsingNetworkFeature(networkType, feature, + new Binder()); } catch (RemoteException e) { return -1; } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index de6859809d82e20ee41fa5d3a0327f729249395c..9f59ccede478c73fa5eb2b182f46c763fb57e306 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -17,6 +17,7 @@ package android.net; import android.net.NetworkInfo; +import android.os.IBinder; /** * Interface that answers queries about, and allows changing, the @@ -39,7 +40,8 @@ interface IConnectivityManager boolean setRadio(int networkType, boolean turnOn); - int startUsingNetworkFeature(int networkType, in String feature); + int startUsingNetworkFeature(int networkType, in String feature, + in IBinder binder); int stopUsingNetworkFeature(int networkType, in String feature); diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index 1064fb64fb942b21875ca642afb66996abeaf52f..538e51ab974f538e86d8584271e9fe908011b86f 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -32,9 +32,6 @@ import android.telephony.TelephonyManager; import android.util.Log; import android.text.TextUtils; -import java.util.List; -import java.util.ArrayList; - /** * Track the state of mobile data connectivity. This is done by * receiving broadcast intents from the Phone process whenever @@ -45,36 +42,49 @@ import java.util.ArrayList; public class MobileDataStateTracker extends NetworkStateTracker { private static final String TAG = "MobileDataStateTracker"; - private static final boolean DBG = false; + private static final boolean DBG = true; private Phone.DataState mMobileDataState; private ITelephony mPhoneService; - private static final String[] sDnsPropNames = { - "net.rmnet0.dns1", - "net.rmnet0.dns2", - "net.eth0.dns1", - "net.eth0.dns2", - "net.eth0.dns3", - "net.eth0.dns4", - "net.gprs.dns1", - "net.gprs.dns2" - }; - private List mDnsServers; - private String mInterfaceName; - private int mDefaultGatewayAddr; - private int mLastCallingPid = -1; + + private String mApnType; + private String mApnName; + private boolean mEnabled; + private BroadcastReceiver mStateReceiver; /** * Create a new MobileDataStateTracker * @param context the application context of the caller * @param target a message handler for getting callbacks about state changes + * @param netType the ConnectivityManager network type + * @param apnType the Phone apnType + * @param tag the name of this network */ - public MobileDataStateTracker(Context context, Handler target) { - super(context, target, ConnectivityManager.TYPE_MOBILE, - TelephonyManager.getDefault().getNetworkType(), "MOBILE", - TelephonyManager.getDefault().getNetworkTypeName()); + public MobileDataStateTracker(Context context, Handler target, + int netType, String apnType, String tag) { + super(context, target, netType, + TelephonyManager.getDefault().getNetworkType(), tag, + TelephonyManager.getDefault().getNetworkTypeName()); + mApnType = apnType; mPhoneService = null; - mDnsServers = new ArrayList(); + if(netType == ConnectivityManager.TYPE_MOBILE) { + mEnabled = true; + } else { + mEnabled = false; + } + + mDnsPropNames = new String[] { + "net.rmnet0.dns1", + "net.rmnet0.dns2", + "net.eth0.dns1", + "net.eth0.dns2", + "net.eth0.dns3", + "net.eth0.dns4", + "net.gprs.dns1", + "net.gprs.dns2", + "net.ppp0.dns1", + "net.ppp0.dns2"}; + } /** @@ -86,105 +96,133 @@ public class MobileDataStateTracker extends NetworkStateTracker { filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED); filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); - Intent intent = mContext.registerReceiver(new MobileDataStateReceiver(), filter); + mStateReceiver = new MobileDataStateReceiver(); + Intent intent = mContext.registerReceiver(mStateReceiver, filter); if (intent != null) mMobileDataState = getMobileDataState(intent); else mMobileDataState = Phone.DataState.DISCONNECTED; } - private static Phone.DataState getMobileDataState(Intent intent) { + private Phone.DataState getMobileDataState(Intent intent) { String str = intent.getStringExtra(Phone.STATE_KEY); - if (str != null) - return Enum.valueOf(Phone.DataState.class, str); - else - return Phone.DataState.DISCONNECTED; - } - - private class MobileDataStateReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { - Phone.DataState state = getMobileDataState(intent); - String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY); - String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); - boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, false); - if (DBG) Log.d(TAG, "Received " + intent.getAction() + - " broadcast - state = " + state - + ", unavailable = " + unavailable - + ", reason = " + (reason == null ? "(unspecified)" : reason)); - mNetworkInfo.setIsAvailable(!unavailable); - if (mMobileDataState != state) { - mMobileDataState = state; - - switch (state) { - case DISCONNECTED: - setDetailedState(DetailedState.DISCONNECTED, reason, apnName); - if (mInterfaceName != null) { - NetworkUtils.resetConnections(mInterfaceName); - } - mInterfaceName = null; - mDefaultGatewayAddr = 0; - break; - case CONNECTING: - setDetailedState(DetailedState.CONNECTING, reason, apnName); - break; - case SUSPENDED: - setDetailedState(DetailedState.SUSPENDED, reason, apnName); - break; - case CONNECTED: - mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); - if (mInterfaceName == null) { - Log.d(TAG, "CONNECTED event did not supply interface name."); - } - setupDnsProperties(); - setDetailedState(DetailedState.CONNECTED, reason, apnName); - break; - } - } - } else if (intent.getAction().equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) { - String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY); - String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); - if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" + - reason == null ? "" : "(" + reason + ")"); - setDetailedState(DetailedState.FAILED, reason, apnName); + if (str != null) { + String apnTypeList = + intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); + if (isApnTypeIncluded(apnTypeList)) { + return Enum.valueOf(Phone.DataState.class, str); } - TelephonyManager tm = TelephonyManager.getDefault(); - setRoamingStatus(tm.isNetworkRoaming()); - setSubtype(tm.getNetworkType(), tm.getNetworkTypeName()); } + return Phone.DataState.DISCONNECTED; } - /** - * Make sure that route(s) exist to the carrier DNS server(s). - */ - public void addPrivateRoutes() { - if (mInterfaceName != null) { - for (String addrString : mDnsServers) { - int addr = NetworkUtils.lookupHost(addrString); - if (addr != -1) { - NetworkUtils.addHostRoute(mInterfaceName, addr); - } - } - } - } + private boolean isApnTypeIncluded(String typeList) { + /* comma seperated list - split and check */ + if (typeList == null) + return false; - public void removePrivateRoutes() { - if(mInterfaceName != null) { - NetworkUtils.removeHostRoutes(mInterfaceName); + String[] list = typeList.split(","); + for(int i=0; i< list.length; i++) { + if (TextUtils.equals(list[i], mApnType) || + TextUtils.equals(list[i], Phone.APN_TYPE_ALL)) { + return true; + } } + return false; } - public void removeDefaultRoute() { - if(mInterfaceName != null) { - mDefaultGatewayAddr = NetworkUtils.getDefaultRoute(mInterfaceName); - NetworkUtils.removeDefaultRoute(mInterfaceName); - } - } + private class MobileDataStateReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + synchronized(this) { + if (intent.getAction().equals(TelephonyIntents. + ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { + Phone.DataState state = getMobileDataState(intent); + String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + String apnTypeList = intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); + mApnName = apnName; + + boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, + false); + + // set this regardless of the apnTypeList. It's all the same radio/network + // underneath + mNetworkInfo.setIsAvailable(!unavailable); + + if (isApnTypeIncluded(apnTypeList)) { + if (mEnabled == false) { + // if we're not enabled but the APN Type is supported by this connection + // we should record the interface name if one's provided. If the user + // turns on this network we will need the interfacename but won't get + // a fresh connected message - TODO fix this when we get per-APN + // notifications + if (state == Phone.DataState.CONNECTED) { + if (DBG) Log.d(TAG, "replacing old mInterfaceName (" + + mInterfaceName + ") with " + + intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY) + + " for " + mApnType); + mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); + } + return; + } + } else { + return; + } - public void restoreDefaultRoute() { - // 0 is not a valid address for a gateway - if (mInterfaceName != null && mDefaultGatewayAddr != 0) { - NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr); + if (DBG) Log.d(TAG, mApnType + " Received state= " + state + ", old= " + + mMobileDataState + ", reason= " + + (reason == null ? "(unspecified)" : reason) + + ", apnTypeList= " + apnTypeList); + + if (mMobileDataState != state) { + mMobileDataState = state; + switch (state) { + case DISCONNECTED: + if(isTeardownRequested()) { + mEnabled = false; + setTeardownRequested(false); + } + + setDetailedState(DetailedState.DISCONNECTED, reason, apnName); + if (mInterfaceName != null) { + NetworkUtils.resetConnections(mInterfaceName); + } + // can't do this here - ConnectivityService needs it to clear stuff + // it's ok though - just leave it to be refreshed next time + // we connect. + //if (DBG) Log.d(TAG, "clearing mInterfaceName for "+ mApnType + + // " as it DISCONNECTED"); + //mInterfaceName = null; + //mDefaultGatewayAddr = 0; + break; + case CONNECTING: + setDetailedState(DetailedState.CONNECTING, reason, apnName); + break; + case SUSPENDED: + setDetailedState(DetailedState.SUSPENDED, reason, apnName); + break; + case CONNECTED: + mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY); + if (mInterfaceName == null) { + Log.d(TAG, "CONNECTED event did not supply interface name."); + } + setDetailedState(DetailedState.CONNECTED, reason, apnName); + break; + } + } + } else if (intent.getAction(). + equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) { + mEnabled = false; + String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY); + String apnName = intent.getStringExtra(Phone.DATA_APN_KEY); + if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" + + reason == null ? "" : "(" + reason + ")"); + setDetailedState(DetailedState.FAILED, reason, apnName); + } + TelephonyManager tm = TelephonyManager.getDefault(); + setRoamingStatus(tm.isNetworkRoaming()); + setSubtype(tm.getNetworkType(), tm.getNetworkTypeName()); + } } } @@ -218,15 +256,6 @@ public class MobileDataStateTracker extends NetworkStateTracker { return false; } - /** - * Return the IP addresses of the DNS servers available for the mobile data - * network interface. - * @return a list of DNS addresses, with no holes. - */ - public String[] getNameServers() { - return getNameServerList(sDnsPropNames); - } - /** * {@inheritDoc} * The mobile data network subtype indicates what generation network technology is in effect, @@ -254,9 +283,21 @@ public class MobileDataStateTracker extends NetworkStateTracker { case TelephonyManager.NETWORK_TYPE_UMTS: networkTypeStr = "umts"; break; + case TelephonyManager.NETWORK_TYPE_HSDPA: + networkTypeStr = "hsdpa"; + break; + case TelephonyManager.NETWORK_TYPE_HSUPA: + networkTypeStr = "hsupa"; + break; + case TelephonyManager.NETWORK_TYPE_HSPA: + networkTypeStr = "hspa"; + break; case TelephonyManager.NETWORK_TYPE_CDMA: networkTypeStr = "cdma"; break; + case TelephonyManager.NETWORK_TYPE_1xRTT: + networkTypeStr = "1xrtt"; + break; case TelephonyManager.NETWORK_TYPE_EVDO_0: networkTypeStr = "evdo"; break; @@ -273,54 +314,58 @@ public class MobileDataStateTracker extends NetworkStateTracker { */ @Override public boolean teardown() { - getPhoneService(false); - /* - * If the phone process has crashed in the past, we'll get a - * RemoteException and need to re-reference the service. - */ - for (int retry = 0; retry < 2; retry++) { - if (mPhoneService == null) { - Log.w(TAG, - "Ignoring mobile data teardown request because could not acquire PhoneService"); - break; - } - - try { - return mPhoneService.disableDataConnectivity(); - } catch (RemoteException e) { - if (retry == 0) getPhoneService(true); - } - } - - Log.w(TAG, "Failed to tear down mobile data connectivity"); - return false; + // since we won't get a notification currently (TODO - per APN notifications) + // we won't get a disconnect message until all APN's on the current connection's + // APN list are disabled. That means privateRoutes for DNS and such will remain on - + // not a problem since that's all shared with whatever other APN is still on, but + // ugly. + setTeardownRequested(true); + return (setEnableApn(mApnType, false) != Phone.APN_REQUEST_FAILED); } /** * Re-enable mobile data connectivity after a {@link #teardown()}. */ public boolean reconnect() { - getPhoneService(false); - /* - * If the phone process has crashed in the past, we'll get a - * RemoteException and need to re-reference the service. - */ - for (int retry = 0; retry < 2; retry++) { - if (mPhoneService == null) { - Log.w(TAG, - "Ignoring mobile data connect request because could not acquire PhoneService"); + setTeardownRequested(false); + switch (setEnableApn(mApnType, true)) { + case Phone.APN_ALREADY_ACTIVE: + // TODO - remove this when we get per-apn notifications + mEnabled = true; + // need to set self to CONNECTING so the below message is handled. + mMobileDataState = Phone.DataState.CONNECTING; + setDetailedState(DetailedState.CONNECTING, Phone.REASON_APN_CHANGED, null); + //send out a connected message + Intent intent = new Intent(TelephonyIntents. + ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + intent.putExtra(Phone.STATE_KEY, Phone.DataState.CONNECTED.toString()); + intent.putExtra(Phone.STATE_CHANGE_REASON_KEY, Phone.REASON_APN_CHANGED); + intent.putExtra(Phone.DATA_APN_TYPES_KEY, mApnType); + intent.putExtra(Phone.DATA_APN_KEY, mApnName); + intent.putExtra(Phone.DATA_IFACE_NAME_KEY, mInterfaceName); + intent.putExtra(Phone.NETWORK_UNAVAILABLE_KEY, false); + if (mStateReceiver != null) mStateReceiver.onReceive(mContext, intent); + break; + case Phone.APN_REQUEST_STARTED: + mEnabled = true; + // no need to do anything - we're already due some status update intents + break; + case Phone.APN_REQUEST_FAILED: + if (mPhoneService == null && mApnType == Phone.APN_TYPE_DEFAULT) { + // on startup we may try to talk to the phone before it's ready + // just leave mEnabled as it is for the default apn. + return false; + } + // else fall through + case Phone.APN_TYPE_NOT_AVAILABLE: + mEnabled = false; + break; + default: + Log.e(TAG, "Error in reconnect - unexpected response."); + mEnabled = false; break; - } - - try { - return mPhoneService.enableDataConnectivity(); - } catch (RemoteException e) { - if (retry == 0) getPhoneService(true); - } } - - Log.w(TAG, "Failed to set up mobile data connectivity"); - return false; + return mEnabled; } /** @@ -374,14 +419,7 @@ public class MobileDataStateTracker extends NetworkStateTracker { *

    */ public int startUsingNetworkFeature(String feature, int callingPid, int callingUid) { - if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) { - mLastCallingPid = callingPid; - return setEnableApn(Phone.APN_TYPE_MMS, true); - } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_SUPL)) { - return setEnableApn(Phone.APN_TYPE_SUPL, true); - } else { - return -1; - } + return -1; } /** @@ -397,13 +435,7 @@ public class MobileDataStateTracker extends NetworkStateTracker { * the value {@code -1} always indicates failure. */ public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid) { - if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) { - return setEnableApn(Phone.APN_TYPE_MMS, false); - } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_SUPL)) { - return setEnableApn(Phone.APN_TYPE_SUPL, false); - } else { - return -1; - } + return -1; } /** @@ -415,10 +447,11 @@ public class MobileDataStateTracker extends NetworkStateTracker { */ @Override public boolean requestRouteToHost(int hostAddress) { + if (DBG) { + Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress) + + " for " + mApnType + "(" + mInterfaceName + ")"); + } if (mInterfaceName != null && hostAddress != -1) { - if (DBG) { - Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress)); - } return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0; } else { return false; @@ -433,43 +466,6 @@ public class MobileDataStateTracker extends NetworkStateTracker { return sb.toString(); } - private void setupDnsProperties() { - mDnsServers.clear(); - // Set up per-process DNS server list on behalf of the MMS process - int i = 1; - if (mInterfaceName != null) { - for (String propName : sDnsPropNames) { - if (propName.indexOf(mInterfaceName) != -1) { - String propVal = SystemProperties.get(propName); - if (propVal != null && propVal.length() != 0 && !propVal.equals("0.0.0.0")) { - mDnsServers.add(propVal); - if (mLastCallingPid != -1) { - SystemProperties.set("net.dns" + i + "." + mLastCallingPid, propVal); - } - ++i; - } - } - } - } - if (i == 1) { - Log.d(TAG, "DNS server addresses are not known."); - } else if (mLastCallingPid != -1) { - /* - * Bump the property that tells the name resolver library - * to reread the DNS server list from the properties. - */ - String propVal = SystemProperties.get("net.dnschange"); - if (propVal.length() != 0) { - try { - int n = Integer.parseInt(propVal); - SystemProperties.set("net.dnschange", "" + (n+1)); - } catch (NumberFormatException e) { - } - } - } - mLastCallingPid = -1; - } - /** * Internal method supporting the ENABLE_MMS feature. * @param apnType the type of APN to be enabled or disabled (e.g., mms) diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 9f539373b6af1d5650ccecdfb6268b1c2d1aac28..649cb8cfc850724a7d2b1e23219dc3c93819e6bc 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -131,7 +131,7 @@ public class NetworkInfo implements Parcelable { mSubtypeName = subtypeName; setDetailedState(DetailedState.IDLE, null, null); mState = State.UNKNOWN; - mIsAvailable = true; + mIsAvailable = false; // until we're told otherwise, assume unavailable mIsRoaming = false; } diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 37087aca23a91d0e8d45cea2561987ce7314df7d..1fb014452f4cfbbf9cb41208bc052800a89113fc 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -27,6 +27,7 @@ import android.text.TextUtils; import android.util.Config; import android.util.Log; + /** * Each subclass of this class keeps track of the state of connectivity * of a network interface. All state information for a network should @@ -40,11 +41,16 @@ public abstract class NetworkStateTracker extends Handler { protected NetworkInfo mNetworkInfo; protected Context mContext; protected Handler mTarget; + protected String mInterfaceName; + protected String[] mDnsPropNames; + private boolean mPrivateDnsRouteSet; + protected int mDefaultGatewayAddr; + private boolean mDefaultRouteSet; private boolean mTeardownRequested; - private static boolean DBG = Config.LOGV; + private static boolean DBG = true; private static final String TAG = "NetworkStateTracker"; - + public static final int EVENT_STATE_CHANGED = 1; public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2; /** @@ -56,6 +62,7 @@ public abstract class NetworkStateTracker extends Handler { public static final int EVENT_CONFIGURATION_CHANGED = 4; public static final int EVENT_ROAMING_CHANGED = 5; public static final int EVENT_NETWORK_SUBTYPE_CHANGED = 6; + public static final int EVENT_RESTORE_DEFAULT_NETWORK = 7; public NetworkStateTracker(Context context, Handler target, @@ -67,6 +74,7 @@ public abstract class NetworkStateTracker extends Handler { mContext = context; mTarget = target; mTeardownRequested = false; + this.mNetworkInfo = new NetworkInfo(networkType, subType, typeName, subtypeName); } @@ -74,19 +82,21 @@ public abstract class NetworkStateTracker extends Handler { return mNetworkInfo; } - /** - * Return the list of DNS servers associated with this network. - * @return a list of the IP addresses of the DNS servers available - * for the network. - */ - public abstract String[] getNameServers(); - /** * Return the system properties name associated with the tcp buffer sizes * for this network. */ public abstract String getTcpBufferSizesPropName(); + /** + * Return the IP addresses of the DNS servers available for the mobile data + * network interface. + * @return a list of DNS addresses, with no holes. + */ + public String[] getNameServers() { + return getNameServerList(mDnsPropNames); + } + /** * Return the IP addresses of the DNS servers available for this * network interface. @@ -112,6 +122,59 @@ public abstract class NetworkStateTracker extends Handler { return dnsAddresses; } + public void addPrivateDnsRoutes() { + if (DBG) { + Log.d(TAG, "addPrivateDnsRoutes for " + this + + "(" + mInterfaceName + ") - mPrivateDnsRouteSet = "+mPrivateDnsRouteSet); + } + if (mInterfaceName != null && !mPrivateDnsRouteSet) { + for (String addrString : getNameServers()) { + int addr = NetworkUtils.lookupHost(addrString); + if (addr != -1 && addr != 0) { + if (DBG) Log.d(TAG, " adding "+addrString+" ("+addr+")"); + NetworkUtils.addHostRoute(mInterfaceName, addr); + } + } + mPrivateDnsRouteSet = true; + } + } + + public void removePrivateDnsRoutes() { + // TODO - we should do this explicitly but the NetUtils api doesnt + // support this yet - must remove all. No worse than before + if (mInterfaceName != null && mPrivateDnsRouteSet) { + if (DBG) { + Log.d(TAG, "removePrivateDnsRoutes for " + mNetworkInfo.getTypeName() + + " (" + mInterfaceName + ")"); + } + NetworkUtils.removeHostRoutes(mInterfaceName); + mPrivateDnsRouteSet = false; + } + } + + public void addDefaultRoute() { + if ((mInterfaceName != null) && (mDefaultGatewayAddr != 0) && + mDefaultRouteSet == false) { + if (DBG) { + Log.d(TAG, "addDefaultRoute for " + mNetworkInfo.getTypeName() + + " (" + mInterfaceName + "), GatewayAddr=" + mDefaultGatewayAddr); + } + NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr); + mDefaultRouteSet = true; + } + } + + public void removeDefaultRoute() { + if (mInterfaceName != null && mDefaultRouteSet == true) { + if (DBG) { + Log.d(TAG, "removeDefaultRoute for " + mNetworkInfo.getTypeName() + " (" + + mInterfaceName + ")"); + } + NetworkUtils.removeDefaultRoute(mInterfaceName); + mDefaultRouteSet = false; + } + } + /** * Reads the network specific TCP buffer sizes from SystemProperties * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system @@ -209,6 +272,7 @@ public abstract class NetworkStateTracker extends Handler { * @param extraInfo optional {@code String} providing extra information about the state change */ public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) { + if (DBG) Log.d(TAG, "setDetailed state, old ="+mNetworkInfo.getDetailedState()+" and new state="+state); if (state != mNetworkInfo.getDetailedState()) { boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING); String lastReason = mNetworkInfo.getReason(); diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 115364872955d47e10064d816ce78233ed598a29..a3ae01b5597b6cd6dde7d1b99d2e7290781bd9f4 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -25,6 +25,9 @@ import java.net.UnknownHostException; * {@hide} */ public class NetworkUtils { + /** Bring the named network interface up. */ + public native static int enableInterface(String interfaceName); + /** Bring the named network interface down. */ public native static int disableInterface(String interfaceName); diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 421d0130b8a0fb844b109d0f0e515e60990f7c1f..298be3bd4499104e0f7469439567f8ef49a09599 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -374,7 +374,7 @@ public abstract class Uri implements Parcelable, Comparable { /** * Creates a Uri which parses the given encoded URI string. * - * @param uriString an RFC 3296-compliant, encoded URI + * @param uriString an RFC 2396-compliant, encoded URI * @throws NullPointerException if uriString is null * @return Uri for this given uri string */ diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java index f4a2a6a94307816d8785b1eeabdf6be4d0b23b80..f4ae66ac5d05bc280a828713371181e9fa637e2a 100644 --- a/core/java/android/net/WebAddress.java +++ b/core/java/android/net/WebAddress.java @@ -54,7 +54,7 @@ public class WebAddress { static Pattern sAddressPattern = Pattern.compile( /* scheme */ "(?:(http|HTTP|https|HTTPS|file|FILE)\\:\\/\\/)?" + /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" + - /* host */ "([-A-Za-z0-9%]+(?:\\.[-A-Za-z0-9%]+)*)?" + + /* host */ "([-A-Za-z0-9%_]+(?:\\.[-A-Za-z0-9%_]+)*|\\[[0-9a-fA-F:\\.]+\\])?" + /* port */ "(?:\\:([0-9]+))?" + /* path */ "(\\/?.*)?"); diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 563634fd4690a0e107b3300f0264ae38a105fcb3..2d39e3905bef2352f49c7415d9c7f991a1ce0295 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -251,9 +251,7 @@ abstract class Connection { pipe.addLast(req); } exception = null; - state = (clearPipe(pipe) || - !mConnectionManager.isNetworkConnected()) ? - DONE : SEND; + state = clearPipe(pipe) ? DONE : SEND; minPipe = maxPipe = 1; break; } @@ -314,9 +312,7 @@ abstract class Connection { mHttpContext.removeAttribute(HTTP_CONNECTION); clearPipe(pipe); minPipe = maxPipe = 1; - /* If network active continue to service this queue */ - state = mConnectionManager.isNetworkConnected() ? - SEND : DONE; + state = SEND; } break; } @@ -408,8 +404,7 @@ abstract class Connection { if (error == EventHandler.OK) { return true; } else { - if (mConnectionManager.isNetworkConnected() == false || - req.mFailCount < RETRY_REQUEST_LIMIT) { + if (req.mFailCount < RETRY_REQUEST_LIMIT) { // requeue mRequestFeeder.requeueRequest(req); req.mFailCount++; @@ -432,14 +427,13 @@ abstract class Connection { */ private boolean httpFailure(Request req, int errorId, Exception e) { boolean ret = true; - boolean networkConnected = mConnectionManager.isNetworkConnected(); // e.printStackTrace(); if (HttpLog.LOGV) HttpLog.v( "httpFailure() ******* " + e + " count " + req.mFailCount + - " networkConnected " + networkConnected + " " + mHost + " " + req.getUri()); + " " + mHost + " " + req.getUri()); - if (networkConnected && ++req.mFailCount >= RETRY_REQUEST_LIMIT) { + if (++req.mFailCount >= RETRY_REQUEST_LIMIT) { ret = false; String error; if (errorId < 0) { diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java index 8e759e24728581ded61cd211efd692fc41314029..0b30e589c4a71f281b1f88a7a10f77bba865a112 100644 --- a/core/java/android/net/http/ConnectionThread.java +++ b/core/java/android/net/http/ConnectionThread.java @@ -32,8 +32,8 @@ class ConnectionThread extends Thread { static final int WAIT_TICK = 1000; // Performance probe - long mStartThreadTime; long mCurrentThreadTime; + long mTotalThreadTime; private boolean mWaiting; private volatile boolean mRunning = true; @@ -69,12 +69,21 @@ class ConnectionThread extends Thread { */ public void run() { android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_DEFAULT + android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE); - mStartThreadTime = -1; - mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + // these are used to get performance data. When it is not in the timing, + // mCurrentThreadTime is 0. When it starts timing, mCurrentThreadTime is + // first set to -1, it will be set to the current thread time when the + // next request starts. + mCurrentThreadTime = 0; + mTotalThreadTime = 0; while (mRunning) { + if (mCurrentThreadTime == -1) { + mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + } + Request request; /* Get a request to process */ @@ -86,14 +95,14 @@ class ConnectionThread extends Thread { if (HttpLog.LOGV) HttpLog.v("ConnectionThread: Waiting for work"); mWaiting = true; try { - if (mStartThreadTime != -1) { - mCurrentThreadTime = SystemClock - .currentThreadTimeMillis(); - } mRequestFeeder.wait(); } catch (InterruptedException e) { } mWaiting = false; + if (mCurrentThreadTime != 0) { + mCurrentThreadTime = SystemClock + .currentThreadTimeMillis(); + } } } else { if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + @@ -123,6 +132,12 @@ class ConnectionThread extends Thread { mConnection.closeConnection(); } mConnection = null; + + if (mCurrentThreadTime > 0) { + long start = mCurrentThreadTime; + mCurrentThreadTime = SystemClock.currentThreadTimeMillis(); + mTotalThreadTime += mCurrentThreadTime - start; + } } } diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java index df4fff015dc0cad04ec012b5ce88190b967d8530..1b6568e7f11803f7e7f3ecd6a2735f90e24e002e 100644 --- a/core/java/android/net/http/Request.java +++ b/core/java/android/net/http/Request.java @@ -67,9 +67,6 @@ class Request { /** Set if I'm using a proxy server */ HttpHost mProxyHost; - /** True if request is .html, .js, .css */ - boolean mHighPriority; - /** True if request has been cancelled */ volatile boolean mCancelled = false; @@ -102,26 +99,29 @@ class Request { * @param eventHandler request will make progress callbacks on * this interface * @param headers reqeust headers - * @param highPriority true for .html, css, .cs */ Request(String method, HttpHost host, HttpHost proxyHost, String path, InputStream bodyProvider, int bodyLength, EventHandler eventHandler, - Map headers, boolean highPriority) { + Map headers) { mEventHandler = eventHandler; mHost = host; mProxyHost = proxyHost; mPath = path; - mHighPriority = highPriority; mBodyProvider = bodyProvider; mBodyLength = bodyLength; - if (bodyProvider == null) { + if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { mHttpRequest = new BasicHttpRequest(method, getUri()); } else { mHttpRequest = new BasicHttpEntityEnclosingRequest( method, getUri()); - setBodyProvider(bodyProvider, bodyLength); + // it is ok to have null entity for BasicHttpEntityEnclosingRequest. + // By using BasicHttpEntityEnclosingRequest, it will set up the + // correct content-length, content-type and content-encoding. + if (bodyProvider != null) { + setBodyProvider(bodyProvider, bodyLength); + } } addHeader(HOST_HEADER, getHostPort()); @@ -255,6 +255,8 @@ class Request { // process gzip content encoding Header contentEncoding = entity.getContentEncoding(); InputStream nis = null; + byte[] buf = null; + int count = 0; try { if (contentEncoding != null && contentEncoding.getValue().equals("gzip")) { @@ -265,9 +267,8 @@ class Request { /* accumulate enough data to make it worth pushing it * up the stack */ - byte[] buf = mConnection.getBuf(); + buf = mConnection.getBuf(); int len = 0; - int count = 0; int lowWater = buf.length / 2; while (len != -1) { len = nis.read(buf, count, buf.length - count); @@ -284,6 +285,10 @@ class Request { /* InflaterInputStream throws an EOFException when the server truncates gzipped content. Handle this case as we do truncated non-gzipped content: no error */ + if (count > 0) { + // if there is uncommited content, we should commit them + mEventHandler.data(buf, count); + } if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e); } catch(IOException e) { // don't throw if we have a non-OK status code @@ -346,7 +351,7 @@ class Request { * for debugging */ public String toString() { - return (mHighPriority ? "P*" : "") + mPath; + return mPath; } @@ -412,8 +417,7 @@ class Request { } return status >= HttpStatus.SC_OK && status != HttpStatus.SC_NO_CONTENT - && status != HttpStatus.SC_NOT_MODIFIED - && status != HttpStatus.SC_RESET_CONTENT; + && status != HttpStatus.SC_NOT_MODIFIED; } /** diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java index 6a97951fecbd47bbb0e92d39fec75138f1159e5e..190ae7ab63783a4bd04990848172b14d78a25af4 100644 --- a/core/java/android/net/http/RequestHandle.java +++ b/core/java/android/net/http/RequestHandle.java @@ -419,6 +419,6 @@ public class RequestHandle { mRequest = mRequestQueue.queueRequest( mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler, mBodyProvider, - mBodyLength, mRequest.mHighPriority).mRequest; + mBodyLength).mRequest; } } diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index 66d572288f1cd3dc14f8363e917cbcdadf81da06..875caa0341c800445b72ebda9e29611c2a743fd9 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -26,7 +26,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; -import android.net.NetworkConnectivityListener; import android.net.NetworkInfo; import android.net.Proxy; import android.net.WebAddress; @@ -50,153 +49,21 @@ import org.apache.http.HttpHost; */ public class RequestQueue implements RequestFeeder { - private Context mContext; /** * Requests, indexed by HttpHost (scheme, host, port) */ - private LinkedHashMap> mPending; - - /* Support for notifying a client when queue is empty */ - private boolean mClientWaiting = false; - - /** true if connected */ - boolean mNetworkConnected = true; + private final LinkedHashMap> mPending; + private final Context mContext; + private final ActivePool mActivePool; + private final ConnectivityManager mConnectivityManager; private HttpHost mProxyHost = null; private BroadcastReceiver mProxyChangeReceiver; - private ActivePool mActivePool; - /* default simultaneous connection count */ private static final int CONNECTION_COUNT = 4; - /** - * This intent broadcast when http is paused or unpaused due to - * net availability toggling - */ - public final static String HTTP_NETWORK_STATE_CHANGED_INTENT = - "android.net.http.NETWORK_STATE"; - public final static String HTTP_NETWORK_STATE_UP = "up"; - - /** - * Listen to platform network state. On a change, - * (1) kick stack on or off as appropriate - * (2) send an intent to my host app telling - * it what I've done - */ - private NetworkStateTracker mNetworkStateTracker; - class NetworkStateTracker { - - final static int EVENT_DATA_STATE_CHANGED = 100; - - Context mContext; - NetworkConnectivityListener mConnectivityListener; - NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED; - int mCurrentNetworkType; - - NetworkStateTracker(Context context) { - mContext = context; - } - - /** - * register for updates - */ - protected void enable() { - if (mConnectivityListener == null) { - /* - * Initializing the network type is really unnecessary, - * since as soon as we register with the NCL, we'll - * get a CONNECTED event for the active network, and - * we'll configure the HTTP proxy accordingly. However, - * as a fallback in case that doesn't happen for some - * reason, initializing to type WIFI would mean that - * we'd start out without a proxy. This seems better - * than thinking we have a proxy (which is probably - * private to the carrier network and therefore - * unreachable outside of that network) when we really - * shouldn't. - */ - mCurrentNetworkType = ConnectivityManager.TYPE_WIFI; - mConnectivityListener = new NetworkConnectivityListener(); - mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED); - mConnectivityListener.startListening(mContext); - } - } - - protected void disable() { - if (mConnectivityListener != null) { - mConnectivityListener.unregisterHandler(mHandler); - mConnectivityListener.stopListening(); - mConnectivityListener = null; - } - } - - private Handler mHandler = new Handler() { - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_DATA_STATE_CHANGED: - networkStateChanged(); - break; - } - } - }; - - int getCurrentNetworkType() { - return mCurrentNetworkType; - } - - void networkStateChanged() { - if (mConnectivityListener == null) - return; - - - NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState(); - NetworkInfo info = mConnectivityListener.getNetworkInfo(); - if (info == null) { - /** - * We've been seeing occasional NPEs here. I believe recent changes - * have made this impossible, but in the interest of being totally - * paranoid, check and log this here. - */ - HttpLog.v("NetworkStateTracker: connectivity broadcast" - + " has null network info - ignoring"); - return; - } - NetworkInfo.State state = info.getState(); - - if (HttpLog.LOGV) { - HttpLog.v("NetworkStateTracker " + info.getTypeName() + - " state= " + state + " last= " + mLastNetworkState + - " connectivityState= " + connectivityState.toString()); - } - - boolean newConnection = - state != mLastNetworkState && state == NetworkInfo.State.CONNECTED; - - if (state == NetworkInfo.State.CONNECTED) { - mCurrentNetworkType = info.getType(); - setProxyConfig(); - } - - mLastNetworkState = state; - if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) { - setNetworkState(false); - broadcastState(false); - } else if (newConnection) { - setNetworkState(true); - broadcastState(true); - } - - } - - void broadcastState(boolean connected) { - Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT); - intent.putExtra(HTTP_NETWORK_STATE_UP, connected); - mContext.sendBroadcast(intent); - } - } - /** * This class maintains active connection threads */ @@ -233,10 +100,6 @@ public class RequestQueue implements RequestFeeder { } } - public boolean isNetworkConnected() { - return mNetworkConnected; - } - void startConnectionThread() { synchronized (RequestQueue.this) { RequestQueue.this.notify(); @@ -245,7 +108,9 @@ public class RequestQueue implements RequestFeeder { public void startTiming() { for (int i = 0; i < mConnectionCount; i++) { - mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime; + ConnectionThread rt = mThreads[i]; + rt.mCurrentThreadTime = -1; + rt.mTotalThreadTime = 0; } mTotalRequest = 0; mTotalConnection = 0; @@ -255,12 +120,14 @@ public class RequestQueue implements RequestFeeder { int totalTime = 0; for (int i = 0; i < mConnectionCount; i++) { ConnectionThread rt = mThreads[i]; - totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime); - rt.mStartThreadTime = -1; + if (rt.mCurrentThreadTime != -1) { + totalTime += rt.mTotalThreadTime; + } + rt.mCurrentThreadTime = 0; } Log.d("Http", "Http thread used " + totalTime + " ms " + " for " + mTotalRequest + " requests and " + mTotalConnection - + " connections"); + + " new connections"); } void logState() { @@ -348,6 +215,9 @@ public class RequestQueue implements RequestFeeder { mActivePool = new ActivePool(connectionCount); mActivePool.startup(); + + mConnectivityManager = (ConnectivityManager) + context.getSystemService(Context.CONNECTIVITY_SERVICE); } /** @@ -367,18 +237,6 @@ public class RequestQueue implements RequestFeeder { mContext.registerReceiver(mProxyChangeReceiver, new IntentFilter(Proxy.PROXY_CHANGE_ACTION)); } - - /* Network state notification is broken on the simulator - don't register for notifications on SIM */ - String device = SystemProperties.get("ro.product.device"); - boolean simulation = TextUtils.isEmpty(device); - - if (!simulation) { - if (mNetworkStateTracker == null) { - mNetworkStateTracker = new NetworkStateTracker(mContext); - } - mNetworkStateTracker.enable(); - } } /** @@ -388,10 +246,6 @@ public class RequestQueue implements RequestFeeder { public synchronized void disablePlatformNotifications() { if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network"); - if (mNetworkStateTracker != null) { - mNetworkStateTracker.disable(); - } - if (mProxyChangeReceiver != null) { mContext.unregisterReceiver(mProxyChangeReceiver); mProxyChangeReceiver = null; @@ -403,7 +257,8 @@ public class RequestQueue implements RequestFeeder { * synchronize setting the proxy */ private synchronized void setProxyConfig() { - if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) { + NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + if (info != null && info.getType() == ConnectivityManager.TYPE_WIFI) { mProxyHost = null; } else { String host = Proxy.getHost(mContext); @@ -434,16 +289,14 @@ public class RequestQueue implements RequestFeeder { * data. Callbacks will be made on the supplied instance. * @param bodyProvider InputStream providing HTTP body, null if none * @param bodyLength length of body, must be 0 if bodyProvider is null - * @param highPriority If true, queues before low priority - * requests if possible */ public RequestHandle queueRequest( String url, String method, Map headers, EventHandler eventHandler, - InputStream bodyProvider, int bodyLength, boolean highPriority) { + InputStream bodyProvider, int bodyLength) { WebAddress uri = new WebAddress(url); return queueRequest(url, uri, method, headers, eventHandler, - bodyProvider, bodyLength, highPriority); + bodyProvider, bodyLength); } /** @@ -456,14 +309,11 @@ public class RequestQueue implements RequestFeeder { * data. Callbacks will be made on the supplied instance. * @param bodyProvider InputStream providing HTTP body, null if none * @param bodyLength length of body, must be 0 if bodyProvider is null - * @param highPriority If true, queues before low priority - * requests if possible */ public RequestHandle queueRequest( String url, WebAddress uri, String method, Map headers, EventHandler eventHandler, - InputStream bodyProvider, int bodyLength, - boolean highPriority) { + InputStream bodyProvider, int bodyLength) { if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri); @@ -478,9 +328,9 @@ public class RequestQueue implements RequestFeeder { // set up request req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, - bodyLength, eventHandler, headers, highPriority); + bodyLength, eventHandler, headers); - queueRequest(req, highPriority); + queueRequest(req, false); mActivePool.mTotalRequest++; @@ -492,19 +342,6 @@ public class RequestQueue implements RequestFeeder { req); } - /** - * Called by the NetworkStateTracker -- updates when network connectivity - * is lost/restored. - * - * If isNetworkConnected is true, start processing requests - */ - public void setNetworkState(boolean isNetworkConnected) { - if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected); - mNetworkConnected = isNetworkConnected; - if (isNetworkConnected) - mActivePool.startConnectionThread(); - } - /** * @return true iff there are any non-active requests pending */ @@ -550,7 +387,7 @@ public class RequestQueue implements RequestFeeder { public synchronized Request getRequest() { Request ret = null; - if (mNetworkConnected && !mPending.isEmpty()) { + if (!mPending.isEmpty()) { ret = removeFirst(mPending); } if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret); @@ -563,7 +400,7 @@ public class RequestQueue implements RequestFeeder { public synchronized Request getRequest(HttpHost host) { Request ret = null; - if (mNetworkConnected && mPending.containsKey(host)) { + if (mPending.containsKey(host)) { LinkedList reqList = mPending.get(host); ret = reqList.removeFirst(); if (reqList.isEmpty()) { @@ -639,7 +476,6 @@ public class RequestQueue implements RequestFeeder { * This interface is exposed to each connection */ interface ConnectionManager { - boolean isNetworkConnected(); HttpHost getProxyHost(); Connection getConnection(Context context, HttpHost host); boolean recycleConnection(HttpHost host, Connection connection); diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index abfb27412de0c333d45d451c1ff4c5867cbe5373..7d2c6988958174a2980f4e98ea9c48ca82e5f8e1 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -413,6 +413,7 @@ public abstract class AsyncTask { } private void finish(Result result) { + if (isCancelled()) result = null; onPostExecute(result); mStatus = Status.FINISHED; } diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 8f1a756b295ce617c87148409b586afcc386df08..44b73c52dd052cc662411eb69fd13a494e50c757 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -18,10 +18,73 @@ package android.os; /** * The BatteryManager class contains strings and constants used for values - * in the ACTION_BATTERY_CHANGED Intent. + * in the {@link android.content.Intent#ACTION_BATTERY_CHANGED} Intent. */ public class BatteryManager { - + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current status constant. + */ + public static final String EXTRA_STATUS = "status"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current health constant. + */ + public static final String EXTRA_HEALTH = "health"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * boolean indicating whether a battery is present. + */ + public static final String EXTRA_PRESENT = "present"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer field containing the current battery level, from 0 to + * {@link #EXTRA_SCALE}. + */ + public static final String EXTRA_LEVEL = "level"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the maximum battery level. + */ + public static final String EXTRA_SCALE = "scale"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the resource ID of a small status bar icon + * indicating the current battery state. + */ + public static final String EXTRA_ICON_SMALL = "icon-small"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer indicating whether the device is plugged in to a power + * source; 0 means it is on battery, other constants are different + * types of power sources. + */ + public static final String EXTRA_PLUGGED = "plugged"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current battery voltage level. + */ + public static final String EXTRA_VOLTAGE = "voltage"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current battery temperature. + */ + public static final String EXTRA_TEMPERATURE = "temperature"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * String describing the technology of the current battery. + */ + public static final String EXTRA_TECHNOLOGY = "technology"; + // values for "status" field in the ACTION_BATTERY_CHANGED Intent public static final int BATTERY_STATUS_UNKNOWN = 1; public static final int BATTERY_STATUS_CHARGING = 2; diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index e203fd59025827041015a1ae184a5f825a6c8eb4..b706c5c1cae40c399d06fa8a203e8f13a3c12afa 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -130,6 +130,7 @@ public abstract class BatteryStats implements Parcelable { private static final String MISC_DATA = "m"; private static final String SCREEN_BRIGHTNESS_DATA = "br"; private static final String SIGNAL_STRENGTH_TIME_DATA = "sgt"; + private static final String SIGNAL_SCANNING_TIME_DATA = "sst"; private static final String SIGNAL_STRENGTH_COUNT_DATA = "sgc"; private static final String DATA_CONNECTION_TIME_DATA = "dct"; private static final String DATA_CONNECTION_COUNT_DATA = "dcc"; @@ -314,6 +315,15 @@ public abstract class BatteryStats implements Parcelable { * @return foreground cpu time in microseconds */ public abstract long getForegroundTime(int which); + + /** + * Returns the approximate cpu time spent in microseconds, at a certain CPU speed. + * @param speedStep the index of the CPU speed. This is not the actual speed of the + * CPU. + * @param which one of STATS_TOTAL, STATS_LAST, STATS_CURRENT or STATS_UNPLUGGED + * @see BatteryStats#getCpuSpeedSteps() + */ + public abstract long getTimeAtCpuSpeedStep(int speedStep, int which); } /** @@ -430,6 +440,15 @@ public abstract class BatteryStats implements Parcelable { public abstract long getPhoneSignalStrengthTime(int strengthBin, long batteryRealtime, int which); + /** + * Returns the time in microseconds that the phone has been trying to + * acquire a signal. + * + * {@hide} + */ + public abstract long getPhoneSignalScanningTime( + long batteryRealtime, int which); + /** * Returns the number of times the phone has entered the given signal strength. * @@ -573,6 +592,9 @@ public abstract class BatteryStats implements Parcelable { public abstract Map getKernelWakelockStats(); + /** Returns the number of different speeds that the CPU can run at */ + public abstract int getCpuSpeedSteps(); + private final static void formatTimeRaw(StringBuilder out, long seconds) { long days = seconds / (60 * 60 * 24); if (days != 0) { @@ -811,6 +833,8 @@ public abstract class BatteryStats implements Parcelable { args[i] = getPhoneSignalStrengthTime(i, batteryRealtime, which) / 1000; } dumpLine(pw, 0 /* uid */, category, SIGNAL_STRENGTH_TIME_DATA, args); + dumpLine(pw, 0 /* uid */, category, SIGNAL_SCANNING_TIME_DATA, + getPhoneSignalScanningTime(batteryRealtime, which) / 1000); for (int i=0; iApplications targeting this or a later release will get these * new changes in behavior:

    @@ -132,6 +136,31 @@ public class Build { * */ public static final int DONUT = 4; + + /** + * November 2009: Android 2.0 + * + *

    Applications targeting this or a later release will get these + * new changes in behavior:

    + *
      + *
    • The {@link android.app.Service#onStartCommand + * Service.onStartCommand} function will return the new + * {@link android.app.Service#START_STICKY} behavior instead of the + * old compatibility {@link android.app.Service#START_STICKY_COMPATIBILITY}. + *
    • The {@link android.app.Activity} class will now execute back + * key presses on the key up instead of key down, to be able to detect + * canceled presses from virtual keys. + *
    • The {@link android.widget.TabWidget} class will use a new color scheme + * for tabs. In the new scheme, the foreground tab has a medium gray background + * the background tabs have a dark gray background. + *
    + */ + public static final int ECLAIR = 5; + + /** + * Current work on Eclair MR1. + */ + public static final int ECLAIR_MR1 = 6; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index d40ea6b300c5516c87987402195a3b809d72336c..b4f64b6d4a29bf37592a8902d5a7b9c04897f574 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -104,7 +104,7 @@ public final class Debug * This class is used to retrieved various statistics about the memory mappings for this * process. The returns info broken down by dalvik, native, and other. All results are in kB. */ - public static class MemoryInfo { + public static class MemoryInfo implements Parcelable { /** The proportional set size for dalvik. */ public int dalvikPss; /** The private dirty pages used by dalvik. */ @@ -125,6 +125,71 @@ public final class Debug public int otherPrivateDirty; /** The shared dirty pages used by everything else. */ public int otherSharedDirty; + + public MemoryInfo() { + } + + /** + * Return total PSS memory usage in kB. + */ + public int getTotalPss() { + return dalvikPss + nativePss + otherPss; + } + + /** + * Return total private dirty memory usage in kB. + */ + public int getTotalPrivateDirty() { + return dalvikPrivateDirty + nativePrivateDirty + otherPrivateDirty; + } + + /** + * Return total shared dirty memory usage in kB. + */ + public int getTotalSharedDirty() { + return dalvikSharedDirty + nativeSharedDirty + otherSharedDirty; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(dalvikPss); + dest.writeInt(dalvikPrivateDirty); + dest.writeInt(dalvikSharedDirty); + dest.writeInt(nativePss); + dest.writeInt(nativePrivateDirty); + dest.writeInt(nativeSharedDirty); + dest.writeInt(otherPss); + dest.writeInt(otherPrivateDirty); + dest.writeInt(otherSharedDirty); + } + + public void readFromParcel(Parcel source) { + dalvikPss = source.readInt(); + dalvikPrivateDirty = source.readInt(); + dalvikSharedDirty = source.readInt(); + nativePss = source.readInt(); + nativePrivateDirty = source.readInt(); + nativeSharedDirty = source.readInt(); + otherPss = source.readInt(); + otherPrivateDirty = source.readInt(); + otherSharedDirty = source.readInt(); + } + + public static final Creator CREATOR = new Creator() { + public MemoryInfo createFromParcel(Parcel source) { + return new MemoryInfo(source); + } + public MemoryInfo[] newArray(int size) { + return new MemoryInfo[size]; + } + }; + + private MemoryInfo(Parcel source) { + readFromParcel(source); + } } @@ -555,6 +620,13 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo */ public static native void getMemoryInfo(MemoryInfo memoryInfo); + /** + * Note: currently only works when the requested pid has the same UID + * as the caller. + * @hide + */ + public static native void getMemoryInfo(int pid, MemoryInfo memoryInfo); + /** * Establish an object allocation limit in the current thread. Useful * for catching regressions in code that is expected to operate @@ -661,6 +733,25 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo */ public static final native int getBinderDeathObjectCount(); + /** + * Primes the register map cache. + * + * Only works for classes in the bootstrap class loader. Does not + * cause classes to be loaded if they're not already present. + * + * The classAndMethodDesc argument is a concatentation of the VM-internal + * class descriptor, method name, and method descriptor. Examples: + * Landroid/os/Looper;.loop:()V + * Landroid/app/ActivityThread;.main:([Ljava/lang/String;)V + * + * @param classAndMethodDesc the method to prepare + * + * @hide + */ + public static final boolean cacheRegisterMap(String classAndMethodDesc) { + return VMDebug.cacheRegisterMap(classAndMethodDesc); + } + /** * API for gathering and querying instruction counts. * diff --git a/core/java/android/os/Exec.java b/core/java/android/os/Exec.java deleted file mode 100644 index a50d5fe45b5ecea41999c5fb263d7d9a28cf192d..0000000000000000000000000000000000000000 --- a/core/java/android/os/Exec.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -import java.io.FileDescriptor; - -/** - * @hide - * Tools for executing commands. Not for public consumption. - */ - -public class Exec -{ - /** - * @param cmd The command to execute - * @param arg0 The first argument to the command, may be null - * @param arg1 the second argument to the command, may be null - * @return the file descriptor of the started process. - * - */ - public static FileDescriptor createSubprocess( - String cmd, String arg0, String arg1) { - return createSubprocess(cmd, arg0, arg1, null); - } - - /** - * @param cmd The command to execute - * @param arg0 The first argument to the command, may be null - * @param arg1 the second argument to the command, may be null - * @param processId A one-element array to which the process ID of the - * started process will be written. - * @return the file descriptor of the started process. - * - */ - public static native FileDescriptor createSubprocess( - String cmd, String arg0, String arg1, int[] processId); - - public static native void setPtyWindowSize(FileDescriptor fd, - int row, int col, int xpixel, int ypixel); - /** - * Causes the calling thread to wait for the process associated with the - * receiver to finish executing. - * - * @return The exit value of the Process being waited on - * - */ - public static native int waitFor(int processId); -} - diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index d9804eab761e022e9232f21b42c2b6465abefe94..38d252ee0b8fcb8c161669313634b05ad5cd2ff9 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -25,22 +25,35 @@ import java.util.ArrayList; import java.util.HashMap; public abstract class FileObserver { - public static final int ACCESS = 0x00000001; /* File was accessed */ - public static final int MODIFY = 0x00000002; /* File was modified */ - public static final int ATTRIB = 0x00000004; /* Metadata changed */ - public static final int CLOSE_WRITE = 0x00000008; /* Writtable file was closed */ - public static final int CLOSE_NOWRITE = 0x00000010; /* Unwrittable file closed */ - public static final int OPEN = 0x00000020; /* File was opened */ - public static final int MOVED_FROM = 0x00000040; /* File was moved from X */ - public static final int MOVED_TO = 0x00000080; /* File was moved to Y */ - public static final int CREATE = 0x00000100; /* Subfile was created */ - public static final int DELETE = 0x00000200; /* Subfile was deleted */ - public static final int DELETE_SELF = 0x00000400; /* Self was deleted */ - public static final int MOVE_SELF = 0x00000800; /* Self was moved */ + /** File was accessed */ + public static final int ACCESS = 0x00000001; + /** File was modified */ + public static final int MODIFY = 0x00000002; + /** Metadata changed */ + public static final int ATTRIB = 0x00000004; + /** Writable file was closed */ + public static final int CLOSE_WRITE = 0x00000008; + /** Unwrittable file closed */ + public static final int CLOSE_NOWRITE = 0x00000010; + /** File was opened */ + public static final int OPEN = 0x00000020; + /** File was moved from X */ + public static final int MOVED_FROM = 0x00000040; + /** File was moved to Y */ + public static final int MOVED_TO = 0x00000080; + /** Subfile was created */ + public static final int CREATE = 0x00000100; + /** Subfile was deleted */ + public static final int DELETE = 0x00000200; + /** Self was deleted */ + public static final int DELETE_SELF = 0x00000400; + /** Self was moved */ + public static final int MOVE_SELF = 0x00000800; + public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE | DELETE_SELF | MOVE_SELF; - + private static final String LOG_TAG = "FileObserver"; private static class ObserverThread extends Thread { diff --git a/core/java/android/os/HandlerStateMachine.java b/core/java/android/os/HandlerStateMachine.java index d004a25b9259ec0346efa6da7a7640304030fb26..9e7902b5c7e44eaefff5d848e17ef56082984e0a 100644 --- a/core/java/android/os/HandlerStateMachine.java +++ b/core/java/android/os/HandlerStateMachine.java @@ -56,22 +56,22 @@ import android.util.LogPrinter; } class S1 extends HandlerState { - @Override public void enter(Message message) { + &#064;Override public void enter(Message message) { } - @Override public void processMessage(Message message) { + &#064;Override public void processMessage(Message message) { deferMessage(message); if (message.what == TEST_WHAT_2) { transitionTo(mS2); } } - @Override public void exit(Message message) { + &#064;Override public void exit(Message message) { } } class S2 extends HandlerState { - @Override public void processMessage(Message message) { + &#064;Override public void processMessage(Message message) { // Do some processing if (message.what == TEST_WHAT_2) { transtionTo(mS1); diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java index 0ce86db972c3d285c16b567d768a681ac16bed72..65301e41bb7f273d07b4ac739d7c8f9617fc6462 100644 --- a/core/java/android/os/HandlerThread.java +++ b/core/java/android/os/HandlerThread.java @@ -64,7 +64,7 @@ public class HandlerThread extends Thread { /** * This method returns the Looper associated with this thread. If this thread not been started * or for any reason is isAlive() returns false, this method will return null. If this thread - * has been started, this method will blocked until the looper has been initialized. + * has been started, this method will block until the looper has been initialized. * @return The looper. */ public Looper getLooper() { @@ -84,6 +84,21 @@ public class HandlerThread extends Thread { return mLooper; } + /** + * Ask the currently running looper to quit. If the thread has not + * been started or has finished (that is if {@link #getLooper} returns + * null), then false is returned. Otherwise the looper is asked to + * quit and true is returned. + */ + public boolean quit() { + Looper looper = getLooper(); + if (looper != null) { + looper.quit(); + return true; + } + return false; + } + /** * Returns the identifier of this thread. See Process.myTid(). */ diff --git a/core/java/android/os/IHardwareService.aidl b/core/java/android/os/IHardwareService.aidl index fb121bb5e2ee1e0c25f6bc6697da45ed570c7468..34f30a7d3304fba893cbb991001109682d928abe 100755 --- a/core/java/android/os/IHardwareService.aidl +++ b/core/java/android/os/IHardwareService.aidl @@ -1,16 +1,16 @@ /** * Copyright (c) 2007, The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and * limitations under the License. */ @@ -20,19 +20,16 @@ package android.os; interface IHardwareService { // Vibrator support - void vibrate(long milliseconds); + void vibrate(long milliseconds, IBinder token); void vibratePattern(in long[] pattern, int repeat, IBinder token); - void cancelVibrate(); - + void cancelVibrate(IBinder token); + // flashlight support boolean getFlashlightEnabled(); void setFlashlightEnabled(boolean on); void enableCameraFlash(int milliseconds); - - // sets the brightness of the backlights (screen, keyboard, button) 0-255 - void setBacklights(int brightness); // for the phone - void setAttentionLight(boolean on); + void setAttentionLight(boolean on, int color); } diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 5486920249cfad61bb70257e9022b49eec3930ce..bcf769da339b9ee97e6d425c95899cecc9f11429 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -26,8 +26,13 @@ interface IPowerManager void userActivity(long when, boolean noChangeLights); void userActivityWithForce(long when, boolean noChangeLights, boolean force); void setPokeLock(int pokey, IBinder lock, String tag); + int getSupportedWakeLockFlags(); void setStayOnSetting(int val); long getScreenOnTime(); void preventScreenOn(boolean prevent); void setScreenBrightnessOverride(int brightness); + boolean isScreenOn(); + + // sets the brightness of the backlights (screen, keyboard, button) 0-255 + void setBacklightBrightness(int brightness); } diff --git a/core/java/android/os/LatencyTimer.java b/core/java/android/os/LatencyTimer.java new file mode 100644 index 0000000000000000000000000000000000000000..ed2f0f9e3eff2fb92d6ca6dd531b6f8288072069 --- /dev/null +++ b/core/java/android/os/LatencyTimer.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.util.Log; + +import java.util.HashMap; + +/** + * A class to help with measuring latency in your code. + * + * Suggested usage: + * 1) Instanciate a LatencyTimer as a class field. + * private [static] LatencyTimer mLt = new LatencyTimer(100, 1000); + * 2) At various points in the code call sample with a string and the time delta to some fixed time. + * The string should be unique at each point of the code you are measuring. + * mLt.sample("before processing event", System.nanoTime() - event.getEventTimeNano()); + * processEvent(event); + * mLt.sample("after processing event ", System.nanoTime() - event.getEventTimeNano()); + * + * @hide + */ +public final class LatencyTimer +{ + final String TAG = "LatencyTimer"; + final int mSampleSize; + final int mScaleFactor; + volatile HashMap store = new HashMap(); + + /** + * Creates a LatencyTimer object + * @param sampleSize number of samples to collect before printing out the average + * @param scaleFactor divisor used to make each sample smaller to prevent overflow when + * (sampleSize * average sample value)/scaleFactor > Long.MAX_VALUE + */ + public LatencyTimer(int sampleSize, int scaleFactor) { + if (scaleFactor == 0) { + scaleFactor = 1; + } + mScaleFactor = scaleFactor; + mSampleSize = sampleSize; + } + + /** + * Add a sample delay for averaging. + * @param tag string used for printing out the result. This should be unique at each point of + * this called. + * @param delta time difference from an unique point of reference for a particular iteration + */ + public void sample(String tag, long delta) { + long[] array = getArray(tag); + + // array[mSampleSize] holds the number of used entries + final int index = (int) array[mSampleSize]++; + array[index] = delta; + if (array[mSampleSize] == mSampleSize) { + long totalDelta = 0; + for (long d : array) { + totalDelta += d/mScaleFactor; + } + array[mSampleSize] = 0; + Log.i(TAG, tag + " average = " + totalDelta / mSampleSize); + } + } + + private long[] getArray(String tag) { + long[] data = store.get(tag); + if (data == null) { + synchronized(store) { + data = store.get(tag); + if (data == null) { + data = new long[mSampleSize + 1]; + store.put(tag, data); + data[mSampleSize] = 0; + } + } + } + return data; + } +} diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index c14925cd38394f91807ef05cd82dee6514c53376..03542ddaa9b785388261486804fe1ddcf087dc91 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -52,7 +52,7 @@ public class MemoryFile private static native void native_write(FileDescriptor fd, int address, byte[] buffer, int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException; - private static native boolean native_is_ashmem_region(FileDescriptor fd) throws IOException; + private static native int native_get_mapped_size(FileDescriptor fd) throws IOException; private FileDescriptor mFD; // ashmem file descriptor private int mAddress; // address of ashmem memory @@ -300,7 +300,20 @@ public class MemoryFile * @hide */ public static boolean isMemoryFile(FileDescriptor fd) throws IOException { - return native_is_ashmem_region(fd); + return (native_get_mapped_size(fd) >= 0); + } + + /** + * Returns the size of the memory file, rounded up to a page boundary, that + * the file descriptor refers to, or -1 if the file descriptor does not + * refer to a memory file. + * + * @throws IOException If fd is not a valid file descriptor. + * + * @hide + */ + public static int getMappedSize(FileDescriptor fd) throws IOException { + return native_get_mapped_size(fd); } /** diff --git a/core/java/android/os/ParcelUuid.aidl b/core/java/android/os/ParcelUuid.aidl new file mode 100644 index 0000000000000000000000000000000000000000..f7e080ac5901cbd03b731fd1a23f891e6d19a6c7 --- /dev/null +++ b/core/java/android/os/ParcelUuid.aidl @@ -0,0 +1,19 @@ +/* +** 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 android.os; + +parcelable ParcelUuid; diff --git a/core/java/android/os/ParcelUuid.java b/core/java/android/os/ParcelUuid.java new file mode 100644 index 0000000000000000000000000000000000000000..88fcfc56622a9ed0fe10eb5524a1ab863d7f0fff --- /dev/null +++ b/core/java/android/os/ParcelUuid.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import java.util.UUID; + +/** + * This class is a Parcelable wrapper around {@link UUID} which is an + * immutable representation of a 128-bit universally unique + * identifier. + */ +public final class ParcelUuid implements Parcelable { + + private final UUID mUuid; + + /** + * Constructor creates a ParcelUuid instance from the + * given {@link UUID}. + * + * @param uuid UUID + */ + public ParcelUuid(UUID uuid) { + mUuid = uuid; + } + + /** + * Creates a new ParcelUuid from a string representation of {@link UUID}. + * + * @param uuid + * the UUID string to parse. + * @return an ParcelUuid instance. + * @throws NullPointerException + * if {@code uuid} is {@code null}. + * @throws IllegalArgumentException + * if {@code uuid} is not formatted correctly. + */ + public static ParcelUuid fromString(String uuid) { + return new ParcelUuid(UUID.fromString(uuid)); + } + + /** + * Get the {@link UUID} represented by the ParcelUuid. + * + * @return UUID contained in the ParcelUuid. + */ + public UUID getUuid() { + return mUuid; + } + + /** + * Returns a string representation of the ParcelUuid + * For example: 0000110B-0000-1000-8000-00805F9B34FB will be the return value. + * + * @return a String instance. + */ + @Override + public String toString() { + return mUuid.toString(); + } + + + @Override + public int hashCode() { + return mUuid.hashCode(); + } + + /** + * Compares this ParcelUuid to another object for equality. If {@code object} + * is not {@code null}, is a ParcelUuid instance, and all bits are equal, then + * {@code true} is returned. + * + * @param object + * the {@code Object} to compare to. + * @return {@code true} if this ParcelUuid is equal to {@code object} + * or {@code false} if not. + */ + @Override + public boolean equals(Object object) { + if (object == null) { + return false; + } + + if (this == object) { + return true; + } + + if (!(object instanceof ParcelUuid)) { + return false; + } + + ParcelUuid that = (ParcelUuid) object; + + return (this.mUuid.equals(that.mUuid)); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ParcelUuid createFromParcel(Parcel source) { + long mostSigBits = source.readLong(); + long leastSigBits = source.readLong(); + UUID uuid = new UUID(mostSigBits, leastSigBits); + return new ParcelUuid(uuid); + } + + public ParcelUuid[] newArray(int size) { + return new ParcelUuid[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mUuid.getMostSignificantBits()); + dest.writeLong(mUuid.getLeastSignificantBits()); + } +} diff --git a/core/java/android/os/Parcelable.java b/core/java/android/os/Parcelable.java index aee1e0b523950abf11244e05ddb4015268a2c9e7..0a4b60fdd811ab2550e77c7ab9fdde1da24c4956 100644 --- a/core/java/android/os/Parcelable.java +++ b/core/java/android/os/Parcelable.java @@ -28,13 +28,17 @@ package android.os; *
      * public class MyParcelable implements Parcelable {
      *     private int mData;
    - *     
    + *
    + *     public int describeContents() {
    + *         return 0;
    + *     }
    + *
      *     public void writeToParcel(Parcel out, int flags) {
      *         out.writeInt(mData);
      *     }
      *
    - *     public static final Parcelable.Creator CREATOR
    - *             = new Parcelable.Creator() {
    + *     public static final Parcelable.Creator<MyParcelable> CREATOR
    + *             = new Parcelable.Creator<MyParcelable>() {
      *         public MyParcelable createFromParcel(Parcel in) {
      *             return new MyParcelable(in);
      *         }
    @@ -42,7 +46,7 @@ package android.os;
      *         public MyParcelable[] newArray(int size) {
      *             return new MyParcelable[size];
      *         }
    - *     }
    + *     };
      *     
      *     private MyParcelable(Parcel in) {
      *         mData = in.readInt();
    diff --git a/core/java/android/os/PerformanceCollector.java b/core/java/android/os/PerformanceCollector.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..be1cf6df6b92b95e62f42d5f3f353ef657851812
    --- /dev/null
    +++ b/core/java/android/os/PerformanceCollector.java
    @@ -0,0 +1,587 @@
    +/*
    + * Copyright (C) 2009 The Android Open Source Project
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package android.os;
    +
    +
    +import java.util.ArrayList;
    +
    +/**
    + * Collects performance data between two function calls in Bundle objects and
    + * outputs the results using writer of type {@link PerformanceResultsWriter}.
    + * 

    + * {@link #beginSnapshot(String)} and {@link #endSnapshot()} functions collect + * memory usage information and measure runtime between calls to begin and end. + * These functions logically wrap around an entire test, and should be called + * with name of test as the label, e.g. EmailPerformanceTest. + *

    + * {@link #startTiming(String)} and {@link #stopTiming(String)} functions + * measure runtime between calls to start and stop. These functions logically + * wrap around a single test case or a small block of code, and should be called + * with the name of test case as the label, e.g. testSimpleSendMailSequence. + *

    + * {@link #addIteration(String)} inserts intermediate measurement point which + * can be labeled with a String, e.g. Launch email app, compose, send, etc. + *

    + * Snapshot and timing functions do not interfere with each other, and thus can + * be called in any order. The intended structure is to wrap begin/endSnapshot + * around calls to start/stopTiming, for example: + *

    + * beginSnapshot("EmailPerformanceTest"); + * startTiming("testSimpleSendSequence"); + * addIteration("Launch email app"); + * addIteration("Compose"); + * stopTiming("Send"); + * startTiming("testComplexSendSequence"); + * stopTiming(""); + * startTiming("testAddLabel"); + * stopTiming(""); + * endSnapshot(); + *

    + * Structure of results output is up to implementor of + * {@link PerformanceResultsWriter }. + * + * {@hide} Pending approval for public API. + */ +public class PerformanceCollector { + + /** + * Interface for reporting performance data. + */ + public interface PerformanceResultsWriter { + + /** + * Callback invoked as first action in + * PerformanceCollector#beginSnapshot(String) for reporting the start of + * a performance snapshot. + * + * @param label description of code block between beginSnapshot and + * PerformanceCollector#endSnapshot() + * @see PerformanceCollector#beginSnapshot(String) + */ + public void writeBeginSnapshot(String label); + + /** + * Callback invoked as last action in PerformanceCollector#endSnapshot() + * for reporting performance data collected in the snapshot. + * + * @param results memory and runtime metrics stored as key/value pairs, + * in the same structure as returned by + * PerformanceCollector#endSnapshot() + * @see PerformanceCollector#endSnapshot() + */ + public void writeEndSnapshot(Bundle results); + + /** + * Callback invoked as first action in + * PerformanceCollector#startTiming(String) for reporting the start of + * a timing measurement. + * + * @param label description of code block between startTiming and + * PerformanceCollector#stopTiming(String) + * @see PerformanceCollector#startTiming(String) + */ + public void writeStartTiming(String label); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#stopTiming(String)} for reporting the + * sequence of timings measured. + * + * @param results runtime metrics of code block between calls to + * startTiming and stopTiming, in the same structure as + * returned by PerformanceCollector#stopTiming(String) + * @see PerformanceCollector#stopTiming(String) + */ + public void writeStopTiming(Bundle results); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, long)} for + * reporting an integer type measurement. + * + * @param label short description of the metric that was measured + * @param value long value of the measurement + */ + public void writeMeasurement(String label, long value); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, float)} for + * reporting a float type measurement. + * + * @param label short description of the metric that was measured + * @param value float value of the measurement + */ + public void writeMeasurement(String label, float value); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, String)} for + * reporting a string field. + * + * @param label short description of the metric that was measured + * @param value string summary of the measurement + */ + public void writeMeasurement(String label, String value); + } + + /** + * In a results Bundle, this key references a List of iteration Bundles. + */ + public static final String METRIC_KEY_ITERATIONS = "iterations"; + /** + * In an iteration Bundle, this key describes the iteration. + */ + public static final String METRIC_KEY_LABEL = "label"; + /** + * In a results Bundle, this key reports the cpu time of the code block + * under measurement. + */ + public static final String METRIC_KEY_CPU_TIME = "cpu_time"; + /** + * In a results Bundle, this key reports the execution time of the code + * block under measurement. + */ + public static final String METRIC_KEY_EXECUTION_TIME = "execution_time"; + /** + * In a snapshot Bundle, this key reports the number of received + * transactions from the binder driver before collection started. + */ + public static final String METRIC_KEY_PRE_RECEIVED_TRANSACTIONS = "pre_received_transactions"; + /** + * In a snapshot Bundle, this key reports the number of transactions sent by + * the running program before collection started. + */ + public static final String METRIC_KEY_PRE_SENT_TRANSACTIONS = "pre_sent_transactions"; + /** + * In a snapshot Bundle, this key reports the number of received + * transactions from the binder driver. + */ + public static final String METRIC_KEY_RECEIVED_TRANSACTIONS = "received_transactions"; + /** + * In a snapshot Bundle, this key reports the number of transactions sent by + * the running program. + */ + public static final String METRIC_KEY_SENT_TRANSACTIONS = "sent_transactions"; + /** + * In a snapshot Bundle, this key reports the number of garbage collection + * invocations. + */ + public static final String METRIC_KEY_GC_INVOCATION_COUNT = "gc_invocation_count"; + /** + * In a snapshot Bundle, this key reports the amount of allocated memory + * used by the running program. + */ + public static final String METRIC_KEY_JAVA_ALLOCATED = "java_allocated"; + /** + * In a snapshot Bundle, this key reports the amount of free memory + * available to the running program. + */ + public static final String METRIC_KEY_JAVA_FREE = "java_free"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by dalvik. + */ + public static final String METRIC_KEY_JAVA_PRIVATE_DIRTY = "java_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for + * dalvik. + */ + public static final String METRIC_KEY_JAVA_PSS = "java_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by dalvik. + */ + public static final String METRIC_KEY_JAVA_SHARED_DIRTY = "java_shared_dirty"; + /** + * In a snapshot Bundle, this key reports the total amount of memory + * available to the running program. + */ + public static final String METRIC_KEY_JAVA_SIZE = "java_size"; + /** + * In a snapshot Bundle, this key reports the amount of allocated memory in + * the native heap. + */ + public static final String METRIC_KEY_NATIVE_ALLOCATED = "native_allocated"; + /** + * In a snapshot Bundle, this key reports the amount of free memory in the + * native heap. + */ + public static final String METRIC_KEY_NATIVE_FREE = "native_free"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by the native heap. + */ + public static final String METRIC_KEY_NATIVE_PRIVATE_DIRTY = "native_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for the + * native heap. + */ + public static final String METRIC_KEY_NATIVE_PSS = "native_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by the native heap. + */ + public static final String METRIC_KEY_NATIVE_SHARED_DIRTY = "native_shared_dirty"; + /** + * In a snapshot Bundle, this key reports the size of the native heap. + */ + public static final String METRIC_KEY_NATIVE_SIZE = "native_size"; + /** + * In a snapshot Bundle, this key reports the number of objects allocated + * globally. + */ + public static final String METRIC_KEY_GLOBAL_ALLOC_COUNT = "global_alloc_count"; + /** + * In a snapshot Bundle, this key reports the size of all objects allocated + * globally. + */ + public static final String METRIC_KEY_GLOBAL_ALLOC_SIZE = "global_alloc_size"; + /** + * In a snapshot Bundle, this key reports the number of objects freed + * globally. + */ + public static final String METRIC_KEY_GLOBAL_FREED_COUNT = "global_freed_count"; + /** + * In a snapshot Bundle, this key reports the size of all objects freed + * globally. + */ + public static final String METRIC_KEY_GLOBAL_FREED_SIZE = "global_freed_size"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by everything else. + */ + public static final String METRIC_KEY_OTHER_PRIVATE_DIRTY = "other_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for + * everything else. + */ + public static final String METRIC_KEY_OTHER_PSS = "other_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by everything else. + */ + public static final String METRIC_KEY_OTHER_SHARED_DIRTY = "other_shared_dirty"; + + private PerformanceResultsWriter mPerfWriter; + private Bundle mPerfSnapshot; + private Bundle mPerfMeasurement; + private long mSnapshotCpuTime; + private long mSnapshotExecTime; + private long mCpuTime; + private long mExecTime; + + public PerformanceCollector() { + } + + public PerformanceCollector(PerformanceResultsWriter writer) { + setPerformanceResultsWriter(writer); + } + + public void setPerformanceResultsWriter(PerformanceResultsWriter writer) { + mPerfWriter = writer; + } + + /** + * Begin collection of memory usage information. + * + * @param label description of code block between beginSnapshot and + * endSnapshot, used to label output + */ + public void beginSnapshot(String label) { + if (mPerfWriter != null) + mPerfWriter.writeBeginSnapshot(label); + startPerformanceSnapshot(); + } + + /** + * End collection of memory usage information. Returns collected data in a + * Bundle object. + * + * @return Memory and runtime metrics stored as key/value pairs. Values are + * of type long, and keys include: + *

      + *
    • {@link #METRIC_KEY_CPU_TIME cpu_time} + *
    • {@link #METRIC_KEY_EXECUTION_TIME execution_time} + *
    • {@link #METRIC_KEY_PRE_RECEIVED_TRANSACTIONS + * pre_received_transactions} + *
    • {@link #METRIC_KEY_PRE_SENT_TRANSACTIONS + * pre_sent_transactions} + *
    • {@link #METRIC_KEY_RECEIVED_TRANSACTIONS + * received_transactions} + *
    • {@link #METRIC_KEY_SENT_TRANSACTIONS sent_transactions} + *
    • {@link #METRIC_KEY_GC_INVOCATION_COUNT gc_invocation_count} + *
    • {@link #METRIC_KEY_JAVA_ALLOCATED java_allocated} + *
    • {@link #METRIC_KEY_JAVA_FREE java_free} + *
    • {@link #METRIC_KEY_JAVA_PRIVATE_DIRTY java_private_dirty} + *
    • {@link #METRIC_KEY_JAVA_PSS java_pss} + *
    • {@link #METRIC_KEY_JAVA_SHARED_DIRTY java_shared_dirty} + *
    • {@link #METRIC_KEY_JAVA_SIZE java_size} + *
    • {@link #METRIC_KEY_NATIVE_ALLOCATED native_allocated} + *
    • {@link #METRIC_KEY_NATIVE_FREE native_free} + *
    • {@link #METRIC_KEY_NATIVE_PRIVATE_DIRTY native_private_dirty} + *
    • {@link #METRIC_KEY_NATIVE_PSS native_pss} + *
    • {@link #METRIC_KEY_NATIVE_SHARED_DIRTY native_shared_dirty} + *
    • {@link #METRIC_KEY_NATIVE_SIZE native_size} + *
    • {@link #METRIC_KEY_GLOBAL_ALLOC_COUNT global_alloc_count} + *
    • {@link #METRIC_KEY_GLOBAL_ALLOC_SIZE global_alloc_size} + *
    • {@link #METRIC_KEY_GLOBAL_FREED_COUNT global_freed_count} + *
    • {@link #METRIC_KEY_GLOBAL_FREED_SIZE global_freed_size} + *
    • {@link #METRIC_KEY_OTHER_PRIVATE_DIRTY other_private_dirty} + *
    • {@link #METRIC_KEY_OTHER_PSS other_pss} + *
    • {@link #METRIC_KEY_OTHER_SHARED_DIRTY other_shared_dirty} + *
    + */ + public Bundle endSnapshot() { + endPerformanceSnapshot(); + if (mPerfWriter != null) + mPerfWriter.writeEndSnapshot(mPerfSnapshot); + return mPerfSnapshot; + } + + /** + * Start measurement of user and cpu time. + * + * @param label description of code block between startTiming and + * stopTiming, used to label output + */ + public void startTiming(String label) { + if (mPerfWriter != null) + mPerfWriter.writeStartTiming(label); + mPerfMeasurement = new Bundle(); + mPerfMeasurement.putParcelableArrayList( + METRIC_KEY_ITERATIONS, new ArrayList()); + mExecTime = SystemClock.uptimeMillis(); + mCpuTime = Process.getElapsedCpuTime(); + } + + /** + * Add a measured segment, and start measuring the next segment. Returns + * collected data in a Bundle object. + * + * @param label description of code block between startTiming and + * addIteration, and between two calls to addIteration, used + * to label output + * @return Runtime metrics stored as key/value pairs. Values are of type + * long, and keys include: + *
      + *
    • {@link #METRIC_KEY_LABEL label} + *
    • {@link #METRIC_KEY_CPU_TIME cpu_time} + *
    • {@link #METRIC_KEY_EXECUTION_TIME execution_time} + *
    + */ + public Bundle addIteration(String label) { + mCpuTime = Process.getElapsedCpuTime() - mCpuTime; + mExecTime = SystemClock.uptimeMillis() - mExecTime; + + Bundle iteration = new Bundle(); + iteration.putString(METRIC_KEY_LABEL, label); + iteration.putLong(METRIC_KEY_EXECUTION_TIME, mExecTime); + iteration.putLong(METRIC_KEY_CPU_TIME, mCpuTime); + mPerfMeasurement.getParcelableArrayList(METRIC_KEY_ITERATIONS).add(iteration); + + mExecTime = SystemClock.uptimeMillis(); + mCpuTime = Process.getElapsedCpuTime(); + return iteration; + } + + /** + * Stop measurement of user and cpu time. + * + * @param label description of code block between addIteration or + * startTiming and stopTiming, used to label output + * @return Runtime metrics stored in a bundle, including all iterations + * between calls to startTiming and stopTiming. List of iterations + * is keyed by {@link #METRIC_KEY_ITERATIONS iterations}. + */ + public Bundle stopTiming(String label) { + addIteration(label); + if (mPerfWriter != null) + mPerfWriter.writeStopTiming(mPerfMeasurement); + return mPerfMeasurement; + } + + /** + * Add an integer type measurement to the collector. + * + * @param label short description of the metric that was measured + * @param value long value of the measurement + */ + public void addMeasurement(String label, long value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /** + * Add a float type measurement to the collector. + * + * @param label short description of the metric that was measured + * @param value float value of the measurement + */ + public void addMeasurement(String label, float value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /** + * Add a string field to the collector. + * + * @param label short description of the metric that was measured + * @param value string summary of the measurement + */ + public void addMeasurement(String label, String value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /* + * Starts tracking memory usage, binder transactions, and real & cpu timing. + */ + private void startPerformanceSnapshot() { + // Create new snapshot + mPerfSnapshot = new Bundle(); + + // Add initial binder counts + Bundle binderCounts = getBinderCounts(); + for (String key : binderCounts.keySet()) { + mPerfSnapshot.putLong("pre_" + key, binderCounts.getLong(key)); + } + + // Force a GC and zero out the performance counters. Do this + // before reading initial CPU/wall-clock times so we don't include + // the cost of this setup in our final metrics. + startAllocCounting(); + + // Record CPU time up to this point, and start timing. Note: this + // must happen at the end of this method, otherwise the timing will + // include noise. + mSnapshotExecTime = SystemClock.uptimeMillis(); + mSnapshotCpuTime = Process.getElapsedCpuTime(); + } + + /* + * Stops tracking memory usage, binder transactions, and real & cpu timing. + * Stores collected data as type long into Bundle object for reporting. + */ + private void endPerformanceSnapshot() { + // Stop the timing. This must be done first before any other counting is + // stopped. + mSnapshotCpuTime = Process.getElapsedCpuTime() - mSnapshotCpuTime; + mSnapshotExecTime = SystemClock.uptimeMillis() - mSnapshotExecTime; + + stopAllocCounting(); + + long nativeMax = Debug.getNativeHeapSize() / 1024; + long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; + long nativeFree = Debug.getNativeHeapFreeSize() / 1024; + + Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memInfo); + + Runtime runtime = Runtime.getRuntime(); + + long dalvikMax = runtime.totalMemory() / 1024; + long dalvikFree = runtime.freeMemory() / 1024; + long dalvikAllocated = dalvikMax - dalvikFree; + + // Add final binder counts + Bundle binderCounts = getBinderCounts(); + for (String key : binderCounts.keySet()) { + mPerfSnapshot.putLong(key, binderCounts.getLong(key)); + } + + // Add alloc counts + Bundle allocCounts = getAllocCounts(); + for (String key : allocCounts.keySet()) { + mPerfSnapshot.putLong(key, allocCounts.getLong(key)); + } + + mPerfSnapshot.putLong(METRIC_KEY_EXECUTION_TIME, mSnapshotExecTime); + mPerfSnapshot.putLong(METRIC_KEY_CPU_TIME, mSnapshotCpuTime); + + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_SIZE, nativeMax); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_ALLOCATED, nativeAllocated); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_FREE, nativeFree); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_PSS, memInfo.nativePss); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_PRIVATE_DIRTY, memInfo.nativePrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_SHARED_DIRTY, memInfo.nativeSharedDirty); + + mPerfSnapshot.putLong(METRIC_KEY_JAVA_SIZE, dalvikMax); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_ALLOCATED, dalvikAllocated); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_FREE, dalvikFree); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_PSS, memInfo.dalvikPss); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_PRIVATE_DIRTY, memInfo.dalvikPrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_SHARED_DIRTY, memInfo.dalvikSharedDirty); + + mPerfSnapshot.putLong(METRIC_KEY_OTHER_PSS, memInfo.otherPss); + mPerfSnapshot.putLong(METRIC_KEY_OTHER_PRIVATE_DIRTY, memInfo.otherPrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_OTHER_SHARED_DIRTY, memInfo.otherSharedDirty); + } + + /* + * Starts allocation counting. This triggers a gc and resets the counts. + */ + private static void startAllocCounting() { + // Before we start trigger a GC and reset the debug counts. Run the + // finalizers and another GC before starting and stopping the alloc + // counts. This will free up any objects that were just sitting around + // waiting for their finalizers to be run. + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + + Debug.resetAllCounts(); + + // start the counts + Debug.startAllocCounting(); + } + + /* + * Stops allocation counting. + */ + private static void stopAllocCounting() { + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Debug.stopAllocCounting(); + } + + /* + * Returns a bundle with the current results from the allocation counting. + */ + private static Bundle getAllocCounts() { + Bundle results = new Bundle(); + results.putLong(METRIC_KEY_GLOBAL_ALLOC_COUNT, Debug.getGlobalAllocCount()); + results.putLong(METRIC_KEY_GLOBAL_ALLOC_SIZE, Debug.getGlobalAllocSize()); + results.putLong(METRIC_KEY_GLOBAL_FREED_COUNT, Debug.getGlobalFreedCount()); + results.putLong(METRIC_KEY_GLOBAL_FREED_SIZE, Debug.getGlobalFreedSize()); + results.putLong(METRIC_KEY_GC_INVOCATION_COUNT, Debug.getGlobalGcInvocationCount()); + return results; + } + + /* + * Returns a bundle with the counts for various binder counts for this + * process. Currently the only two that are reported are the number of send + * and the number of received transactions. + */ + private static Bundle getBinderCounts() { + Bundle results = new Bundle(); + results.putLong(METRIC_KEY_SENT_TRANSACTIONS, Debug.getBinderSentTransactions()); + results.putLong(METRIC_KEY_RECEIVED_TRANSACTIONS, Debug.getBinderReceivedTransactions()); + return results; + } +} diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index bfcf2fc2fbcd83d7e7fbf9aef478e0bf8fe2a1c7..2efc23041038fe83c2bcbedb1b8189163f02ac69 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -114,12 +114,14 @@ public class PowerManager private static final int WAKE_BIT_SCREEN_DIM = 4; private static final int WAKE_BIT_SCREEN_BRIGHT = 8; private static final int WAKE_BIT_KEYBOARD_BRIGHT = 16; + private static final int WAKE_BIT_PROXIMITY_SCREEN_OFF = 32; private static final int LOCK_MASK = WAKE_BIT_CPU_STRONG | WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_DIM | WAKE_BIT_SCREEN_BRIGHT - | WAKE_BIT_KEYBOARD_BRIGHT; + | WAKE_BIT_KEYBOARD_BRIGHT + | WAKE_BIT_PROXIMITY_SCREEN_OFF; /** * Wake lock that ensures that the CPU is running. The screen might @@ -146,6 +148,16 @@ public class PowerManager */ public static final int SCREEN_DIM_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_DIM; + /** + * Wake lock that turns the screen off when the proximity sensor activates. + * Since not all devices have proximity sensors, use + * {@link #getSupportedWakeLockFlags() getSupportedWakeLockFlags()} to determine if + * this wake lock mode is supported. + * + * {@hide} + */ + public static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = WAKE_BIT_PROXIMITY_SCREEN_OFF; + /** * Normally wake locks don't actually wake the device, they just cause * it to remain on once it's already on. Think of the video player @@ -196,6 +208,7 @@ public class PowerManager case SCREEN_DIM_WAKE_LOCK: case SCREEN_BRIGHT_WAKE_LOCK: case FULL_WAKE_LOCK: + case PROXIMITY_SCREEN_OFF_WAKE_LOCK: break; default: throw new IllegalArgumentException(); @@ -365,7 +378,68 @@ public class PowerManager } catch (RemoteException e) { } } - + + /** + * sets the brightness of the backlights (screen, keyboard, button). + * + * @param brightness value from 0 to 255 + * + * {@hide} + */ + public void setBacklightBrightness(int brightness) + { + try { + mService.setBacklightBrightness(brightness); + } catch (RemoteException e) { + } + } + + /** + * Returns the set of flags for {@link #newWakeLock(int, String) newWakeLock()} + * that are supported on the device. + * For example, to test to see if the {@link #PROXIMITY_SCREEN_OFF_WAKE_LOCK} + * is supported: + * + * {@samplecode + * PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + * int supportedFlags = pm.getSupportedWakeLockFlags(); + * boolean proximitySupported = ((supportedFlags & PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK) + * == PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK); + * } + * + * @return the set of supported WakeLock flags. + * + * {@hide} + */ + public int getSupportedWakeLockFlags() + { + try { + return mService.getSupportedWakeLockFlags(); + } catch (RemoteException e) { + return 0; + } + } + + /** + * Returns whether the screen is currently on. The screen could be bright + * or dim. + * + * {@samplecode + * PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + * boolean isScreenOn = pm.isScreenOn(); + * } + * + * @return whether the screen is on (bright or dim). + */ + public boolean isScreenOn() + { + try { + return mService.isScreenOn(); + } catch (RemoteException e) { + return false; + } + } + private PowerManager() { } diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 480519386e4887d9ba15f1a8a6c1415a23c422d9..699ddb27606b7eb97591aa45d2913b3c3f6f462f 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -73,6 +73,12 @@ public class Process { */ public static final int SHELL_UID = 2000; + /** + * Defines the UID/GID for the log group. + * @hide + */ + public static final int LOG_UID = 1007; + /** * Defines the UID/GID for the WIFI supplicant process. * @hide @@ -739,7 +745,7 @@ public class Process { public static final native void sendSignal(int pid, int signal); /** @hide */ - public static final native int getFreeMemory(); + public static final native long getFreeMemory(); /** @hide */ public static final native void readProcLines(String path, diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java index 584224fccc8b0959a2580a14411536855b1423cc..b74af1616fb4741b30a927d5e27697040cf7a1b2 100644 --- a/core/java/android/os/RemoteCallbackList.java +++ b/core/java/android/os/RemoteCallbackList.java @@ -22,7 +22,7 @@ import java.util.HashMap; * Takes care of the grunt work of maintaining a list of remote interfaces, * typically for the use of performing callbacks from a * {@link android.app.Service} to its clients. In particular, this: - * + * *
      *
    • Keeps track of a set of registered {@link IInterface} callbacks, * taking care to identify them through their underlying unique {@link IBinder} @@ -34,13 +34,13 @@ import java.util.HashMap; * multithreaded incoming calls, and a thread-safe way to iterate over a * snapshot of the list without holding its lock. *
    - * + * *

    To use this class, simply create a single instance along with your * service, and call its {@link #register} and {@link #unregister} methods * as client register and unregister with your service. To call back on to * the registered clients, use {@link #beginBroadcast}, * {@link #getBroadcastItem}, and {@link #finishBroadcast}. - * + * *

    If a registered callback's process goes away, this class will take * care of automatically removing it from the list. If you want to do * additional work in this situation, you can create a subclass that @@ -52,7 +52,7 @@ public class RemoteCallbackList { private Object[] mActiveBroadcast; private int mBroadcastCount = -1; private boolean mKilled = false; - + private final class Callback implements IBinder.DeathRecipient { final E mCallback; final Object mCookie; @@ -61,7 +61,7 @@ public class RemoteCallbackList { mCallback = callback; mCookie = cookie; } - + public void binderDied() { synchronized (mCallbacks) { mCallbacks.remove(mCallback.asBinder()); @@ -69,7 +69,7 @@ public class RemoteCallbackList { onCallbackDied(mCallback, mCookie); } } - + /** * Simple version of {@link RemoteCallbackList#register(E, Object)} * that does not take a cookie object. @@ -86,19 +86,20 @@ public class RemoteCallbackList { * object is already in the list), then it will be left as-is. * Registrations are not counted; a single call to {@link #unregister} * will remove a callback after any number calls to register it. - * + * * @param callback The callback interface to be added to the list. Must * not be null -- passing null here will cause a NullPointerException. * Most services will want to check for null before calling this with * an object given from a client, so that clients can't crash the * service with bad data. + * * @param cookie Optional additional data to be associated with this * callback. * * @return Returns true if the callback was successfully added to the list. * Returns false if it was not added, either because {@link #kill} had * previously been called or the callback's process has gone away. - * + * * @see #unregister * @see #kill * @see #onCallbackDied @@ -119,7 +120,7 @@ public class RemoteCallbackList { } } } - + /** * Remove from the list a callback that was previously added with * {@link #register}. This uses the @@ -127,14 +128,14 @@ public class RemoteCallbackList { * find the previous registration. * Registrations are not counted; a single unregister call will remove * a callback after any number calls to {@link #register} for it. - * + * * @param callback The callback to be removed from the list. Passing * null here will cause a NullPointerException, so you will generally want * to check for null before calling. - * + * * @return Returns true if the callback was found and unregistered. Returns * false if the given callback was not found on the list. - * + * * @see #register */ public boolean unregister(E callback) { @@ -147,13 +148,13 @@ public class RemoteCallbackList { return false; } } - + /** * Disable this callback list. All registered callbacks are unregistered, * and the list is disabled so that future calls to {@link #register} will * fail. This should be used when a Service is stopping, to prevent clients * from registering callbacks after it is stopped. - * + * * @see #register */ public void kill() { @@ -165,7 +166,7 @@ public class RemoteCallbackList { mKilled = true; } } - + /** * Old version of {@link #onCallbackDied(E, Object)} that * does not provide a cookie. @@ -190,7 +191,7 @@ public class RemoteCallbackList { public void onCallbackDied(E callback, Object cookie) { onCallbackDied(callback); } - + /** * Prepare to start making calls to the currently registered callbacks. * This creates a copy of the callback list, which you can retrieve items @@ -199,12 +200,12 @@ public class RemoteCallbackList { * same thread (usually by scheduling with {@link Handler}) or * do your own synchronization. You must call {@link #finishBroadcast} * when done. - * + * *

    A typical loop delivering a broadcast looks like this: - * + * *

          * int i = callbacks.beginBroadcast();
    -     * while (i > 0) {
    +     * while (i > 0) {
          *     i--;
          *     try {
          *         callbacks.getBroadcastItem(i).somethingHappened();
    @@ -214,11 +215,11 @@ public class RemoteCallbackList {
          *     }
          * }
          * callbacks.finishBroadcast();
    - * + * * @return Returns the number of callbacks in the broadcast, to be used * with {@link #getBroadcastItem} to determine the range of indices you * can supply. - * + * * @see #getBroadcastItem * @see #finishBroadcast */ @@ -244,26 +245,26 @@ public class RemoteCallbackList { return i; } } - + /** * Retrieve an item in the active broadcast that was previously started * with {@link #beginBroadcast}. This can only be called after * the broadcast is started, and its data is no longer valid after * calling {@link #finishBroadcast}. - * + * *

    Note that it is possible for the process of one of the returned * callbacks to go away before you call it, so you will need to catch * {@link RemoteException} when calling on to the returned object. * The callback list itself, however, will take care of unregistering * these objects once it detects that it is no longer valid, so you can * handle such an exception by simply ignoring it. - * + * * @param index Which of the registered callbacks you would like to * retrieve. Ranges from 0 to 1-{@link #beginBroadcast}. - * + * * @return Returns the callback interface that you can call. This will * always be non-null. - * + * * @see #beginBroadcast */ public E getBroadcastItem(int index) { @@ -279,12 +280,12 @@ public class RemoteCallbackList { public Object getBroadcastCookie(int index) { return ((Callback)mActiveBroadcast[index]).mCookie; } - + /** * Clean up the state of a broadcast previously initiated by calling * {@link #beginBroadcast}. This must always be called when you are done * with a broadcast. - * + * * @see #beginBroadcast */ public void finishBroadcast() { diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java index 2b57b3942a2ffabdcf562f44d16d881e24d66406..2dd674987fc514ff1abbc2c121cde100e91a8aa5 100644 --- a/core/java/android/os/SystemClock.java +++ b/core/java/android/os/SystemClock.java @@ -30,7 +30,13 @@ package android.os; * backwards or forwards unpredictably. This clock should only be used * when correspondence with real-world dates and times is important, such * as in a calendar or alarm clock application. Interval or elapsed - * time measurements should use a different clock. + * time measurements should use a different clock. If you are using + * System.currentTimeMillis(), consider listening to the + * {@link android.content.Intent#ACTION_TIME_TICK ACTION_TIME_TICK}, + * {@link android.content.Intent#ACTION_TIME_CHANGED ACTION_TIME_CHANGED} + * and {@link android.content.Intent#ACTION_TIMEZONE_CHANGED + * ACTION_TIMEZONE_CHANGED} {@link android.content.Intent Intent} + * broadcasts to find out when the time changes. * *

  • {@link #uptimeMillis} is counted in milliseconds since the * system was booted. This clock stops when the system enters deep diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java index c3ae3c26aa4346686fd28371e8f1d96a6a1b2ee4..4a036ecbd6e7e0155cd3dbc58fd5a433803f3ea5 100644 --- a/core/java/android/os/SystemProperties.java +++ b/core/java/android/os/SystemProperties.java @@ -30,6 +30,9 @@ public class SystemProperties private static native String native_get(String key); private static native String native_get(String key, String def); + private static native int native_get_int(String key, int def); + private static native long native_get_long(String key, long def); + private static native boolean native_get_boolean(String key, boolean def); private static native void native_set(String key, String def); /** @@ -65,11 +68,10 @@ public class SystemProperties * @throws IllegalArgumentException if the key exceeds 32 characters */ public static int getInt(String key, int def) { - try { - return Integer.parseInt(get(key)); - } catch (NumberFormatException e) { - return def; + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); } + return native_get_int(key, def); } /** @@ -81,11 +83,10 @@ public class SystemProperties * @throws IllegalArgumentException if the key exceeds 32 characters */ public static long getLong(String key, long def) { - try { - return Long.parseLong(get(key)); - } catch (NumberFormatException e) { - return def; + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); } + return native_get_long(key, def); } /** @@ -102,27 +103,10 @@ public class SystemProperties * @throws IllegalArgumentException if the key exceeds 32 characters */ public static boolean getBoolean(String key, boolean def) { - String value = get(key); - // Deal with these quick cases first: not found, 0 and 1 - if (value.equals("")) { - return def; - } else if (value.equals("0")) { - return false; - } else if (value.equals("1")) { - return true; - // now for slower (and hopefully less common) cases - } else if (value.equalsIgnoreCase("n") || - value.equalsIgnoreCase("no") || - value.equalsIgnoreCase("false") || - value.equalsIgnoreCase("off")) { - return false; - } else if (value.equalsIgnoreCase("y") || - value.equalsIgnoreCase("yes") || - value.equalsIgnoreCase("true") || - value.equalsIgnoreCase("on")) { - return true; + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); } - return def; + return native_get_boolean(key, def); } /** diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 0f75289fef182631c2c07837052a47dbec0e7469..51dcff11c20704592b928b332b8777f543950f94 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -24,6 +24,7 @@ package android.os; public class Vibrator { IHardwareService mService; + private final Binder mToken = new Binder(); /** @hide */ public Vibrator() @@ -40,7 +41,7 @@ public class Vibrator public void vibrate(long milliseconds) { try { - mService.vibrate(milliseconds); + mService.vibrate(milliseconds, mToken); } catch (RemoteException e) { } } @@ -65,7 +66,7 @@ public class Vibrator // anyway if (repeat < pattern.length) { try { - mService.vibratePattern(pattern, repeat, new Binder()); + mService.vibratePattern(pattern, repeat, mToken); } catch (RemoteException e) { } } else { @@ -79,7 +80,7 @@ public class Vibrator public void cancel() { try { - mService.cancelVibrate(); + mService.cancelVibrate(mToken); } catch (RemoteException e) { } } diff --git a/core/java/android/pim/ContactsAsyncHelper.java b/core/java/android/pim/ContactsAsyncHelper.java index a21281e03b356111e61f9148bbb7a396253e2f39..7c78a818e405951d6e1d714e26f42c35e0fc8458 100644 --- a/core/java/android/pim/ContactsAsyncHelper.java +++ b/core/java/android/pim/ContactsAsyncHelper.java @@ -27,8 +27,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.provider.Contacts; -import android.provider.Contacts.People; +import android.provider.ContactsContract.Contacts; import android.util.Log; import android.view.View; import android.widget.ImageView; @@ -39,35 +38,35 @@ import java.io.InputStream; * Helper class for async access of images. */ public class ContactsAsyncHelper extends Handler { - + private static final boolean DBG = false; private static final String LOG_TAG = "ContactsAsyncHelper"; - + /** * Interface for a WorkerHandler result return. */ public interface OnImageLoadCompleteListener { /** * Called when the image load is complete. - * + * * @param imagePresent true if an image was found - */ + */ public void onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent); } - + // constants private static final int EVENT_LOAD_IMAGE = 1; private static final int DEFAULT_TOKEN = -1; - + // static objects private static Handler sThreadHandler; private static ContactsAsyncHelper sInstance; - + static { sInstance = new ContactsAsyncHelper(); } - + private static final class WorkerArgs { public Context context; public ImageView view; @@ -78,12 +77,12 @@ public class ContactsAsyncHelper extends Handler { public OnImageLoadCompleteListener listener; public CallerInfo info; } - + /** - * public inner class to help out the ContactsAsyncHelper callers - * with tracking the state of the CallerInfo Queries and image + * public inner class to help out the ContactsAsyncHelper callers + * with tracking the state of the CallerInfo Queries and image * loading. - * + * * Logic contained herein is used to remove the race conditions * that exist as the CallerInfo queries run and mix with the image * loads, which then mix with the Phone state changes. @@ -94,11 +93,11 @@ public class ContactsAsyncHelper extends Handler { public static final int DISPLAY_UNDEFINED = 0; public static final int DISPLAY_IMAGE = -1; public static final int DISPLAY_DEFAULT = -2; - + // State of the image on the imageview. private CallerInfo mCurrentCallerInfo; private int displayMode; - + public ImageTracker() { mCurrentCallerInfo = null; displayMode = DISPLAY_UNDEFINED; @@ -107,17 +106,17 @@ public class ContactsAsyncHelper extends Handler { /** * Used to see if the requested call / connection has a * different caller attached to it than the one we currently - * have in the CallCard. + * have in the CallCard. */ public boolean isDifferentImageRequest(CallerInfo ci) { // note, since the connections are around for the lifetime of the - // call, and the CallerInfo-related items as well, we can + // call, and the CallerInfo-related items as well, we can // definitely use a simple != comparison. return (mCurrentCallerInfo != ci); } - + public boolean isDifferentImageRequest(Connection connection) { - // if the connection does not exist, see if the + // if the connection does not exist, see if the // mCurrentCallerInfo is also null to match. if (connection == null) { if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null"); @@ -133,58 +132,65 @@ public class ContactsAsyncHelper extends Handler { } return runQuery; } - + /** - * Simple setter for the CallerInfo object. + * Simple setter for the CallerInfo object. */ public void setPhotoRequest(CallerInfo ci) { - mCurrentCallerInfo = ci; + mCurrentCallerInfo = ci; } - + /** - * Convenience method used to retrieve the URI - * representing the Photo file recorded in the attached - * CallerInfo Object. + * Convenience method used to retrieve the URI + * representing the Photo file recorded in the attached + * CallerInfo Object. */ public Uri getPhotoUri() { if (mCurrentCallerInfo != null) { - return ContentUris.withAppendedId(People.CONTENT_URI, + return ContentUris.withAppendedId(Contacts.CONTENT_URI, mCurrentCallerInfo.person_id); } - return null; + return null; } - + /** - * Simple setter for the Photo state. + * Simple setter for the Photo state. */ public void setPhotoState(int state) { displayMode = state; } - + /** - * Simple getter for the Photo state. + * Simple getter for the Photo state. */ public int getPhotoState() { return displayMode; } } - + /** - * Thread worker class that handles the task of opening the stream and loading + * Thread worker class that handles the task of opening the stream and loading * the images. */ private class WorkerHandler extends Handler { public WorkerHandler(Looper looper) { super(looper); } - + + @Override public void handleMessage(Message msg) { WorkerArgs args = (WorkerArgs) msg.obj; - + switch (msg.arg1) { case EVENT_LOAD_IMAGE: - InputStream inputStream = Contacts.People.openContactPhotoInputStream( - args.context.getContentResolver(), args.uri); + InputStream inputStream = null; + try { + inputStream = Contacts.openContactPhotoInputStream( + args.context.getContentResolver(), args.uri); + } catch (Exception e) { + Log.e(LOG_TAG, "Error opening photo input stream", e); + } + if (inputStream != null) { args.result = Drawable.createFromStream(inputStream, args.uri.toString()); @@ -192,22 +198,22 @@ public class ContactsAsyncHelper extends Handler { " token: " + msg.what + " image URI: " + args.uri); } else { args.result = null; - if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.uri + + if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + + " token: " + msg.what + " image URI: " + args.uri + ", using default image."); } break; default: } - - // send the reply to the enclosing class. + + // send the reply to the enclosing class. Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what); reply.arg1 = msg.arg1; reply.obj = msg.obj; reply.sendToTarget(); } } - + /** * Private constructor for static class */ @@ -216,14 +222,14 @@ public class ContactsAsyncHelper extends Handler { thread.start(); sThreadHandler = new WorkerHandler(thread.getLooper()); } - + /** * Convenience method for calls that do not want to deal with listeners and tokens. */ - public static final void updateImageViewWithContactPhotoAsync(Context context, + public static final void updateImageViewWithContactPhotoAsync(Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. - updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, + updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } @@ -231,24 +237,24 @@ public class ContactsAsyncHelper extends Handler { * Convenience method for calls that do not want to deal with listeners and tokens, but have * a CallerInfo object to cache the image to. */ - public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, + public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. - updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, + updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } - + /** * Start an image load, attach the result to the specified CallerInfo object. * Note, when the query is started, we make the ImageView INVISIBLE if the * placeholderImageResource value is -1. When we're given a valid (!= -1) * placeholderImageResource value, we make sure the image is visible. */ - public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, - OnImageLoadCompleteListener listener, Object cookie, Context context, + public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, + OnImageLoadCompleteListener listener, Object cookie, Context context, ImageView imageView, Uri person, int placeholderImageResource) { - + // in case the source caller info is null, the URI will be null as well. // just update using the placeholder image in this case. if (person == null) { @@ -257,10 +263,10 @@ public class ContactsAsyncHelper extends Handler { imageView.setImageResource(placeholderImageResource); return; } - + // Added additional Cookie field in the callee to handle arguments // sent to the callback function. - + // setup arguments WorkerArgs args = new WorkerArgs(); args.cookie = cookie; @@ -270,15 +276,15 @@ public class ContactsAsyncHelper extends Handler { args.defaultResource = placeholderImageResource; args.listener = listener; args.info = info; - + // setup message arguments Message msg = sThreadHandler.obtainMessage(token); msg.arg1 = EVENT_LOAD_IMAGE; msg.obj = args; - - if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + + + if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + ", displaying default image for now."); - + // set the default image first, when the query is complete, we will // replace the image with the correct one. if (placeholderImageResource != -1) { @@ -287,11 +293,11 @@ public class ContactsAsyncHelper extends Handler { } else { imageView.setVisibility(View.INVISIBLE); } - + // notify the thread to begin working sThreadHandler.sendMessage(msg); } - + /** * Called when loading is done. */ @@ -316,21 +322,21 @@ public class ContactsAsyncHelper extends Handler { args.view.setVisibility(View.VISIBLE); args.view.setImageResource(args.defaultResource); } - + // Note that the data is cached. if (args.info != null) { args.info.isCachedPhotoCurrent = true; } - + // notify the listener if it is there. if (args.listener != null) { - if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + + if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + " image: " + args.uri + " completed"); args.listener.onImageLoadComplete(msg.what, args.cookie, args.view, imagePresent); } break; - default: + default: } } } diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 1a287c814dcac068e60f451306c689718d1a3cfe..bd7924aa621d023b887c8b029acdf2abad2eda46 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -223,6 +223,7 @@ public class RecurrenceSet { return true; } + // This can be removed when the old CalendarSyncAdapter is removed. public static boolean populateComponent(Cursor cursor, ICalendar.Component component) { @@ -292,6 +293,64 @@ public class RecurrenceSet { return true; } +public static boolean populateComponent(ContentValues values, + ICalendar.Component component) { + long dtstart = -1; + if (values.containsKey(Calendar.Events.DTSTART)) { + dtstart = values.getAsLong(Calendar.Events.DTSTART); + } + String duration = values.getAsString(Calendar.Events.DURATION); + String tzid = values.getAsString(Calendar.Events.EVENT_TIMEZONE); + String rruleStr = values.getAsString(Calendar.Events.RRULE); + String rdateStr = values.getAsString(Calendar.Events.RDATE); + String exruleStr = values.getAsString(Calendar.Events.EXRULE); + String exdateStr = values.getAsString(Calendar.Events.EXDATE); + boolean allDay = values.getAsInteger(Calendar.Events.ALL_DAY) == 1; + + if ((dtstart == -1) || + (TextUtils.isEmpty(duration))|| + ((TextUtils.isEmpty(rruleStr))&& + (TextUtils.isEmpty(rdateStr)))) { + // no recurrence. + return false; + } + + ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); + Time dtstartTime = null; + if (!TextUtils.isEmpty(tzid)) { + if (!allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); + } + dtstartTime = new Time(tzid); + } else { + // use the "floating" timezone + dtstartTime = new Time(Time.TIMEZONE_UTC); + } + + dtstartTime.set(dtstart); + // make sure the time is printed just as a date, if all day. + // TODO: android.pim.Time really should take care of this for us. + if (allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); + dtstartTime.allDay = true; + dtstartTime.hour = 0; + dtstartTime.minute = 0; + dtstartTime.second = 0; + } + + dtstartProp.setValue(dtstartTime.format2445()); + component.addProperty(dtstartProp); + ICalendar.Property durationProp = new ICalendar.Property("DURATION"); + durationProp.setValue(duration); + component.addProperty(durationProp); + + addPropertiesForRuleStr(component, "RRULE", rruleStr); + addPropertyForDateStr(component, "RDATE", rdateStr); + addPropertiesForRuleStr(component, "EXRULE", exruleStr); + addPropertyForDateStr(component, "EXDATE", exdateStr); + return true; + } + private static void addPropertiesForRuleStr(ICalendar.Component component, String propertyName, String ruleStr) { @@ -351,10 +410,14 @@ public class RecurrenceSet { Time end = new Time(endTzid); end.parse(dtendProperty.getValue()); - long durationMillis = end.toMillis(false /* use isDst */) + long durationMillis = end.toMillis(false /* use isDst */) - start.toMillis(false /* use isDst */); long durationSeconds = (durationMillis / 1000); - return "P" + durationSeconds + "S"; + if (start.allDay && (durationSeconds % 86400) == 0) { + return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S + } else { + return "P" + durationSeconds + "S"; + } } private static String flattenProperties(ICalendar.Component component, diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..ca41ce59af5d35520c5ec89b205fec30a3bb5035 --- /dev/null +++ b/core/java/android/pim/vcard/Constants.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +/** + * Constants used in both composer and parser. + */ +/* package */ class Constants { + + public static final String ATTR_TYPE = "TYPE"; + + public static final String VERSION_V21 = "2.1"; + public static final String VERSION_V30 = "3.0"; + + // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions + // shown in http://en.wikipedia.org/wiki/VCard support are defined here. + public static final String PROPERTY_X_AIM = "X-AIM"; + public static final String PROPERTY_X_MSN = "X-MSN"; + public static final String PROPERTY_X_YAHOO = "X-YAHOO"; + public static final String PROPERTY_X_ICQ = "X-ICQ"; + public static final String PROPERTY_X_JABBER = "X-JABBER"; + public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; + public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; + // Phone number for Skype, available as usual phone. + public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; + // Some device emits this "X-" attribute, which is specifically invalid but should be + // always properly accepted, and emitted in some special case (for that device/application). + public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; + + // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 + // + // e.g. + // 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..." + // 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..." + // 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..." + // + // 2) has been the default of VCard exporter/importer in Android, but we can see the other + // formats in vCard data emitted by the other softwares/devices. + // + // So we are currently not sure which type is the best; probably we will have to change which + // type should be emitted depending on the device. + public static final String ATTR_TYPE_HOME = "HOME"; + public static final String ATTR_TYPE_WORK = "WORK"; + public static final String ATTR_TYPE_FAX = "FAX"; + public static final String ATTR_TYPE_CELL = "CELL"; + public static final String ATTR_TYPE_VOICE = "VOICE"; + public static final String ATTR_TYPE_INTERNET = "INTERNET"; + + public static final String ATTR_TYPE_PREF = "PREF"; + + // Phone types valid in vCard and known to ContactsContract, but not so common. + public static final String ATTR_TYPE_CAR = "CAR"; + public static final String ATTR_TYPE_ISDN = "ISDN"; + public static final String ATTR_TYPE_PAGER = "PAGER"; + + // Phone types existing in vCard 2.1 but not known to ContactsContract. + // TODO: should make parser make these TYPE_CUSTOM. + public static final String ATTR_TYPE_MODEM = "MODEM"; + public static final String ATTR_TYPE_MSG = "MSG"; + public static final String ATTR_TYPE_BBS = "BBS"; + public static final String ATTR_TYPE_VIDEO = "VIDEO"; + + // Phone types existing in the current Contacts structure but not valid in vCard (at least 2.1) + // These types are encoded to "X-" attributes when composing vCard for now. + // Parser passes these even if "X-" is added to the attribute. + public static final String ATTR_TYPE_PHONE_EXTRA_OTHER = "OTHER"; + public static final String ATTR_TYPE_PHONE_EXTRA_CALLBACK = "CALLBACK"; + // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". + public static final String ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN = "COMPANY-MAIN"; + public static final String ATTR_TYPE_PHONE_EXTRA_RADIO = "RADIO"; + public static final String ATTR_TYPE_PHONE_EXTRA_TELEX = "TELEX"; + public static final String ATTR_TYPE_PHONE_EXTRA_TTY_TDD = "TTY-TDD"; + public static final String ATTR_TYPE_PHONE_EXTRA_ASSISTANT = "ASSISTANT"; + + // DoCoMo specific attribute. Used with "SOUND" property, which is alternate of SORT-STRING in + // vCard 3.0. + public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + + private Constants() { + } +} \ No newline at end of file diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java new file mode 100644 index 0000000000000000000000000000000000000000..36e5e233d6a327ece612191437a6fbdc1af3dc2d --- /dev/null +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import android.accounts.Account; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This class bridges between data structure of Contact app and VCard data. + */ +public class ContactStruct { + private static final String LOG_TAG = "vcard.ContactStruct"; + + // Key: the name shown in VCard. e.g. "X-AIM", "X-ICQ" + // Value: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} + private static final Map sImMap = new HashMap(); + + static { + sImMap.put(Constants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); + sImMap.put(Constants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); + sImMap.put(Constants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); + sImMap.put(Constants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); + sImMap.put(Constants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); + sImMap.put(Constants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); + sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + } + + /** + * @hide only for testing + */ + static public class PhoneData { + public final int type; + public final String data; + public final String label; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PhoneData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PhoneData) { + return false; + } + PhoneData phoneData = (PhoneData)obj; + return (type == phoneData.type && data.equals(phoneData.data) && + label.equals(phoneData.label) && isPrimary == phoneData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + /** + * @hide only for testing + */ + static public class EmailData { + public final int type; + public final String data; + // Used only when TYPE is TYPE_CUSTOM. + public final String label; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public EmailData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EmailData) { + return false; + } + EmailData emailData = (EmailData)obj; + return (type == emailData.type && data.equals(emailData.data) && + label.equals(emailData.label) && isPrimary == emailData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + static public class PostalData { + // Determined by vCard spec. + // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name + public static final int ADDR_MAX_DATA_SIZE = 7; + private final String[] dataArray; + public final String pobox; + public final String extendedAddress; + public final String street; + public final String localty; + public final String region; + public final String postalCode; + public final String country; + + public final int type; + + // Used only when type variable is TYPE_CUSTOM. + public final String label; + + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PostalData(int type, List propValueList, + String label, boolean isPrimary) { + this.type = type; + dataArray = new String[ADDR_MAX_DATA_SIZE]; + + int size = propValueList.size(); + if (size > ADDR_MAX_DATA_SIZE) { + size = ADDR_MAX_DATA_SIZE; + } + + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + // + // Use Iterator assuming List may be LinkedList, though actually it is + // always ArrayList in the current implementation. + int i = 0; + for (String addressElement : propValueList) { + dataArray[i] = addressElement; + if (++i >= size) { + break; + } + } + while (i < ADDR_MAX_DATA_SIZE) { + dataArray[i++] = null; + } + + this.pobox = dataArray[0]; + this.extendedAddress = dataArray[1]; + this.street = dataArray[2]; + this.localty = dataArray[3]; + this.region = dataArray[4]; + this.postalCode = dataArray[5]; + this.country = dataArray[6]; + + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PostalData) { + return false; + } + PostalData postalData = (PostalData)obj; + return (Arrays.equals(dataArray, postalData.dataArray) && + (type == postalData.type && + (type == StructuredPostal.TYPE_CUSTOM ? + (label == postalData.label) : true)) && + (isPrimary == postalData.isPrimary)); + } + + public String getFormattedAddress(int vcardType) { + StringBuilder builder = new StringBuilder(); + boolean empty = true; + if (VCardConfig.isJapaneseDevice(vcardType)) { + // In Japan, the order is reversed. + for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } else { + for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } + + return builder.toString().trim(); + } + + @Override + public String toString() { + return String.format("type: %d, label: %s, isPrimary: %s", + type, label, isPrimary); + } + } + + /** + * @hide only for testing. + */ + static public class OrganizationData { + public final int type; + public final String companyName; + // can be changed in some VCard format. + public String positionName; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public OrganizationData(int type, String companyName, String positionName, + boolean isPrimary) { + this.type = type; + this.companyName = companyName; + this.positionName = positionName; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof OrganizationData) { + return false; + } + OrganizationData organization = (OrganizationData)obj; + return (type == organization.type && companyName.equals(organization.companyName) && + positionName.equals(organization.positionName) && + isPrimary == organization.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, company: %s, position: %s, isPrimary: %s", + type, companyName, positionName, isPrimary); + } + } + + static public class ImData { + public final int type; + public final String data; + public final String label; + public final boolean isPrimary; + + // TODO: ContactsConstant#PROTOCOL, ContactsConstant#CUSTOM_PROTOCOL should be used? + public ImData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ImData) { + return false; + } + ImData imData = (ImData)obj; + return (type == imData.type && data.equals(imData.data) && + label.equals(imData.label) && isPrimary == imData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + /** + * @hide only for testing. + */ + static public class PhotoData { + public static final String FORMAT_FLASH = "SWF"; + public final int type; + public final String formatName; // used when type is not defined in ContactsContract. + public final byte[] photoBytes; + + public PhotoData(int type, String formatName, byte[] photoBytes) { + this.type = type; + this.formatName = formatName; + this.photoBytes = photoBytes; + } + } + + static /* package */ class Property { + private String mPropertyName; + private Map> mParameterMap = + new HashMap>(); + private List mPropertyValueList = new ArrayList(); + private byte[] mPropertyBytes; + + public Property() { + clear(); + } + + public void setPropertyName(final String propertyName) { + mPropertyName = propertyName; + } + + public void addParameter(final String paramName, final String paramValue) { + Collection values; + if (!mParameterMap.containsKey(paramName)) { + if (paramName.equals("TYPE")) { + values = new HashSet(); + } else { + values = new ArrayList(); + } + mParameterMap.put(paramName, values); + } else { + values = mParameterMap.get(paramName); + } + values.add(paramValue); + } + + public void addToPropertyValueList(final String propertyValue) { + mPropertyValueList.add(propertyValue); + } + + public void setPropertyBytes(final byte[] propertyBytes) { + mPropertyBytes = propertyBytes; + } + + public final Collection getParameters(String type) { + return mParameterMap.get(type); + } + + public final List getPropertyValueList() { + return mPropertyValueList; + } + + public void clear() { + mPropertyName = null; + mParameterMap.clear(); + mPropertyValueList.clear(); + } + } + + private String mFamilyName; + private String mGivenName; + private String mMiddleName; + private String mPrefix; + private String mSuffix; + + // Used only when no family nor given name is found. + private String mFullName; + + private String mPhoneticFamilyName; + private String mPhoneticGivenName; + private String mPhoneticMiddleName; + + private String mPhoneticFullName; + + private List mNickNameList; + + private String mDisplayName; + + private String mBirthday; + + private List mNoteList; + private List mPhoneList; + private List mEmailList; + private List mPostalList; + private List mOrganizationList; + private List mImList; + private List mPhotoList; + private List mWebsiteList; + + private final int mVCardType; + private final Account mAccount; + + // Each Column of four properties has ISPRIMARY field + // (See android.provider.Contacts) + // If false even after the parsing loop, we choose the first entry as a "primary" + // entry. + private boolean mPrefIsSet_Address; + private boolean mPrefIsSet_Phone; + private boolean mPrefIsSet_Email; + private boolean mPrefIsSet_Organization; + + public ContactStruct() { + this(VCardConfig.VCARD_TYPE_V21_GENERIC); + } + + public ContactStruct(int vcardType) { + this(vcardType, null); + } + + public ContactStruct(int vcardType, Account account) { + mVCardType = vcardType; + mAccount = account; + } + + /** + * @hide only for testing. + */ + public ContactStruct(String givenName, + String familyName, + String middleName, + String prefix, + String suffix, + String phoneticGivenName, + String pheneticFamilyName, + String phoneticMiddleName, + List photoBytesList, + List notes, + List phoneList, + List emailList, + List postalList, + List organizationList, + List photoList, + List websiteList) { + this(VCardConfig.VCARD_TYPE_DEFAULT); + mGivenName = givenName; + mFamilyName = familyName; + mPrefix = prefix; + mSuffix = suffix; + mPhoneticGivenName = givenName; + mPhoneticFamilyName = familyName; + mPhoneticMiddleName = middleName; + mEmailList = emailList; + mPostalList = postalList; + mOrganizationList = organizationList; + mPhotoList = photoList; + mWebsiteList = websiteList; + } + + // All getter methods should be used carefully, since they may change + // in the future as of 2009-09-24, on which I cannot be sure this structure + // is completely consolidated. + // When we are sure we will no longer change them, we'll be happy to + // make it complete public (withouth @hide tag) + // + // Also note that these getter methods should be used only after + // all properties being pushed into this object. If not, incorrect + // value will "be stored in the local cache and" be returned to you. + + /** + * @hide + */ + public String getFamilyName() { + return mFamilyName; + } + + /** + * @hide + */ + public String getGivenName() { + return mGivenName; + } + + /** + * @hide + */ + public String getMiddleName() { + return mMiddleName; + } + + /** + * @hide + */ + public String getPrefix() { + return mPrefix; + } + + /** + * @hide + */ + public String getSuffix() { + return mSuffix; + } + + /** + * @hide + */ + public String getFullName() { + return mFullName; + } + + /** + * @hide + */ + public String getPhoneticFamilyName() { + return mPhoneticFamilyName; + } + + /** + * @hide + */ + public String getPhoneticGivenName() { + return mPhoneticGivenName; + } + + /** + * @hide + */ + public String getPhoneticMiddleName() { + return mPhoneticMiddleName; + } + + /** + * @hide + */ + public String getPhoneticFullName() { + return mPhoneticFullName; + } + + /** + * @hide + */ + public final List getNickNameList() { + return mNickNameList; + } + + /** + * @hide + */ + public String getDisplayName() { + if (mDisplayName == null) { + constructDisplayName(); + } + return mDisplayName; + } + + /** + * @hide + */ + public String getBirthday() { + return mBirthday; + } + + /** + * @hide + */ + public final List getPhotoList() { + return mPhotoList; + } + + /** + * @hide + */ + public final List getNotes() { + return mNoteList; + } + + /** + * @hide + */ + public final List getPhoneList() { + return mPhoneList; + } + + /** + * @hide + */ + public final List getEmailList() { + return mEmailList; + } + + /** + * @hide + */ + public final List getPostalList() { + return mPostalList; + } + + /** + * @hide + */ + public final List getOrganizationList() { + return mOrganizationList; + } + + /** + * Add a phone info to phoneList. + * @param data phone number + * @param type type col of content://contacts/phones + * @param label lable col of content://contacts/phones + */ + private void addPhone(int type, String data, String label, boolean isPrimary){ + if (mPhoneList == null) { + mPhoneList = new ArrayList(); + } + StringBuilder builder = new StringBuilder(); + String trimed = data.trim(); + int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } + } + + PhoneData phoneData = new PhoneData(type, + PhoneNumberUtils.formatNumber(builder.toString()), + label, isPrimary); + + mPhoneList.add(phoneData); + } + + private void addNickName(final String nickName) { + if (mNickNameList == null) { + mNickNameList = new ArrayList(); + } + mNickNameList.add(nickName); + } + + private void addEmail(int type, String data, String label, boolean isPrimary){ + if (mEmailList == null) { + mEmailList = new ArrayList(); + } + mEmailList.add(new EmailData(type, data, label, isPrimary)); + } + + private void addPostal(int type, List propValueList, String label, boolean isPrimary){ + if (mPostalList == null) { + mPostalList = new ArrayList(); + } + mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); + } + + private void addOrganization(int type, final String companyName, + final String positionName, boolean isPrimary) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList(); + } + mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); + } + + private void addIm(int type, String data, String label, boolean isPrimary) { + if (mImList == null) { + mImList = new ArrayList(); + } + mImList.add(new ImData(type, data, label, isPrimary)); + } + + private void addNote(final String note) { + if (mNoteList == null) { + mNoteList = new ArrayList(1); + } + mNoteList.add(note); + } + + private void addPhotoBytes(String formatName, byte[] photoBytes) { + if (mPhotoList == null) { + mPhotoList = new ArrayList(1); + } + final PhotoData photoData = new PhotoData(0, null, photoBytes); + mPhotoList.add(photoData); + } + + /** + * Set "position" value to the appropriate data. If there's more than one + * OrganizationData objects, the value is set to the last one. If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * empty. + * + * TODO: incomplete logic. fix this: + * + * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not + * know how to handle it in general cases... + * ---- + * TITLE:Software Engineer + * ORG:Google + * ---- + */ + private void setPosition(String positionValue) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList(); + } + int size = mOrganizationList.size(); + if (size == 0) { + addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER, + "", null, false); + size = 1; + } + OrganizationData lastData = mOrganizationList.get(size - 1); + lastData.positionName = positionValue; + } + + @SuppressWarnings("fallthrough") + private void handleNProperty(List elems) { + // Family, Given, Middle, Prefix, Suffix. (1 - 5) + int size; + if (elems == null || (size = elems.size()) < 1) { + return; + } + if (size > 5) { + size = 5; + } + + switch (size) { + // fallthrough + case 5: + mSuffix = elems.get(4); + case 4: + mPrefix = elems.get(3); + case 3: + mMiddleName = elems.get(2); + case 2: + mGivenName = elems.get(1); + default: + mFamilyName = elems.get(0); + } + } + + /** + * Some Japanese mobile phones use this field for phonetic name, + * since vCard 2.1 does not have "SORT-STRING" type. + * Also, in some cases, the field has some ';'s in it. + * Assume the ';' means the same meaning in N property + */ + @SuppressWarnings("fallthrough") + private void handlePhoneticNameFromSound(List elems) { + // Family, Given, Middle. (1-3) + // This is not from specification but mere assumption. Some Japanese phones use this order. + int size; + if (elems == null || (size = elems.size()) < 1) { + return; + } + if (size > 3) { + size = 3; + } + + switch (size) { + // fallthrough + case 3: + mPhoneticMiddleName = elems.get(2); + case 2: + mPhoneticGivenName = elems.get(1); + default: + mPhoneticFamilyName = elems.get(0); + } + } + + public void addProperty(Property property) { + String propName = property.mPropertyName; + final Map> paramMap = property.mParameterMap; + final List propValueList = property.mPropertyValueList; + byte[] propBytes = property.mPropertyBytes; + + if (propValueList.size() == 0) { + return; + } + final String propValue = listToString(propValueList).trim(); + + if (propName.equals("VERSION")) { + // vCard version. Ignore this. + } else if (propName.equals("FN")) { + mFullName = propValue; + } else if (propName.equals("NAME") && mFullName == null) { + // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not + // actually exist in the real vCard data, does not exist. + mFullName = propValue; + } else if (propName.equals("N")) { + handleNProperty(propValueList); + } else if (propName.equals("SORT-STRING")) { + mPhoneticFullName = propValue; + } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + addNickName(propValue); + } else if (propName.equals("SOUND")) { + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) { + handlePhoneticNameFromSound(propValueList); + } else { + // Ignore this field since Android cannot understand what it is. + } + } else if (propName.equals("ADR")) { + boolean valuesAreAllEmpty = true; + for (String value : propValueList) { + if (value.length() > 0) { + valuesAreAllEmpty = false; + break; + } + } + if (valuesAreAllEmpty) { + return; + } + + int type = -1; + String label = ""; + boolean isPrimary = false; + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { + // Only first "PREF" is considered. + mPrefIsSet_Address = true; + isPrimary = true; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = StructuredPostal.TYPE_HOME; + label = ""; + } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || + typeString.equalsIgnoreCase("COMPANY")) { + // "COMPANY" seems emitted by Windows Mobile, which is not + // specifically supported by vCard 2.1. We assume this is same + // as "WORK". + type = StructuredPostal.TYPE_WORK; + label = ""; + } else if (typeString.equals("PARCEL") || + typeString.equals("DOM") || + typeString.equals("INTL")) { + // We do not have any appropriate way to store this information. + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters + // emit non-standard types. We do not handle their values now. + type = StructuredPostal.TYPE_CUSTOM; + label = typeString; + } + } + } + // We use "HOME" as default + if (type < 0) { + type = StructuredPostal.TYPE_HOME; + } + + addPostal(type, propValueList, label, isPrimary); + } else if (propName.equals("EMAIL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { + // Only first "PREF" is considered. + mPrefIsSet_Email = true; + isPrimary = true; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = Email.TYPE_HOME; + } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + type = Email.TYPE_WORK; + } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + type = Email.TYPE_MOBILE; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + // vCard 3.0 allows iana-token. + // We may have INTERNET (specified in vCard spec), + // SCHOOL, etc. + type = Email.TYPE_CUSTOM; + label = typeString; + } + } + } + if (type < 0) { + type = Email.TYPE_OTHER; + } + addEmail(type, propValue, label, isPrimary); + } else if (propName.equals("ORG")) { + // vCard specification does not specify other types. + int type = Organization.TYPE_WORK; + boolean isPrimary = false; + + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { + // vCard specification officially does not have PREF in ORG. + // This is just for safety. + mPrefIsSet_Organization = true; + isPrimary = true; + } + } + } + + StringBuilder builder = new StringBuilder(); + for (Iterator iter = propValueList.iterator(); iter.hasNext();) { + builder.append(iter.next()); + if (iter.hasNext()) { + builder.append(' '); + } + } + addOrganization(type, builder.toString(), "", isPrimary); + } else if (propName.equals("TITLE")) { + setPosition(propValue); + } else if (propName.equals("ROLE")) { + setPosition(propValue); + } else if (propName.equals("PHOTO") || propName.equals("LOGO")) { + String formatName = null; + Collection typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + formatName = typeCollection.iterator().next(); + } + Collection paramMapValue = paramMap.get("VALUE"); + if (paramMapValue != null && paramMapValue.contains("URL")) { + // Currently we do not have appropriate example for testing this case. + } else { + addPhotoBytes(formatName, propBytes); + } + } else if (propName.equals("TEL")) { + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); + final int type; + final String label; + if (typeObject instanceof Integer) { + type = (Integer)typeObject; + label = null; + } else { + type = Phone.TYPE_CUSTOM; + label = typeObject.toString(); + } + + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; + } + addPhone(type, propValue, label, isPrimary); + } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { + // The phone number available via Skype. + Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) + int type = Phone.TYPE_OTHER; + final String label = null; + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; + } + addPhone(type, propValue, label, isPrimary); + } else if (sImMap.containsKey(propName)){ + int type = sImMap.get(propName); + boolean isPrimary = false; + final Collection typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + isPrimary = true; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { + type = Phone.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { + type = Phone.TYPE_WORK; + } + } + } + if (type < 0) { + type = Phone.TYPE_HOME; + } + addIm(type, propValue, null, isPrimary); + } else if (propName.equals("NOTE")) { + addNote(propValue); + } else if (propName.equals("URL")) { + if (mWebsiteList == null) { + mWebsiteList = new ArrayList(1); + } + mWebsiteList.add(propValue); + } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { + mPhoneticGivenName = propValue; + } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + mPhoneticMiddleName = propValue; + } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + mPhoneticFamilyName = propValue; + } else if (propName.equals("BDAY")) { + mBirthday = propValue; + /*} else if (propName.equals("REV")) { + // Revision of this VCard entry. I think we can ignore this. + } else if (propName.equals("UID")) { + } else if (propName.equals("KEY")) { + // Type is X509 or PGP? I don't know how to handle this... + } else if (propName.equals("MAILER")) { + } else if (propName.equals("TZ")) { + } else if (propName.equals("GEO")) { + } else if (propName.equals("CLASS")) { + // vCard 3.0 only. + // e.g. CLASS:CONFIDENTIAL + } else if (propName.equals("PROFILE")) { + // VCard 3.0 only. Must be "VCARD". I think we can ignore this. + } else if (propName.equals("CATEGORIES")) { + // VCard 3.0 only. + // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY + } else if (propName.equals("SOURCE")) { + // VCard 3.0 only. + } else if (propName.equals("PRODID")) { + // VCard 3.0 only. + // To specify the identifier for the product that created + // the vCard object.*/ + } else { + // Unknown X- words and IANA token. + } + } + + /** + * Construct the display name. The constructed data must not be null. + */ + private void constructDisplayName() { + if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { + StringBuilder builder = new StringBuilder(); + List nameList; + switch (VCardConfig.getNameOrderType(mVCardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + if (VCardUtils.containsOnlyPrintableAscii(mFamilyName) && + VCardUtils.containsOnlyPrintableAscii(mGivenName)) { + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + } else { + nameList = Arrays.asList(mPrefix, mFamilyName, mMiddleName, mGivenName, mSuffix); + } + break; + case VCardConfig.NAME_ORDER_EUROPE: + nameList = Arrays.asList(mPrefix, mMiddleName, mGivenName, mFamilyName, mSuffix); + break; + default: + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + break; + } + boolean first = true; + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + mDisplayName = builder.toString(); + } else if (!TextUtils.isEmpty(mFullName)) { + mDisplayName = mFullName; + } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && + TextUtils.isEmpty(mPhoneticGivenName))) { + mDisplayName = VCardUtils.constructNameFromElements(mVCardType, + mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName); + } else if (mEmailList != null && mEmailList.size() > 0) { + mDisplayName = mEmailList.get(0).data; + } else if (mPhoneList != null && mPhoneList.size() > 0) { + mDisplayName = mPhoneList.get(0).data; + } else if (mPostalList != null && mPostalList.size() > 0) { + mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); + } + + if (mDisplayName == null) { + mDisplayName = ""; + } + } + + /** + * Consolidate several fielsds (like mName) using name candidates, + */ + public void consolidateFields() { + constructDisplayName(); + + if (mPhoneticFullName != null) { + mPhoneticFullName = mPhoneticFullName.trim(); + } + + // If there is no "PREF", we choose the first entries as primary. + if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) { + mPhoneList.get(0).isPrimary = true; + } + + if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) { + mPostalList.get(0).isPrimary = true; + } + if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) { + mEmailList.get(0).isPrimary = true; + } + if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) { + mOrganizationList.get(0).isPrimary = true; + } + } + + // From GoogleSource.java in Contacts app. + private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; + private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; + + public void pushIntoContentResolver(ContentResolver resolver) { + ArrayList operationList = + new ArrayList(); + ContentProviderOperation.Builder builder = + ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); + String myGroupsId = null; + if (mAccount != null) { + builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); + builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); + + // Assume that caller side creates this group if it does not exist. + // TODO: refactor this code along with the change in GoogleSource.java + if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { + final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { + Groups.SOURCE_ID }, + Groups.TITLE + "=?", new String[] { + GOOGLE_MY_CONTACTS_GROUP }, null); + try { + if (cursor != null && cursor.moveToFirst()) { + myGroupsId = cursor.getString(0); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } else { + builder.withValue(RawContacts.ACCOUNT_NAME, null); + builder.withValue(RawContacts.ACCOUNT_TYPE, null); + } + operationList.add(builder.build()); + + { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredName.GIVEN_NAME, mGivenName); + builder.withValue(StructuredName.FAMILY_NAME, mFamilyName); + builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName); + builder.withValue(StructuredName.PREFIX, mPrefix); + builder.withValue(StructuredName.SUFFIX, mSuffix); + + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); + builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); + builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + + builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); + operationList.add(builder.build()); + } + + if (mNickNameList != null && mNickNameList.size() > 0) { + boolean first = true; + for (String nickName : mNickNameList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); + + builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); + builder.withValue(Nickname.NAME, nickName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); + } + } + + if (mPhoneList != null) { + for (PhoneData phoneData : mPhoneList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); + + builder.withValue(Phone.TYPE, phoneData.type); + if (phoneData.type == Phone.TYPE_CUSTOM) { + builder.withValue(Phone.LABEL, phoneData.label); + } + builder.withValue(Phone.NUMBER, phoneData.data); + if (phoneData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mOrganizationList != null) { + boolean first = true; + for (OrganizationData organizationData : mOrganizationList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); + + // Currently, we do not use TYPE_CUSTOM. + builder.withValue(Organization.TYPE, organizationData.type); + builder.withValue(Organization.COMPANY, organizationData.companyName); + builder.withValue(Organization.TITLE, organizationData.positionName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mEmailList != null) { + for (EmailData emailData : mEmailList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Email.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); + + builder.withValue(Email.TYPE, emailData.type); + if (emailData.type == Email.TYPE_CUSTOM) { + builder.withValue(Email.LABEL, emailData.label); + } + builder.withValue(Email.DATA, emailData.data); + if (emailData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mPostalList != null) { + for (PostalData postalData : mPostalList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + VCardUtils.insertStructuredPostalDataUsingContactsStruct( + mVCardType, builder, postalData); + operationList.add(builder.build()); + } + } + + if (mImList != null) { + for (ImData imData : mImList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); + + builder.withValue(Im.TYPE, imData.type); + if (imData.type == Im.TYPE_CUSTOM) { + builder.withValue(Im.LABEL, imData.label); + } + builder.withValue(Im.DATA, imData.data); + if (imData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + } + + if (mNoteList != null) { + for (String note : mNoteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); + + builder.withValue(Note.NOTE, note); + operationList.add(builder.build()); + } + } + + if (mPhotoList != null) { + boolean first = true; + for (PhotoData photoData : mPhotoList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); + builder.withValue(Photo.PHOTO, photoData.photoBytes); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); + } + } + + if (mWebsiteList != null) { + for (String website : mWebsiteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Website.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); + builder.withValue(Website.URL, website); + // There's no information about the type of URL in vCard. + // We use TYPE_HOME for safety. + builder.withValue(Website.TYPE, Website.TYPE_HOME); + operationList.add(builder.build()); + } + } + + if (!TextUtils.isEmpty(mBirthday)) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); + builder.withValue(Event.START_DATE, mBirthday); + builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); + operationList.add(builder.build()); + } + + if (myGroupsId != null) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); + builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId); + operationList.add(builder.build()); + } + + try { + resolver.applyBatch(ContactsContract.AUTHORITY, operationList); + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + } + + public boolean isIgnorable() { + return getDisplayName().length() == 0; + } + + private String listToString(List list){ + final int size = list.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + int i = 0; + for (String type : list) { + builder.append(type); + if (i < size - 1) { + builder.append(";"); + } + } + return builder.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; + } + } +} diff --git a/core/java/android/pim/vcard/EntryCommitter.java b/core/java/android/pim/vcard/EntryCommitter.java new file mode 100644 index 0000000000000000000000000000000000000000..3f1655d1cefeb31b9067895331d191f21474d06b --- /dev/null +++ b/core/java/android/pim/vcard/EntryCommitter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import android.content.ContentResolver; +import android.util.Log; + +/** + * EntryHandler implementation which commits the entry to Contacts Provider + */ +public class EntryCommitter implements EntryHandler { + public static String LOG_TAG = "vcard.EntryComitter"; + + private ContentResolver mContentResolver; + private long mTimeToCommit; + + public EntryCommitter(ContentResolver resolver) { + mContentResolver = resolver; + } + + public void onParsingStart() { + } + + public void onParsingEnd() { + if (VCardConfig.showPerformanceLog()) { + Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); + } + } + + public void onEntryCreated(final ContactStruct contactStruct) { + long start = System.currentTimeMillis(); + contactStruct.pushIntoContentResolver(mContentResolver); + mTimeToCommit += System.currentTimeMillis() - start; + } +} \ No newline at end of file diff --git a/core/java/android/pim/vcard/EntryHandler.java b/core/java/android/pim/vcard/EntryHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7fb81149bd9dfd689b373528ea807aa8c5c5570e --- /dev/null +++ b/core/java/android/pim/vcard/EntryHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +/** + * Unlike {@link VCardBuilder}, this (and {@link VCardDataBuilder}) assumes + * "each VCard entry should be correctly parsed and passed to each EntryHandler object", + */ +public interface EntryHandler { + /** + * Called when the parsing started. + */ + public void onParsingStart(); + + /** + * The method called when one VCard entry is successfully created + */ + public void onEntryCreated(final ContactStruct entry); + + /** + * Called when the parsing ended. + * Able to be use this method for showing performance log, etc. + */ + public void onParsingEnd(); +} diff --git a/core/java/android/syncml/pim/VBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java similarity index 87% rename from core/java/android/syncml/pim/VBuilder.java rename to core/java/android/pim/vcard/VCardBuilder.java index 452864526608f8a850f1bdac0b750d3b843ae388..e1c4b338e501c5cedd625e35e4212c14f42951cc 100644 --- a/core/java/android/syncml/pim/VBuilder.java +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.syncml.pim; +package android.pim.vcard; import java.util.List; -public interface VBuilder { +public interface VCardBuilder { void start(); void end(); - /** - * @param type - * VXX
    - * BEGIN:VXX + /** + * BEGIN:VCARD */ void startRecord(String type); diff --git a/core/java/android/syncml/pim/VBuilderCollection.java b/core/java/android/pim/vcard/VCardBuilderCollection.java similarity index 61% rename from core/java/android/syncml/pim/VBuilderCollection.java rename to core/java/android/pim/vcard/VCardBuilderCollection.java index f09c1c49445c4c193cd94a2555599cb21ce58ede..e3985b600e6972a84ff521be6c5019cf20537f67 100644 --- a/core/java/android/syncml/pim/VBuilderCollection.java +++ b/core/java/android/pim/vcard/VCardBuilderCollection.java @@ -13,87 +13,86 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.syncml.pim; +package android.pim.vcard; import java.util.Collection; import java.util.List; -public class VBuilderCollection implements VBuilder { +public class VCardBuilderCollection implements VCardBuilder { - private final Collection mVBuilderCollection; + private final Collection mVCardBuilderCollection; - public VBuilderCollection(Collection vBuilderCollection) { - mVBuilderCollection = vBuilderCollection; + public VCardBuilderCollection(Collection vBuilderCollection) { + mVCardBuilderCollection = vBuilderCollection; } - public Collection getVBuilderCollection() { - return mVBuilderCollection; + public Collection getVCardBuilderBaseCollection() { + return mVCardBuilderCollection; } public void start() { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.start(); } } public void end() { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.end(); } } public void startRecord(String type) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.startRecord(type); } } public void endRecord() { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.endRecord(); } } public void startProperty() { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.startProperty(); } } public void endProperty() { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.endProperty(); } } public void propertyGroup(String group) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.propertyGroup(group); } } public void propertyName(String name) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.propertyName(name); } } public void propertyParamType(String type) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.propertyParamType(type); } } public void propertyParamValue(String value) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.propertyParamValue(value); } } public void propertyValues(List values) { - for (VBuilder builder : mVBuilderCollection) { + for (VCardBuilder builder : mVCardBuilderCollection) { builder.propertyValues(values); } } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java new file mode 100644 index 0000000000000000000000000000000000000000..f9dce255056e12c93f07bd0c4e8086a9fc3a5cc3 --- /dev/null +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -0,0 +1,2049 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.pim.vcard; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.EntityIterator; +import android.content.Entity.NamedContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.format.Time; +import android.util.CharsetUtils; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +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 class for composing VCard from Contacts information. Note that this is + * completely differnt implementation from + * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. + *

    + * + *

    + * Usually, this class should be used like this. + *

    + * + *
     VCardComposer composer = null; try { composer = new
    + * VCardComposer(context); composer.addHandler(composer.new
    + * HandlerForOutputStream(outputStream)); if (!composer.init()) { // Do
    + * something handling the situation. return; } while (!composer.isAfterLast()) {
    + * if (mCanceled) { // Assume a user may cancel this operation during the
    + * export. return; } if (!composer.createOneEntry()) { // Do something handling
    + * the error situation. return; } } } finally { if (composer != null) {
    + * composer.terminate(); } } 
    + */ +public class VCardComposer { + private static final String LOG_TAG = "vcard.VCardComposer"; + + private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + + public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = + "Failed to get database information"; + + public static final String FAILURE_REASON_NO_ENTRY = + "There's no exportable in the database"; + + public static final String FAILURE_REASON_NOT_INITIALIZED = + "The vCard composer object is not correctly initialized"; + + public static final String NO_ERROR = "No error"; + + private static final Uri sDataRequestUri; + + static { + Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); + builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); + sDataRequestUri = builder.build(); + } + + public static interface OneEntryHandler { + public boolean onInit(Context context); + + public boolean onEntryCreated(String vcard); + + public void onTerminate(); + } + + /** + *

    + * An useful example handler, which emits VCard String to outputstream one + * by one. + *

    + *

    + * The input OutputStream object is closed() on {{@link #onTerminate()}. + * Must not close the stream outside. + *

    + */ + public class HandlerForOutputStream implements OneEntryHandler { + @SuppressWarnings("hiding") + private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; + + final private OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + + private boolean mOnTerminateIsCalled = false; + + /** + * Input stream will be closed on the detruction of this object. + */ + public HandlerForOutputStream(OutputStream outputStream) { + mOutputStream = outputStream; + } + + public boolean onInit(Context context) { + try { + mWriter = new BufferedWriter(new OutputStreamWriter( + mOutputStream, mCharsetString)); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); + mErrorReason = "Encoding is not supported (usually this does not happen!): " + + mCharsetString; + return false; + } + + if (mIsDoCoMo) { + try { + // Create one empty entry. + mWriter.write(createOneEntryInternal("-1")); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + } + return true; + } + + public boolean onEntryCreated(String vcard) { + try { + mWriter.write(vcard); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + return true; + } + + public void onTerminate() { + mOnTerminateIsCalled = true; + if (mWriter != null) { + try { + // Flush and sync the data so that a user is able to pull + // the SDCard just after + // the export. + mWriter.flush(); + if (mOutputStream != null + && mOutputStream instanceof FileOutputStream) { + ((FileOutputStream) mOutputStream).getFD().sync(); + } + } catch (IOException e) { + Log.d(LOG_TAG, + "IOException during closing the output stream: " + + e.getMessage()); + } finally { + try { + mWriter.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void finalize() { + if (!mOnTerminateIsCalled) { + onTerminate(); + } + } + } + + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + private static final String VCARD_PROPERTY_ADR = "ADR"; + private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; + private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; + private static final String VCARD_PROPERTY_END = "END"; + private static final String VCARD_PROPERTY_NAME = "N"; + private static final String VCARD_PROPERTY_FULL_NAME = "FN"; + private static final String VCARD_PROPERTY_NOTE = "NOTE"; + private static final String VCARD_PROPERTY_ORG = "ORG"; + private static final String VCARD_PROPERTY_SOUND = "SOUND"; + private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; + private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; + private static final String VCARD_PROPERTY_TEL = "TEL"; + private static final String VCARD_PROPERTY_TITLE = "TITLE"; + private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; + private static final String VCARD_PROPERTY_VERSION = "VERSION"; + private static final String VCARD_PROPERTY_URL = "URL"; + private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; + + private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Android specific properties + // TODO: ues extra MIME-TYPE instead of adding this kind of inflexible fields + private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; + + // Property for call log entry + private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; + private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; + private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; + private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; + + // Properties for DoCoMo vCard. + private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; + private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; + private static final String VCARD_PROPERTY_X_NO = "X-NO"; + private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + + private static final String VCARD_DATA_VCARD = "VCARD"; + private static final String VCARD_DATA_PUBLIC = "PUBLIC"; + + private static final String VCARD_ATTR_SEPARATOR = ";"; + private static final String VCARD_COL_SEPARATOR = "\r\n"; + private static final String VCARD_DATA_SEPARATOR = ":"; + private static final String VCARD_ITEM_SEPARATOR = ";"; + private static final String VCARD_WS = " "; + private static final String VCARD_ATTR_EQUAL = "="; + + // Type strings are now in VCardConstants.java. + + private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; + + private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; + private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + + private final Context mContext; + private final int mVCardType; + private final boolean mCareHandlerErrors; + private final ContentResolver mContentResolver; + + // Convenient member variables about the restriction of the vCard format. + // Used for not calling the same methods returning same results. + private final boolean mIsV30; + private final boolean mIsJapaneseMobilePhone; + private final boolean mOnlyOneNoteFieldIsAvailable; + private final boolean mIsDoCoMo; + private final boolean mUsesQuotedPrintable; + private final boolean mUsesAndroidProperty; + private final boolean mUsesDefactProperty; + private final boolean mUsesUtf8; + private final boolean mUsesShiftJis; + private final boolean mUsesQPToPrimaryProperties; + + private Cursor mCursor; + private int mIdColumn; + + private final String mCharsetString; + private final String mVCardAttributeCharset; + private boolean mTerminateIsCalled; + final private List mHandlerList; + + private String mErrorReason = NO_ERROR; + + private static final Map sImMap; + + static { + sImMap = new HashMap(); + sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + // Google talk is a special case. + } + + private boolean mIsCallLogComposer = false; + + private boolean mNeedPhotoForVCard = true; + + private static final String[] sContactsProjection = new String[] { + Contacts._ID, + }; + + /** The projection to use when querying the call log table */ + private static final String[] sCallLogProjection = new String[] { + Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE, + Calls.CACHED_NUMBER_LABEL + }; + private static final int NUMBER_COLUMN_INDEX = 0; + private static final int DATE_COLUMN_INDEX = 1; + private static final int CALL_TYPE_COLUMN_INDEX = 2; + private static final int CALLER_NAME_COLUMN_INDEX = 3; + private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; + private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; + + private static final String FLAG_TIMEZONE_UTC = "Z"; + + public VCardComposer(Context context) { + this(context, VCardConfig.VCARD_TYPE_DEFAULT, true, false, true); + } + + public VCardComposer(Context context, String vcardTypeStr, + boolean careHandlerErrors) { + this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), + careHandlerErrors, false, true); + } + + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { + this(context, vcardType, careHandlerErrors, false, true); + } + + /** + * Construct for supporting call log entry vCard composing. + * + * @param isCallLogComposer true if this composer is for creating Call Log vCard. + */ + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors, + boolean isCallLogComposer, boolean needPhotoInVCard) { + mContext = context; + mVCardType = vcardType; + mCareHandlerErrors = careHandlerErrors; + mIsCallLogComposer = isCallLogComposer; + mNeedPhotoForVCard = needPhotoInVCard; + mContentResolver = context.getContentResolver(); + + mIsV30 = VCardConfig.isV30(vcardType); + mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mIsJapaneseMobilePhone = VCardConfig + .needsToConvertPhoneticString(vcardType); + mOnlyOneNoteFieldIsAvailable = VCardConfig + .onlyOneNoteFieldIsAvailable(vcardType); + mUsesAndroidProperty = VCardConfig + .usesAndroidSpecificProperty(vcardType); + mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); + mUsesUtf8 = VCardConfig.usesUtf8(vcardType); + mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); + mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); + mHandlerList = new ArrayList(); + + if (mIsDoCoMo) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but + // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in + // Android, not shown to the public). + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else if (mUsesShiftJis) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else { + mCharsetString = "UTF-8"; + mVCardAttributeCharset = "CHARSET=UTF-8"; + } + } + + /** + * This static function is to compose vCard for phone own number + */ + public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, + String phoneNumber, boolean vcardVer21) { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (!vcardVer21) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + + boolean needCharset = false; + if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { + needCharset = true; + } + // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, phoneName, needCharset, false); + appendVCardLine(builder, VCARD_PROPERTY_NAME, phoneName, needCharset, false); + + String label = Integer.toString(phonetype); + appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); + + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + /** + * Must call before {{@link #init()}. + */ + public void addHandler(OneEntryHandler handler) { + mHandlerList.add(handler); + } + + public boolean init() { + return init(null, null); + } + + /** + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. + */ + public boolean init(final String selection, final String[] selectionArgs) { + if (mCareHandlerErrors) { + List finishedList = new ArrayList( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onInit(mContext)) { + for (OneEntryHandler finished : finishedList) { + finished.onTerminate(); + } + return false; + } + } + } else { + // Just ignore the false returned from onInit(). + for (OneEntryHandler handler : mHandlerList) { + handler.onInit(mContext); + } + } + + if (mIsCallLogComposer) { + mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection, + selection, selectionArgs, null); + } else { + mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection, + selection, selectionArgs, null); + } + + if (mCursor == null) { + mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; + return false; + } + + if (getCount() == 0 || !mCursor.moveToFirst()) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); + } finally { + mCursor = null; + mErrorReason = FAILURE_REASON_NO_ENTRY; + } + return false; + } + + if (mIsCallLogComposer) { + mIdColumn = -1; + } else { + mIdColumn = mCursor.getColumnIndex(Contacts._ID); + } + + return true; + } + + public boolean createOneEntry() { + if (mCursor == null || mCursor.isAfterLast()) { + mErrorReason = FAILURE_REASON_NOT_INITIALIZED; + return false; + } + String name = null; + String vcard; + try { + if (mIsCallLogComposer) { + vcard = createOneCallLogEntryInternal(); + } else { + if (mIdColumn >= 0) { + vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); + } else { + Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); + return true; + } + } + } catch (OutOfMemoryError error) { + // Maybe some data (e.g. photo) is too big to have in memory. But it + // should be rare. + Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + + name); + System.gc(); + // TODO: should tell users what happened? + return true; + } finally { + mCursor.moveToNext(); + } + + // This function does not care the OutOfMemoryError on the handler side + // :-P + if (mCareHandlerErrors) { + List finishedList = new ArrayList( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onEntryCreated(vcard)) { + return false; + } + } + } else { + for (OneEntryHandler handler : mHandlerList) { + handler.onEntryCreated(vcard); + } + } + + return true; + } + + /** + * Format according to RFC 2445 DATETIME type. + * The format is: ("%Y%m%dT%H%M%SZ"). + */ + private final String toRfc2455Format(final long millSecs) { + Time startDate = new Time(); + startDate.set(millSecs); + String date = startDate.format2445(); + return date + FLAG_TIMEZONE_UTC; + } + + /** + * Try to append the property line for a call history time stamp field if possible. + * Do nothing if the call log type gotton from the database is invalid. + */ + private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { + // Extension for call history as defined in + // in the Specification for Ic Mobile Communcation - ver 1.1, + // Oct 2000. This is used to send the details of the call + // history - missed, incoming, outgoing along with date and time + // to the requesting device (For example, transferring phone book + // when connected over bluetooth) + // + // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" + final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); + final String callLogTypeStr; + switch (callLogType) { + case Calls.INCOMING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; + break; + } + case Calls.OUTGOING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; + break; + } + case Calls.MISSED_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; + break; + } + default: { + Log.w(LOG_TAG, "Call log type not correct."); + return; + } + } + + final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); + builder.append(VCARD_PROPERTY_X_TIMESTAMP); + builder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(builder, callLogTypeStr); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(toRfc2455Format(dateAsLong)); + builder.append(VCARD_COL_SEPARATOR); + } + + private String createOneCallLogEntryInternal() { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (TextUtils.isEmpty(name)) { + name = mCursor.getString(NUMBER_COLUMN_INDEX); + } + final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); + // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, name, needCharset, false); + appendVCardLine(builder, VCARD_PROPERTY_NAME, name, needCharset, false); + + String number = mCursor.getString(NUMBER_COLUMN_INDEX); + int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + if (TextUtils.isEmpty(label)) { + label = Integer.toString(type); + } + appendVCardTelephoneLine(builder, type, label, number); + tryAppendCallHistoryTimeStampField(builder); + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + return builder.toString(); + } + + private String createOneEntryInternal(final String contactId) { + final Map> contentValuesListMap = + new HashMap>(); + final String selection = Data.CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; + // The resolver may return the entity iterator with no data. It is possiible. + // e.g. If all the data in the contact of the given contact id are not exportable ones, + // they are hidden from the view of this method, though contact id itself exists. + boolean dataExists = false; + EntityIterator entityIterator = null; + try { + entityIterator = mContentResolver.queryEntities( + sDataRequestUri, selection, selectionArgs, null); + dataExists = entityIterator.hasNext(); + while (entityIterator.hasNext()) { + Entity entity = entityIterator.next(); + for (NamedContentValues namedContentValues : entity + .getSubValues()) { + ContentValues contentValues = namedContentValues.values; + String key = contentValues.getAsString(Data.MIMETYPE); + if (key != null) { + List contentValuesList = + contentValuesListMap.get(key); + if (contentValuesList == null) { + contentValuesList = new ArrayList(); + contentValuesListMap.put(key, contentValuesList); + } + contentValuesList.add(contentValues); + } + } + } + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)", + contactId, e.getMessage())); + return ""; + } finally { + if (entityIterator != null) { + entityIterator.close(); + } + } + + if (!dataExists) { + return ""; + } + + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + + appendStructuredNames(builder, contentValuesListMap); + appendNickNames(builder, contentValuesListMap); + appendPhones(builder, contentValuesListMap); + appendEmails(builder, contentValuesListMap); + appendPostals(builder, contentValuesListMap); + appendIms(builder, contentValuesListMap); + appendWebsites(builder, contentValuesListMap); + appendBirthday(builder, contentValuesListMap); + appendOrganizations(builder, contentValuesListMap); + if (mNeedPhotoForVCard) { + appendPhotos(builder, contentValuesListMap); + } + appendNotes(builder, contentValuesListMap); + // TODO: GroupMembership + + if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); + } + + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + public void terminate() { + for (OneEntryHandler handler : mHandlerList) { + handler.onTerminate(); + } + + if (mCursor != null) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + + e.getMessage()); + } + mCursor = null; + } + + mTerminateIsCalled = true; + } + + @Override + public void finalize() { + if (!mTerminateIsCalled) { + terminate(); + } + } + + public int getCount() { + if (mCursor == null) { + return 0; + } + return mCursor.getCount(); + } + + public boolean isAfterLast() { + if (mCursor == null) { + return false; + } + return mCursor.isAfterLast(); + } + + /** + * @return Return the error reason if possible. + */ + public String getErrorReason() { + return mErrorReason; + } + + private void appendStructuredNames(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(StructuredName.CONTENT_ITEM_TYPE); + if (contentValuesList != null && contentValuesList.size() > 0) { + appendStructuredNamesInternal(builder, contentValuesList); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } else if (mIsV30) { + // vCard 3.0 requires "N" and "FN" properties. + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + } + } + + private boolean containsNonEmptyName(ContentValues contentValues) { + final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = contentValues.getAsString(StructuredName.PREFIX); + final String suffix = contentValues.getAsString(StructuredName.SUFFIX); + final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); + return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && + TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && + TextUtils.isEmpty(suffix) && TextUtils.isEmpty(displayName)); + } + + private void appendStructuredNamesInternal(final StringBuilder builder, + final List contentValuesList) { + // For safety, we'll emit just one value around StructuredName, as external importers + // may get confused with multiple "N", "FN", etc. properties, though it is valid in + // vCard spec. + ContentValues primaryContentValues = null; + ContentValues subprimaryContentValues = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null){ + continue; + } + Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); + if (isSuperPrimary != null && isSuperPrimary > 0) { + // We choose "super primary" ContentValues. + primaryContentValues = contentValues; + break; + } else if (primaryContentValues == null) { + // We choose the first "primary" ContentValues + // if "super primary" ContentValues does not exist. + Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); + if (isPrimary != null && isPrimary > 0 && + containsNonEmptyName(contentValues)) { + primaryContentValues = contentValues; + // Do not break, since there may be ContentValues with "super primary" + // afterword. + } else if (subprimaryContentValues == null && + containsNonEmptyName(contentValues)) { + subprimaryContentValues = contentValues; + } + } + } + + if (primaryContentValues == null) { + if (subprimaryContentValues != null) { + // We choose the first ContentValues if any "primary" ContentValues does not exist. + primaryContentValues = subprimaryContentValues; + } else { + Log.e(LOG_TAG, "All ContentValues given from database is empty."); + primaryContentValues = new ContentValues(); + } + } + + final String familyName = primaryContentValues + .getAsString(StructuredName.FAMILY_NAME); + final String middleName = primaryContentValues + .getAsString(StructuredName.MIDDLE_NAME); + final String givenName = primaryContentValues + .getAsString(StructuredName.GIVEN_NAME); + final String prefix = primaryContentValues + .getAsString(StructuredName.PREFIX); + final String suffix = primaryContentValues + .getAsString(StructuredName.SUFFIX); + final String displayName = primaryContentValues + .getAsString(StructuredName.DISPLAY_NAME); + + if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final String encodedFamily; + final String encodedGiven; + final String encodedMiddle; + final String encodedPrefix; + final String encodedSuffix; + + final boolean reallyUseQuotedPrintableToName = + (mUsesQPToPrimaryProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); + + if (reallyUseQuotedPrintableToName) { + encodedFamily = encodeQuotedPrintable(familyName); + encodedGiven = encodeQuotedPrintable(givenName); + encodedMiddle = encodeQuotedPrintable(middleName); + encodedPrefix = encodeQuotedPrintable(prefix); + encodedSuffix = encodeQuotedPrintable(suffix); + } else { + encodedFamily = escapeCharacters(familyName); + encodedGiven = escapeCharacters(givenName); + encodedMiddle = escapeCharacters(middleName); + encodedPrefix = escapeCharacters(prefix); + encodedSuffix = escapeCharacters(suffix); + } + + // N property. This order is specified by vCard spec and does not depend on countries. + builder.append(VCARD_PROPERTY_NAME); + if (shouldAppendCharsetAttribute(Arrays.asList( + familyName, givenName, middleName, prefix, suffix))) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFamily); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedGiven); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedMiddle); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPrefix); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedSuffix); + builder.append(VCARD_COL_SEPARATOR); + + final String fullname = VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); + final boolean reallyUseQuotedPrintableToFullname = + mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); + + final String encodedFullname = + reallyUseQuotedPrintableToFullname ? + encodeQuotedPrintable(fullname) : + escapeCharacters(fullname); + + // FN property + builder.append(VCARD_PROPERTY_FULL_NAME); + if (shouldAppendCharsetAttribute(encodedFullname)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToFullname) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFullname); + builder.append(VCARD_COL_SEPARATOR); + } else if (!TextUtils.isEmpty(displayName)) { + final boolean reallyUseQuotedPrintableToDisplayName = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); + final String encodedDisplayName = + reallyUseQuotedPrintableToDisplayName ? + encodeQuotedPrintable(displayName) : + escapeCharacters(displayName); + + builder.append(VCARD_PROPERTY_NAME); + if (shouldAppendCharsetAttribute(encodedDisplayName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToDisplayName) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedDisplayName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } else if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + } + + String phoneticFamilyName = primaryContentValues + .getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String phoneticMiddleName = primaryContentValues + .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String phoneticGivenName = primaryContentValues + .getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (!(TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) && + TextUtils.isEmpty(phoneticGivenName))) { // if not empty + if (mIsJapaneseMobilePhone) { + phoneticFamilyName = VCardUtils + .toHalfWidthString(phoneticFamilyName); + phoneticMiddleName = VCardUtils + .toHalfWidthString(phoneticMiddleName); + phoneticGivenName = VCardUtils + .toHalfWidthString(phoneticGivenName); + } + + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, + phoneticMiddleName, + phoneticGivenName); + builder.append(VCARD_PROPERTY_SORT_STRING); + + // Do not need to care about QP, since vCard 3.0 does not allow it. + final String encodedSortString = escapeCharacters(sortString); + if (shouldAppendCharsetAttribute(encodedSortString)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedSortString); + builder.append(VCARD_COL_SEPARATOR); + } else { + // Note: There is no appropriate property for expressing + // phonetic name in vCard 2.1, while there is in + // vCard 3.0 (SORT-STRING). + // We chose to use DoCoMo's way since it is supported by + // a lot of Japanese mobile phones. This is "X-" property, so + // any parser hopefully would not get confused with this. + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + + boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); + + final String encodedPhoneticFamilyName; + final String encodedPhoneticMiddleName; + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + + if (shouldAppendCharsetAttribute(Arrays.asList( + encodedPhoneticFamilyName, encodedPhoneticMiddleName, + encodedPhoneticGivenName))) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticFamilyName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPhoneticGivenName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPhoneticMiddleName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + if (mUsesDefactProperty) { + if (!TextUtils.isEmpty(phoneticGivenName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticGivenName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticMiddleName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); + final String encodedPhoneticMiddleName; + if (reallyUseQuotedPrintable) { + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + } else { + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticMiddleName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticFamilyName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); + final String encodedPhoneticFamilyName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticFamilyName); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + + private void appendNickNames(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Nickname.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + final String propertyNickname; + if (mIsV30) { + propertyNickname = VCARD_PROPERTY_NICKNAME; + } else if (mUsesAndroidProperty) { + propertyNickname = VCARD_PROPERTY_X_NICKNAME; + } else { + // There's no way to add this field. + return; + } + + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + + final String encodedNickname; + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); + if (reallyUseQuotedPrintable) { + encodedNickname = encodeQuotedPrintable(nickname); + } else { + encodedNickname = escapeCharacters(nickname); + } + + builder.append(propertyNickname); + if (shouldAppendCharsetAttribute(propertyNickname)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedNickname); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + + private void appendPhones(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Phone.CONTENT_ITEM_TYPE); + boolean phoneLineExists = false; + if (contentValuesList != null) { + Set phoneSet = new HashSet(); + for (ContentValues contentValues : contentValuesList) { + final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); + final String label = contentValues.getAsString(Phone.LABEL); + String phoneNumber = contentValues.getAsString(Phone.NUMBER); + if (phoneNumber != null) { + phoneNumber = phoneNumber.trim(); + } + if (TextUtils.isEmpty(phoneNumber)) { + continue; + } + int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); + + phoneLineExists = true; + if (type == Phone.TYPE_PAGER) { + phoneLineExists = true; + if (!phoneSet.contains(phoneNumber)) { + phoneSet.add(phoneNumber); + appendVCardTelephoneLine(builder, type, label, phoneNumber); + } + } else { + // The entry "may" have several phone numbers when the contact entry is + // corrupted because of its original source. + // + // e.g. I encountered the entry like the following. + // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." + // This kind of entry is not able to be inserted via Android devices, but + // possible if the source of the data is already corrupted. + List phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); + if (phoneNumberList.isEmpty()) { + continue; + } + phoneLineExists = true; + for (String actualPhoneNumber : phoneNumberList) { + if (!phoneSet.contains(actualPhoneNumber)) { + final int format = VCardUtils.getPhoneNumberFormat(mVCardType); + SpannableStringBuilder tmpBuilder = + new SpannableStringBuilder(actualPhoneNumber); + PhoneNumberUtils.formatNumber(tmpBuilder, format); + final String formattedPhoneNumber = tmpBuilder.toString(); + phoneSet.add(actualPhoneNumber); + appendVCardTelephoneLine(builder, type, label, formattedPhoneNumber); + } + } + } + } + } + + if (!phoneLineExists && mIsDoCoMo) { + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + } + } + + private List splitIfSeveralPhoneNumbersExist(final String phoneNumber) { + List phoneList = new ArrayList(); + + StringBuilder builder = new StringBuilder(); + final int length = phoneNumber.length(); + for (int i = 0; i < length; i++) { + final char ch = phoneNumber.charAt(i); + if (Character.isDigit(ch)) { + builder.append(ch); + } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { + phoneList.add(builder.toString()); + builder = new StringBuilder(); + } + } + if (builder.length() > 0) { + phoneList.add(builder.toString()); + } + + return phoneList; + } + + private void appendEmails(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Email.CONTENT_ITEM_TYPE); + boolean emailAddressExists = false; + if (contentValuesList != null) { + Set addressSet = new HashSet(); + for (ContentValues contentValues : contentValuesList) { + Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : Email.TYPE_OTHER); + final String label = contentValues.getAsString(Email.LABEL); + String emailAddress = contentValues.getAsString(Email.DATA); + if (emailAddress != null) { + emailAddress = emailAddress.trim(); + } + if (TextUtils.isEmpty(emailAddress)) { + continue; + } + emailAddressExists = true; + if (!addressSet.contains(emailAddress)) { + addressSet.add(emailAddress); + appendVCardEmailLine(builder, type, label, emailAddress); + } + } + } + + if (!emailAddressExists && mIsDoCoMo) { + appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + } + } + + private void appendPostals(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(StructuredPostal.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + if (mIsDoCoMo) { + appendPostalsForDoCoMo(builder, contentValuesList); + } else { + appendPostalsForGeneric(builder, contentValuesList); + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_HOME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + } + + /** + * Tries to append just one line. If there's no appropriate address + * information, append an empty line. + */ + private void appendPostalsForDoCoMo(final StringBuilder builder, + final List contentValuesList) { + // TODO: from old, inefficient code. fix this. + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_HOME)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_WORK)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_OTHER)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_CUSTOM)) { + return; + } + + Log.w(LOG_TAG, + "Should not come here. Must have at least one postal data."); + } + + private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, + final List contentValuesList, Integer preferedType) { + for (ContentValues contentValues : contentValuesList) { + final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + if (type == preferedType) { + appendVCardPostalLine(builder, type, label, contentValues); + return true; + } + } + return false; + } + + private void appendPostalsForGeneric(final StringBuilder builder, + final List contentValuesList) { + for (ContentValues contentValues : contentValuesList) { + final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + if (type != null) { + appendVCardPostalLine(builder, type, label, contentValues); + } + } + } + + private void appendIms(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Im.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); + String data = contentValues.getAsString(Im.DATA); + if (data != null) { + data = data.trim(); + } + if (TextUtils.isEmpty(data)) { + continue; + } + + if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { + if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { + appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); + } + // TODO: add "X-GOOGLE TALK" case... + } + } + } + } + + private void appendWebsites(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Website.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String website = contentValues.getAsString(Website.URL); + if (website != null) { + website = website.trim(); + } + if (!TextUtils.isEmpty(website)) { + appendVCardLine(builder, VCARD_PROPERTY_URL, website); + } + } + } + } + + private void appendBirthday(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Event.CONTENT_ITEM_TYPE); + if (contentValuesList != null && contentValuesList.size() > 0) { + Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); + if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { + return; + } + // Theoretically, there must be only one birthday for each vCard data and + // we are afraid of some parse error occuring in some devices, so + // we emit only one birthday entry for now. + String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); + if (birthday != null) { + birthday = birthday.trim(); + } + if (!TextUtils.isEmpty(birthday)) { + appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); + } + } + } + + private void appendOrganizations(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Organization.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String company = contentValues + .getAsString(Organization.COMPANY); + if (company != null) { + company = company.trim(); + } + String title = contentValues + .getAsString(Organization.TITLE); + if (title != null) { + title = title.trim(); + } + + if (!TextUtils.isEmpty(company)) { + appendVCardLine(builder, VCARD_PROPERTY_ORG, company, + !VCardUtils.containsOnlyPrintableAscii(company), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); + } + if (!TextUtils.isEmpty(title)) { + appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, + !VCardUtils.containsOnlyPrintableAscii(title), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); + } + } + } + } + + private void appendPhotos(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = contentValuesListMap + .get(Photo.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + byte[] data = contentValues.getAsByteArray(Photo.PHOTO); + if (data == null) { + continue; + } + final String photoType; + // Use some heuristics for guessing the format of the image. + // TODO: there should be some general API for detecting the file format. + if (data.length >= 3 && data[0] == 'G' && data[1] == 'I' + && data[2] == 'F') { + photoType = "GIF"; + } else if (data.length >= 4 && data[0] == (byte) 0x89 + && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { + // Note: vCard 2.1 officially does not support PNG, but we + // may have it + // and using X- word like "X-PNG" may not let importers know + // it is + // PNG. So we use the String "PNG" as is... + photoType = "PNG"; + } else if (data.length >= 2 && data[0] == (byte) 0xff + && data[1] == (byte) 0xd8) { + photoType = "JPEG"; + } else { + Log.d(LOG_TAG, "Unknown photo type. Ignore."); + continue; + } + final String photoString = VCardUtils.encodeBase64(data); + if (photoString.length() > 0) { + appendVCardPhotoLine(builder, photoString, photoType); + } + } + } + } + + private void appendNotes(final StringBuilder builder, + final Map> contentValuesListMap) { + final List contentValuesList = + contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + if (mOnlyOneNoteFieldIsAvailable) { + StringBuilder noteBuilder = new StringBuilder(); + boolean first = true; + for (ContentValues contentValues : contentValuesList) { + String note = contentValues.getAsString(Note.NOTE); + if (note == null) { + note = ""; + } + if (note.length() > 0) { + if (first) { + first = false; + } else { + noteBuilder.append('\n'); + } + noteBuilder.append(note); + } + } + final String noteStr = noteBuilder.toString(); + // This means we scan noteStr completely twice, which is redundant. + // But for now, we assume this is not so time-consuming.. + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } else { + for (ContentValues contentValues : contentValuesList) { + final String noteStr = contentValues.getAsString(Note.NOTE); + if (!TextUtils.isEmpty(noteStr)) { + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } + } + } + } + } + + /** + * Append '\' to the characters which should be escaped. The character set is different + * not only between vCard 2.1 and vCard 3.0 but also among each device. + * + * Note that Quoted-Printable string must not be input here. + */ + @SuppressWarnings("fallthrough") + private String escapeCharacters(final String unescaped) { + if (TextUtils.isEmpty(unescaped)) { + return ""; + } + + final StringBuilder tmpBuilder = new StringBuilder(); + final int length = unescaped.length(); + for (int i = 0; i < length; i++) { + char ch = unescaped.charAt(i); + switch (ch) { + case ';': { + tmpBuilder.append('\\'); + tmpBuilder.append(';'); + break; + } + case '\r': { + if (i + 1 < length) { + char nextChar = unescaped.charAt(i); + if (nextChar == '\n') { + continue; + } else { + // fall through + } + } else { + // fall through + } + } + case '\n': { + // In vCard 2.1, there's no specification about this, while + // vCard 3.0 explicitly requires this should be encoded to "\n". + tmpBuilder.append("\\n"); + break; + } + case '\\': { + if (mIsV30) { + tmpBuilder.append("\\\\"); + break; + } else { + // fall through + } + } + case '<': + case '>': { + if (mIsDoCoMo) { + tmpBuilder.append('\\'); + tmpBuilder.append(ch); + } else { + tmpBuilder.append(ch); + } + break; + } + case ',': { + if (mIsV30) { + tmpBuilder.append("\\,"); + } else { + tmpBuilder.append(ch); + } + break; + } + default: { + tmpBuilder.append(ch); + break; + } + } + } + return tmpBuilder.toString(); + } + + private void appendVCardPhotoLine(final StringBuilder builder, + final String encodedData, final String photoType) { + StringBuilder tmpBuilder = new StringBuilder(); + tmpBuilder.append(VCARD_PROPERTY_PHOTO); + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + if (mIsV30) { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); + } else { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); + } + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(tmpBuilder, photoType); + tmpBuilder.append(VCARD_DATA_SEPARATOR); + tmpBuilder.append(encodedData); + + final String tmpStr = tmpBuilder.toString(); + tmpBuilder = new StringBuilder(); + int lineCount = 0; + int length = tmpStr.length(); + for (int i = 0; i < length; i++) { + tmpBuilder.append(tmpStr.charAt(i)); + lineCount++; + if (lineCount > 72) { + tmpBuilder.append(VCARD_COL_SEPARATOR); + tmpBuilder.append(VCARD_WS); + lineCount = 0; + } + } + builder.append(tmpBuilder.toString()); + builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardPostalLine(final StringBuilder builder, + final Integer typeAsObject, final String label, + final ContentValues contentValues) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + + // Note: Not sure why we need to emit "empty" line even when actual data does not exist. + // There may be some reason or may not be any. We keep safer side. + // TODO: investigate this. + boolean dataExists = false; + String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); + boolean actuallyUseQuotedPrintable = false; + boolean shouldAppendCharset = false; + for (String data : dataArray) { + if (!TextUtils.isEmpty(data)) { + dataExists = true; + if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { + shouldAppendCharset = true; + } + if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { + actuallyUseQuotedPrintable = true; + break; + } + } + } + + int length = dataArray.length; + for (int i = 0; i < length; i++) { + String data = dataArray[i]; + if (!TextUtils.isEmpty(data)) { + if (actuallyUseQuotedPrintable) { + dataArray[i] = encodeQuotedPrintable(data); + } else { + dataArray[i] = escapeCharacters(data); + } + } + } + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = StructuredPostal.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + String typeAsString = null; + switch (typeAsPrimitive) { + case StructuredPostal.TYPE_HOME: { + typeAsString = Constants.ATTR_TYPE_HOME; + break; + } + case StructuredPostal.TYPE_WORK: { + typeAsString = Constants.ATTR_TYPE_WORK; + break; + } + case StructuredPostal.TYPE_CUSTOM: { + if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + // We're not sure whether the label is valid in the spec + // ("IANA-token" in the vCard 3.0 is unclear...) + // Just for safety, we add "X-" at the beggining of each label. + // Also checks the label obeys with vCard 3.0 spec. + builder.append("X-"); + builder.append(label); + builder.append(VCARD_DATA_SEPARATOR); + } + break; + } + case StructuredPostal.TYPE_OTHER: { + break; + } + default: { + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); + break; + } + } + + // Attribute(s). + + { + boolean shouldAppendAttrSeparator = false; + if (typeAsString != null) { + appendTypeAttribute(builder, typeAsString); + shouldAppendAttrSeparator = true; + } + + if (dataExists) { + if (shouldAppendCharset) { + // Strictly, vCard 3.0 does not allow exporters to emit charset information, + // but we will add it since the information should be useful for importers, + // + // Assume no parser does not emit error with this attribute in vCard 3.0. + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); + } + builder.append(mVCardAttributeCharset); + shouldAppendAttrSeparator = true; + } + + if (actuallyUseQuotedPrintable) { + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); + } + builder.append(VCARD_ATTR_ENCODING_QP); + shouldAppendAttrSeparator = true; + } + } + } + + // Property values. + + builder.append(VCARD_DATA_SEPARATOR); + if (dataExists) { + // The elements in dataArray are already encoded to quoted printable + // if needed. + // See above. + // + // TODO: in vCard 3.0, one line may become too huge. Fix this. + builder.append(dataArray[0]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[1]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[2]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[3]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[4]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[5]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[6]); + } + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardEmailLine(final StringBuilder builder, + final Integer typeAsObject, final String label, final String data) { + builder.append(VCARD_PROPERTY_EMAIL); + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = Email.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + final String typeAsString; + switch (typeAsPrimitive) { + case Email.TYPE_CUSTOM: { + // For backward compatibility. + // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. + // To support mobile type at that time, this custom label had been used. + if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME + .equals(label)) { + typeAsString = Constants.ATTR_TYPE_CELL; + } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + typeAsString = "X-" + label; + } else { + typeAsString = DEFAULT_EMAIL_TYPE; + } + break; + } + case Email.TYPE_HOME: { + typeAsString = Constants.ATTR_TYPE_HOME; + break; + } + case Email.TYPE_WORK: { + typeAsString = Constants.ATTR_TYPE_WORK; + break; + } + case Email.TYPE_OTHER: { + typeAsString = DEFAULT_EMAIL_TYPE; + break; + } + case Email.TYPE_MOBILE: { + typeAsString = Constants.ATTR_TYPE_CELL; + break; + } + default: { + Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); + typeAsString = DEFAULT_EMAIL_TYPE; + break; + } + } + + builder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(builder, typeAsString); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(data); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardTelephoneLine(final StringBuilder builder, + final Integer typeAsObject, final String label, + String encodedData) { + builder.append(VCARD_PROPERTY_TEL); + builder.append(VCARD_ATTR_SEPARATOR); + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = Phone.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + switch (typeAsPrimitive) { + case Phone.TYPE_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_FAX_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_FAX_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_MOBILE: + builder.append(Constants.ATTR_TYPE_CELL); + break; + case Phone.TYPE_PAGER: + if (mIsDoCoMo) { + // Not sure about the reason, but previous implementation had + // used "VOICE" instead of "PAGER" + // Also, refrain from using appendType() so that "TYPE=" is never be appended. + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); + } + break; + case Phone.TYPE_OTHER: + appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + break; + case Phone.TYPE_CUSTOM: + if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + appendTypeAttribute(builder, "X-" + label); + } else { + // Just ignore the custom type. + appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + } + break; + default: + appendUncommonPhoneType(builder, typeAsPrimitive); + break; + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + /** + * Appends phone type string which may not be available in some devices. + */ + private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { + if (mIsDoCoMo) { + // The previous implementation for DoCoMo had been conservative + // about miscellaneous types. + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + String phoneAttribute = VCardUtils.getPhoneAttributeString(type); + if (phoneAttribute != null) { + appendTypeAttribute(builder, phoneAttribute); + } else { + Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); + } + } + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLine(builder, propertyName, rawData, false, false); + } + + private void appendVCardLine(final StringBuilder builder, + final String field, final String rawData, final boolean needCharset, + boolean needQuotedPrintable) { + builder.append(field); + if (needCharset) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + final String encodedData; + if (needQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + encodedData = encodeQuotedPrintable(rawData); + } else { + // TODO: one line may be too huge, which may be invalid in vCard spec, though + // several (even well-known) applications do not care this. + encodedData = escapeCharacters(rawData); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendTypeAttributes(final StringBuilder builder, + final List types) { + // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, + // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. + boolean first = true; + for (String type : types) { + if (first) { + first = false; + } else { + builder.append(VCARD_ATTR_SEPARATOR); + } + appendTypeAttribute(builder, type); + } + } + + private void appendTypeAttribute(final StringBuilder builder, final String type) { + // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" + if (mIsV30) { + builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); + } + builder.append(type); + } + + /** + * Returns true when the property line should contain charset attribute + * information. This method may return true even when vCard version is 3.0. + * + * Strictly, adding charset information is invalid in VCard 3.0. + * However we'll add the info only when used charset is not UTF-8 + * in vCard 3.0 format, since parser side may be able to use the charset + * via this field, though we may encounter another problem by adding it... + * + * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 + * recommends UTF-8. By adding this field, parsers may be able + * to know this text is NOT UTF-8 but Shift_Jis. + */ + private boolean shouldAppendCharsetAttribute(final String propertyValue) { + return (!VCardUtils.containsOnlyPrintableAscii(propertyValue) && + (!mIsV30 || !mUsesUtf8)); + } + + private boolean shouldAppendCharsetAttribute(final List propertyValueList) { + boolean shouldAppendBasically = false; + for (String propertyValue : propertyValueList) { + if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { + shouldAppendBasically = true; + break; + } + } + return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); + } + + private String encodeQuotedPrintable(String str) { + if (TextUtils.isEmpty(str)) { + return ""; + } + { + // Replace "\n" and "\r" with "\r\n". + StringBuilder tmpBuilder = new StringBuilder(); + int length = str.length(); + for (int i = 0; i < length; i++) { + char ch = str.charAt(i); + if (ch == '\r') { + if (i + 1 < length && str.charAt(i + 1) == '\n') { + i++; + } + tmpBuilder.append("\r\n"); + } else if (ch == '\n') { + tmpBuilder.append("\r\n"); + } else { + tmpBuilder.append(ch); + } + } + str = tmpBuilder.toString(); + } + + final StringBuilder tmpBuilder = new StringBuilder(); + int index = 0; + int lineCount = 0; + byte[] strArray = null; + + try { + strArray = str.getBytes(mCharsetString); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + + "Try default charset"); + strArray = str.getBytes(); + } + while (index < strArray.length) { + tmpBuilder.append(String.format("=%02X", strArray[index])); + index += 1; + lineCount += 3; + + if (lineCount >= 67) { + // Specification requires CRLF must be inserted before the + // length of the line + // becomes more than 76. + // Assuming that the next character is a multi-byte character, + // it will become + // 6 bytes. + // 76 - 6 - 3 = 67 + tmpBuilder.append("=\r\n"); + lineCount = 0; + } + } + + return tmpBuilder.toString(); + } +} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..68cd0df3d80a885305167bb96f4e476a70ef6d4f --- /dev/null +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import java.util.HashMap; +import java.util.Map; + +/** + * The class representing VCard related configurations. Useful static methods are not in this class + * but in VCardUtils. + */ +public class VCardConfig { + // TODO: may be better to make the instance of this available and stop using static methods and + // one integer. + + /* package */ static final int LOG_LEVEL_NONE = 0; + /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; + /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2; + /* package */ static final int LOG_LEVEL_VERBOSE = + LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING; + + /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; + + // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and + // decode the unicode to the original charset. If not, this setting will cause some bug. + public static final String DEFAULT_CHARSET = "iso-8859-1"; + + // TODO: make the other codes use this flag + public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; + + private static final int FLAG_V21 = 0; + private static final int FLAG_V30 = 1; + + // 0x2 is reserved for the future use ... + + public static final int NAME_ORDER_DEFAULT = 0; + public static final int NAME_ORDER_EUROPE = 0x4; + public static final int NAME_ORDER_JAPANESE = 0x8; + private static final int NAME_ORDER_MASK = 0xC; + + // 0x10 is reserved for safety + + private static final int FLAG_CHARSET_UTF8 = 0; + private static final int FLAG_CHARSET_SHIFT_JIS = 0x20; + + /** + * The flag indicating the vCard composer will add some "X-" properties used only in Android + * when the formal vCard specification does not have appropriate fields for that data. + * + * For example, Android accepts nickname information while vCard 2.1 does not. + * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") + * instead of just dropping it. + * + * vCard parser code automatically parses the field emitted even when this flag is off. + * + * Note that this flag does not assure all the information must be hold in the emitted vCard. + */ + private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; + + /** + * The flag indicating the vCard composer will add some "X-" properties seen in the + * vCard data emitted by the other softwares/devices when the formal vCard specification + * does not have appropriate field(s) for that data. + * + * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are + * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other + * non-Android devices/softwares. We chose to enable the vCard composer to use those + * defact properties since they are also useful for Android devices. + * + * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 + * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens + * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. + */ + private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; + + /** + * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese + * mobile careers) should be used. This flag does not include any other information like + * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's + * dialect but the name order should be European", but it is not recommended. + */ + private static final int FLAG_DOCOMO = 0x20000000; + + /** + * The flag indicating the vCard composer use Quoted-Printable toward even "primary" types. + * In this context, "primary" types means "N", "FN", etc. which are usually "not" encoded + * into Quoted-Printable format in external exporters. + * This flag is useful when some target importer does not accept "primary" property values + * without Quoted-Printable encoding. + * + * @hide Temporaly made public. We don't strictly define "primary", so we may change the + * behavior around this flag in the future. Do not use this flag without any reason. + */ + public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; + + // VCard types + + /** + * General vCard format with the version 2.1. Uses UTF-8 for the charset. + * When composing a vCard entry, the US convension will be used. + * + * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", + * while in Japan, it should be "Prefix Family Middle Given Suffix". + */ + public static final int VCARD_TYPE_V21_GENERIC = + (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; + + /** + * General vCard format with the version 3.0. Uses UTF-8 for the charset. + * + * Note that this type is not fully implemented, so probably some bugs remain both in + * parsing and composing. + * + * TODO: implement this type correctly. + */ + public static final int VCARD_TYPE_V30_GENERIC = + (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + + /** + * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + */ + public static final int VCARD_TYPE_V21_EUROPE = + (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; + + /** + * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + */ + public static final int VCARD_TYPE_V30_EUROPE = + (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; + + /** + * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for + * parsing/composing the vCard data. + */ + public static final int VCARD_TYPE_V21_JAPANESE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese"; + + /** + * vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + */ + public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + + /** + * vCard format for miscellaneous Japanese devices, using Shift_Jis for + * parsing/composing the vCard data. + */ + public static final int VCARD_TYPE_V30_JAPANESE = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese"; + + /** + * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + */ + public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; + + /** + * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. + * No Android-specific property nor defact property is included. + */ + public static final int VCARD_TYPE_DOCOMO = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + + private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; + + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + + private static final Map VCARD_TYPES_MAP; + + static { + VCARD_TYPES_MAP = new HashMap(); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + VCARD_TYPES_MAP.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + } + + public static int getVCardTypeFromString(String vcardTypeString) { + String loweredKey = vcardTypeString.toLowerCase(); + if (VCARD_TYPES_MAP.containsKey(loweredKey)) { + return VCARD_TYPES_MAP.get(loweredKey); + } else { + // XXX: should return the value indicating the input is invalid? + return VCARD_TYPE_DEFAULT; + } + } + + public static boolean isV30(int vcardType) { + return ((vcardType & FLAG_V30) != 0); + } + + public static boolean usesQuotedPrintable(int vcardType) { + return !isV30(vcardType); + } + + public static boolean isDoCoMo(int vcardType) { + return ((vcardType & FLAG_DOCOMO) != 0); + } + + /** + * @return true if the device is Japanese and some Japanese convension is + * applied to creating "formatted" something like FORMATTED_ADDRESS. + */ + public static boolean isJapaneseDevice(int vcardType) { + return ((vcardType == VCARD_TYPE_V21_JAPANESE) || + (vcardType == VCARD_TYPE_V21_JAPANESE_UTF8) || + (vcardType == VCARD_TYPE_V30_JAPANESE) || + (vcardType == VCARD_TYPE_V30_JAPANESE_UTF8) || + (vcardType == VCARD_TYPE_DOCOMO)); + } + + public static boolean usesUtf8(int vcardType) { + return ((vcardType & FLAG_CHARSET_UTF8) != 0); + } + + public static boolean usesShiftJis(int vcardType) { + return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); + } + + /** + * @return true when Japanese phonetic string must be converted to a string + * containing only half-width katakana. This method exists since Japanese mobile + * phones usually use only half-width katakana for expressing phonetic names and + * some devices are not ready for parsing other phonetic strings like hiragana and + * full-width katakana. + */ + public static boolean needsToConvertPhoneticString(int vcardType) { + return (vcardType == VCARD_TYPE_DOCOMO); + } + + public static int getNameOrderType(int vcardType) { + return vcardType & NAME_ORDER_MASK; + } + + public static boolean usesAndroidSpecificProperty(int vcardType) { + return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); + } + + public static boolean usesDefactProperty(int vcardType) { + return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + + public static boolean showPerformanceLog() { + return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; + } + + /** + * @hide + */ + public static boolean usesQPToPrimaryProperties(int vcardType) { + return (usesQuotedPrintable(vcardType) && + ((vcardType & FLAG_USE_QP_TO_PRIMARY_PROPERTIES) != 0)); + } + + private VCardConfig() { + } +} \ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java similarity index 51% rename from core/java/android/syncml/pim/vcard/VCardDataBuilder.java rename to core/java/android/pim/vcard/VCardDataBuilder.java index a0513f1654f8b055ce17e372f87b824bb3b16108..d2026d00479214632929ab5515e4e2159a4004a3 100644 --- a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.pim.vcard; -package android.syncml.pim.vcard; - -import android.app.ProgressDialog; -import android.content.AbstractSyncableContentProvider; -import android.content.ContentProvider; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.IContentProvider; -import android.os.Handler; -import android.provider.Contacts; -import android.syncml.pim.PropertyNode; -import android.syncml.pim.VBuilder; -import android.syncml.pim.VNode; -import android.syncml.pim.VParser; +import android.accounts.Account; import android.util.CharsetUtils; import android.util.Log; @@ -39,6 +27,7 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -47,32 +36,18 @@ import java.util.List; * OutOfMemoryError may be thrown. Thus, this class push each VCard entry into * ContentResolver immediately. */ -public class VCardDataBuilder implements VBuilder { +public class VCardDataBuilder implements VCardBuilder { static private String LOG_TAG = "VCardDataBuilder"; /** * If there's no other information available, this class uses this charset for encoding * byte arrays. */ - static public String DEFAULT_CHARSET = "UTF-8"; - - private class ProgressShower implements Runnable { - private ContactStruct mContact; - - public ProgressShower(ContactStruct contact) { - mContact = contact; - } - - public void run () { - mProgressDialog.setMessage(mProgressMessage + "\n" + - mContact.displayString()); - } - } + static public String TARGET_CHARSET = "UTF-8"; - /** type=VNode */ - private VNode mCurrentVNode; - private PropertyNode mCurrentPropNode; - private String mCurrentParamType; + private ContactStruct.Property mCurrentProperty = new ContactStruct.Property(); + private ContactStruct mCurrentContactStruct; + private String mParamType; /** * The charset using which VParser parses the text. @@ -84,199 +59,130 @@ public class VCardDataBuilder implements VBuilder { */ private String mTargetCharset; private boolean mStrictLineBreakParsing; - private ContentResolver mContentResolver; - // For letting VCardDataBuilder show the display name of VCard while handling it. - private Handler mHandler; - private ProgressDialog mProgressDialog; - private String mProgressMessage; - private Runnable mOnProgressRunnable; - private boolean mLastNameComesBeforeFirstName; + final private int mVCardType; + final private Account mAccount; // Just for testing. - private long mTimeCreateContactStruct; private long mTimePushIntoContentResolver; - // Ideally, this should be ContactsProvider but it seems Class loader cannot find it, - // even when it is subclass of ContactsProvider... - private AbstractSyncableContentProvider mProvider; - private long mMyContactsGroupId; + private List mEntryHandlers = new ArrayList(); - public VCardDataBuilder(ContentResolver resolver) { - mTargetCharset = DEFAULT_CHARSET; - mContentResolver = resolver; + public VCardDataBuilder() { + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); } - + /** - * Constructor which requires minimum requiredvariables. - * - * @param resolver insert each data into this ContentResolver - * @param progressDialog - * @param progressMessage - * @param handler if this importer works on the different thread than main one, - * set appropriate handler object. If not, it is ok to set this null. + * @hide */ - public VCardDataBuilder(ContentResolver resolver, - ProgressDialog progressDialog, - String progressMessage, - Handler handler) { - this(resolver, progressDialog, progressMessage, handler, - null, null, false, false); + public VCardDataBuilder(int vcardType) { + this(null, null, false, vcardType, null); } - public VCardDataBuilder(ContentResolver resolver, - ProgressDialog progressDialog, - String progressMessage, - Handler handler, - String charset, - boolean strictLineBreakParsing, - boolean lastNameComesBeforeFirstName) { - this(resolver, progressDialog, progressMessage, handler, - null, charset, strictLineBreakParsing, - lastNameComesBeforeFirstName); + /** + * @hide + */ + public VCardDataBuilder(String charset, + boolean strictLineBreakParsing, int vcardType, Account account) { + this(null, charset, strictLineBreakParsing, vcardType, account); } /** * @hide */ - public VCardDataBuilder(ContentResolver resolver, - ProgressDialog progressDialog, - String progressMessage, - Handler handler, - String sourceCharset, + public VCardDataBuilder(String sourceCharset, String targetCharset, boolean strictLineBreakParsing, - boolean lastNameComesBeforeFirstName) { + int vcardType, + Account account) { if (sourceCharset != null) { mSourceCharset = sourceCharset; } else { - mSourceCharset = VParser.DEFAULT_CHARSET; + mSourceCharset = VCardConfig.DEFAULT_CHARSET; } if (targetCharset != null) { mTargetCharset = targetCharset; } else { - mTargetCharset = DEFAULT_CHARSET; + mTargetCharset = TARGET_CHARSET; } - mContentResolver = resolver; mStrictLineBreakParsing = strictLineBreakParsing; - mHandler = handler; - mProgressDialog = progressDialog; - mProgressMessage = progressMessage; - mLastNameComesBeforeFirstName = lastNameComesBeforeFirstName; - - tryGetOriginalProvider(); - } - - private void tryGetOriginalProvider() { - final ContentResolver resolver = mContentResolver; - - if ((mMyContactsGroupId = Contacts.People.tryGetMyContactsGroupId(resolver)) == 0) { - Log.e(LOG_TAG, "Could not get group id of MyContact"); - return; - } - - IContentProvider iProviderForName = resolver.acquireProvider(Contacts.CONTENT_URI); - ContentProvider contentProvider = - ContentProvider.coerceToLocalContentProvider(iProviderForName); - if (contentProvider == null) { - Log.e(LOG_TAG, "Fail to get ContentProvider object."); - return; - } - - if (!(contentProvider instanceof AbstractSyncableContentProvider)) { - Log.e(LOG_TAG, - "Acquired ContentProvider object is not AbstractSyncableContentProvider."); - return; - } - - mProvider = (AbstractSyncableContentProvider)contentProvider; + mVCardType = vcardType; + mAccount = account; } - public void setOnProgressRunnable(Runnable runnable) { - mOnProgressRunnable = runnable; + public void addEntryHandler(EntryHandler entryHandler) { + mEntryHandlers.add(entryHandler); } public void start() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onParsingStart(); + } } public void end() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onParsingEnd(); + } } /** * Assume that VCard is not nested. In other words, this code does not accept */ public void startRecord(String type) { - if (mCurrentVNode != null) { + // TODO: add the method clear() instead of using null for reducing GC? + if (mCurrentContactStruct != null) { // This means startRecord() is called inside startRecord() - endRecord() block. // TODO: should throw some Exception Log.e(LOG_TAG, "Nested VCard code is not supported now."); } - mCurrentVNode = new VNode(); - mCurrentVNode.parseStatus = 1; - mCurrentVNode.VName = type; + if (!type.equalsIgnoreCase("VCARD")) { + // TODO: add test case for this + Log.e(LOG_TAG, "This is not VCARD!"); + } + + mCurrentContactStruct = new ContactStruct(mVCardType, mAccount); } public void endRecord() { - mCurrentVNode.parseStatus = 0; - long start = System.currentTimeMillis(); - ContactStruct contact = ContactStruct.constructContactFromVNode(mCurrentVNode, - mLastNameComesBeforeFirstName ? ContactStruct.NAME_ORDER_TYPE_JAPANESE : - ContactStruct.NAME_ORDER_TYPE_ENGLISH); - mTimeCreateContactStruct += System.currentTimeMillis() - start; - if (!contact.isIgnorable()) { - if (mProgressDialog != null && mProgressMessage != null) { - if (mHandler != null) { - mHandler.post(new ProgressShower(contact)); - } else { - mProgressDialog.setMessage(mProgressMessage + "\n" + - contact.displayString()); - } - } - start = System.currentTimeMillis(); - if (mProvider != null) { - contact.pushIntoAbstractSyncableContentProvider( - mProvider, mMyContactsGroupId); - } else { - contact.pushIntoContentResolver(mContentResolver); - } - mTimePushIntoContentResolver += System.currentTimeMillis() - start; - } - if (mOnProgressRunnable != null) { - mOnProgressRunnable.run(); + mCurrentContactStruct.consolidateFields(); + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEntryCreated(mCurrentContactStruct); } - mCurrentVNode = null; + mCurrentContactStruct = null; } public void startProperty() { - mCurrentPropNode = new PropertyNode(); + mCurrentProperty.clear(); } public void endProperty() { - mCurrentVNode.propList.add(mCurrentPropNode); - mCurrentPropNode = null; + mCurrentContactStruct.addProperty(mCurrentProperty); } public void propertyName(String name) { - mCurrentPropNode.propName = name; + mCurrentProperty.setPropertyName(name); } public void propertyGroup(String group) { - mCurrentPropNode.propGroupSet.add(group); + // ContactStruct does not support Group. } public void propertyParamType(String type) { - mCurrentParamType = type; + if (mParamType != null) { + Log.e(LOG_TAG, "propertyParamType() is called more than once " + + "before propertyParamValue() is called"); + } + mParamType = type; } public void propertyParamValue(String value) { - if (mCurrentParamType == null || - mCurrentParamType.equalsIgnoreCase("TYPE")) { - mCurrentPropNode.paramMap_TYPE.add(value); - } else { - mCurrentPropNode.paramMap.put(mCurrentParamType, value); + if (mParamType == null) { + // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. + mParamType = "TYPE"; } - - mCurrentParamType = null; + mCurrentProperty.addParameter(mParamType, value); + mParamType = null; } private String encodeString(String originalString, String targetCharset) { @@ -293,15 +199,14 @@ public class VCardDataBuilder implements VBuilder { return new String(bytes, targetCharset); } catch (UnsupportedEncodingException e) { Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); - return new String(bytes); + return null; } } private String handleOneValue(String value, String targetCharset, String encoding) { if (encoding != null) { if (encoding.equals("BASE64") || encoding.equals("B")) { - mCurrentPropNode.propValue_bytes = - Base64.decodeBase64(value.getBytes()); + mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); return value; } else if (encoding.equals("QUOTED-PRINTABLE")) { // "= " -> " ", "=\t" -> "\t". @@ -391,52 +296,30 @@ public class VCardDataBuilder implements VBuilder { public void propertyValues(List values) { if (values == null || values.size() == 0) { - mCurrentPropNode.propValue_bytes = null; - mCurrentPropNode.propValue_vector.clear(); - mCurrentPropNode.propValue_vector.add(""); - mCurrentPropNode.propValue = ""; return; } + + final Collection charsetCollection = mCurrentProperty.getParameters("CHARSET"); + String charset = + ((charsetCollection != null) ? charsetCollection.iterator().next() : null); + String targetCharset = CharsetUtils.nameForDefaultVendor(charset); - ContentValues paramMap = mCurrentPropNode.paramMap; - - String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); - String encoding = paramMap.getAsString("ENCODING"); + final Collection encodingCollection = mCurrentProperty.getParameters("ENCODING"); + String encoding = + ((encodingCollection != null) ? encodingCollection.iterator().next() : null); if (targetCharset == null || targetCharset.length() == 0) { targetCharset = mTargetCharset; } for (String value : values) { - mCurrentPropNode.propValue_vector.add( + mCurrentProperty.addToPropertyValueList( handleOneValue(value, targetCharset, encoding)); } - - mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector); } - public void showDebugInfo() { - Log.d(LOG_TAG, "time for creating ContactStruct: " + mTimeCreateContactStruct + " ms"); + public void showPerformanceInfo() { Log.d(LOG_TAG, "time for insert ContactStruct to database: " + mTimePushIntoContentResolver + " ms"); } - - private String listToString(List list){ - int size = list.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - int i = 0; - for (String type : list) { - builder.append(type); - if (i < size - 1) { - builder.append(";"); - } - } - return builder.toString(); - } else if (size == 1) { - return list.get(0); - } else { - return ""; - } - } } diff --git a/core/java/android/syncml/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java similarity index 91% rename from core/java/android/syncml/pim/vcard/VCardEntryCounter.java rename to core/java/android/pim/vcard/VCardEntryCounter.java index 03cd1d9439fddb0b659bfcbc752ce4df48f76b75..f99b46c83edebc772a016f64e8179a85d508445b 100644 --- a/core/java/android/syncml/pim/vcard/VCardEntryCounter.java +++ b/core/java/android/pim/vcard/VCardEntryCounter.java @@ -13,14 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.syncml.pim.vcard; +package android.pim.vcard; import java.util.List; -import android.syncml.pim.VBuilder; - -public class VCardEntryCounter implements VBuilder { +public class VCardEntryCounter implements VCardBuilder { private int mCount; public int getCount() { diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java new file mode 100644 index 0000000000000000000000000000000000000000..b5e504982b36f004a0524c2e9e09862542d587fa --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import android.pim.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class VCardParser { + + protected boolean mCanceled; + + /** + * Parses the given stream and send the VCard data into VCardBuilderBase object. + * + * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets + * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is + * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, + * In some exreme case, some VCard may have different charsets in one VCard (though + * we do not see any device which emits such kind of malicious data) + * + * In order to avoid "misunderstanding" charset as much as possible, this method + * use "ISO-8859-1" for reading the stream. When charset is specified in some property + * (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to + * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit + * characters, which is not completely sure. In some cases, this "decoding-encoding" + * scheme may fail. To avoid the case, + * + * We recommend you to use VCardSourceDetector and detect which kind of source the + * VCard comes from and explicitly specify a charset using the result. + * + * @param is The source to parse. + * @param builder The VCardBuilderBase object which used to construct data. If you want to + * include multiple VCardBuilderBase objects in this field, consider using + * {#link VCardBuilderCollection} class. + * @return Returns true for success. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which accept charset. + * + * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use + * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese + * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses + * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification + * (e.g. W53K). + * + * @param is The source to parse. + * @param charset Charset to be used. + * @param builder The VCardBuilderBase object. + * @return Returns true when successful. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, String charset, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which tells this object the operation is already canceled. + * XXX: Is this really necessary? + * @hide + */ + public abstract void parse(InputStream is, String charset, + VCardBuilder builder, boolean canceled) + throws IOException, VCardException; + + /** + * Cancel parsing. + * Actual cancel is done after the end of the current one vcard entry parsing. + */ + public void cancel() { + mCanceled = true; + } +} diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java similarity index 81% rename from core/java/android/syncml/pim/vcard/VCardParser_V21.java rename to core/java/android/pim/vcard/VCardParser_V21.java index d86566811d85f33fe030f3fead9ac845431894eb..11b38886d8928162e47fc9efc0fe6ed8ed47cda2 100644 --- a/core/java/android/syncml/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.pim.vcard; -package android.syncml.pim.vcard; - -import android.syncml.pim.VBuilder; -import android.syncml.pim.VParser; +import android.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardInvalidCommentLineException; +import android.pim.vcard.exception.VCardInvalidLineException; +import android.pim.vcard.exception.VCardNestedException; +import android.pim.vcard.exception.VCardNotSupportedException; +import android.pim.vcard.exception.VCardVersionException; import android.util.Log; import java.io.BufferedReader; @@ -32,10 +35,8 @@ import java.util.HashSet; /** * This class is used to parse vcard. Please refer to vCard Specification 2.1. */ -public class VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V21"; - - public static final String DEFAULT_CHARSET = VParser.DEFAULT_CHARSET; +public class VCardParser_V21 extends VCardParser { + private static final String LOG_TAG = "vcard.VCardParser_V21"; /** Store the known-type */ private static final HashSet sKnownTypeSet = new HashSet( @@ -53,14 +54,16 @@ public class VCardParser_V21 { Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); /** Store the property names available in vCard 2.1 */ - private static final HashSet sAvailablePropertyNameV21 = + private static final HashSet sAvailablePropertyNameSetV21 = new HashSet(Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); - // Though vCard 2.1 specification does not allow "B" encoding, some data may have it. - // We allow it for safety... + /** + * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety... + */ private static final HashSet sAvailableEncodingV21 = new HashSet(Arrays.asList( "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); @@ -69,9 +72,12 @@ public class VCardParser_V21 { private String mPreviousLine; /** The builder to build parsed data */ - protected VBuilder mBuilder = null; + protected VCardBuilder mBuilder = null; - /** The encoding type */ + /** + * The encoding type. "Encoding" in vCard is different from "Charset". + * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. + */ protected String mEncoding = null; protected final String sDefaultEncoding = "8BIT"; @@ -79,8 +85,6 @@ public class VCardParser_V21 { // Should not directly read a line from this. Use getLine() instead. protected BufferedReader mReader; - private boolean mCanceled; - // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. // See v21_foma_1.vcf in test directory for more information. private int mNestCount; @@ -91,17 +95,17 @@ public class VCardParser_V21 { // Just for debugging private long mTimeTotal; - private long mTimeStartRecord; - private long mTimeEndRecord; + private long mTimeReadStartRecord; + private long mTimeReadEndRecord; private long mTimeStartProperty; private long mTimeEndProperty; private long mTimeParseItems; - private long mTimeParseItem1; - private long mTimeParseItem2; - private long mTimeParseItem3; - private long mTimeHandlePropertyValue1; - private long mTimeHandlePropertyValue2; - private long mTimeHandlePropertyValue3; + private long mTimeParseLineAndHandleGroup; + private long mTimeParsePropertyValues; + private long mTimeParseAdrOrgN; + private long mTimeHandleMiscPropertyValue; + private long mTimeHandleQuotedPrintable; + private long mTimeHandleBase64; /** * Create a new VCard parser. @@ -119,7 +123,7 @@ public class VCardParser_V21 { /** * Parse the file at the given position - * vcard_file = [wsls] vcard [wsls] + * vcard_file = [wsls] vcard [wsls] */ protected void parseVCardFile() throws IOException, VCardException { boolean firstReading = true; @@ -150,7 +154,7 @@ public class VCardParser_V21 { * @return true when the propertyName is a valid property name. */ protected boolean isValidPropertyName(String propertyName) { - if (!(sAvailablePropertyNameV21.contains(propertyName.toUpperCase()) || + if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || propertyName.startsWith("X-")) && !mWarningValueMap.contains(propertyName)) { mWarningValueMap.add(propertyName); @@ -216,7 +220,7 @@ public class VCardParser_V21 { if (mBuilder != null) { start = System.currentTimeMillis(); mBuilder.startRecord("VCARD"); - mTimeStartRecord += System.currentTimeMillis() - start; + mTimeReadStartRecord += System.currentTimeMillis() - start; } start = System.currentTimeMillis(); parseItems(); @@ -225,7 +229,7 @@ public class VCardParser_V21 { if (mBuilder != null) { start = System.currentTimeMillis(); mBuilder.endRecord(); - mTimeEndRecord += System.currentTimeMillis() - start; + mTimeReadEndRecord += System.currentTimeMillis() - start; } return true; } @@ -253,26 +257,6 @@ public class VCardParser_V21 { // Though vCard 2.1/3.0 specification does not allow lower cases, // some data may have them, so we allow it (Actually, previous code // had explicitly allowed "BEGIN:vCard" though there's no example). - // - // TODO: ignore non vCard entry (e.g. vcalendar). - // XXX: Not sure, but according to VDataBuilder.java, vcalendar - // entry - // may be nested. Just seeking "END:SOMETHING" may not be enough. - // e.g. - // BEGIN:VCARD - // ... (Valid. Must parse this) - // END:VCARD - // BEGIN:VSOMETHING - // ... (Must ignore this) - // BEGIN:VSOMETHING2 - // ... (Must ignore this) - // END:VSOMETHING2 - // ... (Must ignore this!) - // END:VSOMETHING - // BEGIN:VCARD - // ... (Valid. Must parse this) - // END:VCARD - // INVALID_STRING (VCardException should be thrown) if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") && strArray[1].trim().equalsIgnoreCase("VCARD")) { @@ -360,7 +344,12 @@ public class VCardParser_V21 { mBuilder.startProperty(); mTimeStartProperty += System.currentTimeMillis() - start; } - ended = parseItem(); + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } if (mBuilder != null && !ended) { long start = System.currentTimeMillis(); mBuilder.endProperty(); @@ -370,11 +359,11 @@ public class VCardParser_V21 { } /** - * item = [groups "."] name [params] ":" value CRLF - * / [groups "."] "ADR" [params] ":" addressparts CRLF - * / [groups "."] "ORG" [params] ":" orgparts CRLF - * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF + * item = [groups "."] name [params] ":" value CRLF + * / [groups "."] "ADR" [params] ":" addressparts CRLF + * / [groups "."] "ORG" [params] ":" orgparts CRLF + * / [groups "."] "N" [params] ":" nameparts CRLF + * / [groups "."] "AGENT" [params] ":" vcard CRLF */ protected boolean parseItem() throws IOException, VCardException { mEncoding = sDefaultEncoding; @@ -387,19 +376,18 @@ public class VCardParser_V21 { return true; } if (propertyNameAndValue.length != 2) { - throw new VCardException("Invalid line \"" + line + "\""); + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); } String propertyName = propertyNameAndValue[0].toUpperCase(); String propertyValue = propertyNameAndValue[1]; - mTimeParseItem1 += System.currentTimeMillis() - start; + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; - if (propertyName.equals("ADR") || - propertyName.equals("ORG") || + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { start = System.currentTimeMillis(); handleMultiplePropertyValue(propertyName, propertyValue); - mTimeParseItem3 += System.currentTimeMillis() - start; + mTimeParseAdrOrgN += System.currentTimeMillis() - start; return false; } else if (propertyName.equals("AGENT")) { handleAgent(propertyValue); @@ -411,14 +399,13 @@ public class VCardParser_V21 { } else { throw new VCardException("Unknown BEGIN type: " + propertyValue); } - } else if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersion())) { + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { throw new VCardVersionException("Incompatible version: " + propertyValue + " != " + getVersion()); } start = System.currentTimeMillis(); handlePropertyValue(propertyName, propertyValue); - mTimeParseItem2 += System.currentTimeMillis() - start; + mTimeParsePropertyValues += System.currentTimeMillis() - start; return false; } @@ -438,7 +425,11 @@ public class VCardParser_V21 { int nameIndex = 0; String[] propertyNameAndValue = new String[2]; - + + if (length > 0 && line.charAt(0) == '#') { + throw new VCardInvalidCommentLineException(); + } + for (int i = 0; i < length; i++) { char ch = line.charAt(i); switch (state) { @@ -503,7 +494,7 @@ public class VCardParser_V21 { } } - throw new VCardException("Invalid line: \"" + line + "\""); + throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); } @@ -545,9 +536,9 @@ public class VCardParser_V21 { } /** - * ptypeval = knowntype / "X-" word + * ptypeval = knowntype / "X-" word */ - protected void handleType(String ptypeval) { + protected void handleType(final String ptypeval) { String upperTypeValue = ptypeval; if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && !mWarningValueMap.contains(ptypeval)) { @@ -563,7 +554,7 @@ public class VCardParser_V21 { /** * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word */ - protected void handleValue(String pvalueval) throws VCardException { + protected void handleValue(final String pvalueval) throws VCardException { if (sKnownValueSet.contains(pvalueval.toUpperCase()) || pvalueval.startsWith("X-")) { if (mBuilder != null) { @@ -640,8 +631,7 @@ public class VCardParser_V21 { } } - protected void handlePropertyValue( - String propertyName, String propertyValue) throws + protected void handlePropertyValue(String propertyName, String propertyValue) throws IOException, VCardException { if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { long start = System.currentTimeMillis(); @@ -651,7 +641,7 @@ public class VCardParser_V21 { v.add(result); mBuilder.propertyValues(v); } - mTimeHandlePropertyValue2 += System.currentTimeMillis() - start; + mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; } else if (mEncoding.equalsIgnoreCase("BASE64") || mEncoding.equalsIgnoreCase("B")) { long start = System.currentTimeMillis(); @@ -670,7 +660,7 @@ public class VCardParser_V21 { mBuilder.propertyValues(null); } } - mTimeHandlePropertyValue3 += System.currentTimeMillis() - start; + mTimeHandleBase64 += System.currentTimeMillis() - start; } else { if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") || mEncoding.equalsIgnoreCase("8BIT") @@ -684,7 +674,7 @@ public class VCardParser_V21 { v.add(maybeUnescapeText(propertyValue)); mBuilder.propertyValues(v); } - mTimeHandlePropertyValue1 += System.currentTimeMillis() - start; + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; } } @@ -773,15 +763,15 @@ public class VCardParser_V21 { * We are not sure whether we should add "\" CRLF to each value. * For now, we exclude them. */ - protected void handleMultiplePropertyValue( - String propertyName, String propertyValue) throws IOException, VCardException { - // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some data have it. + protected void handleMultiplePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + // vCard 2.1 does not allow QUOTED-PRINTABLE here, + // but some softwares/devices emit such data. if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { propertyValue = getQuotedPrintable(propertyValue); } if (mBuilder != null) { - // TODO: limit should be set in accordance with propertyName? StringBuilder builder = new StringBuilder(); ArrayList list = new ArrayList(); int length = propertyValue.length(); @@ -789,7 +779,7 @@ public class VCardParser_V21 { char ch = propertyValue.charAt(i); if (ch == '\\' && i < length - 1) { char nextCh = propertyValue.charAt(i + 1); - String unescapedString = maybeUnescape(nextCh); + String unescapedString = maybeUnescapeCharacter(nextCh); if (unescapedString != null) { builder.append(unescapedString); i++; @@ -819,10 +809,9 @@ public class VCardParser_V21 { * */ protected void handleAgent(String propertyValue) throws VCardException { - throw new VCardException("AGENT Property is not supported."); + throw new VCardNotSupportedException("AGENT Property is not supported now."); /* This is insufficient support. Also, AGENT Property is very rare. Ignore it for now. - TODO: fix this. String[] strArray = propertyValue.split(":", 2); if (!(strArray.length == 2 || @@ -846,7 +835,7 @@ public class VCardParser_V21 { * Returns unescaped String if the character should be unescaped. Return null otherwise. * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. */ - protected String maybeUnescape(char ch) { + protected String maybeUnescapeCharacter(char ch) { // Original vCard 2.1 specification does not allow transformation // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // this class allowed them, so keep it as is. @@ -857,39 +846,21 @@ public class VCardParser_V21 { } } - /** - * Parse the given stream and constructs VCardDataBuilder object. - * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets - * local encoding to it. For example, Japanese phone career uses Shift_JIS, which - * is not formally allowed in vCard specification. - * As a result, there is a case where the encoding given here does not do well with - * the "CHARSET". - * - * In order to avoid such cases, It may be fine to use "ISO-8859-1" as an encoding, - * and to encode each localized String afterward. - * - * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use - * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese - * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses - * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification - * (e.g. W53K). - * - * @param is - * The source to parse. - * @param charset - * The charset. - * @param builder - * The v builder which used to construct data. - * @return Return true for success, otherwise false. - * @throws IOException - */ - public boolean parse(InputStream is, String charset, VBuilder builder) + @Override + public boolean parse(InputStream is, VCardBuilder builder) throws IOException, VCardException { - // TODO: make this count error entries instead of just throwing VCardException. - - // TODO: If we really need to allow only CRLF as line break, - // we will have to develop our own BufferedReader(). - mReader = new CustomBufferedReader(new InputStreamReader(is, charset)); + return parse(is, VCardConfig.DEFAULT_CHARSET, builder); + } + + @Override + public boolean parse(InputStream is, String charset, VCardBuilder builder) + throws IOException, VCardException { + final InputStreamReader tmpReader = new InputStreamReader(is, charset); + if (VCardConfig.showPerformanceLog()) { + mReader = new CustomBufferedReader(tmpReader); + } else { + mReader = new BufferedReader(tmpReader); + } mBuilder = builder; @@ -902,49 +873,42 @@ public class VCardParser_V21 { mBuilder.end(); } mTimeTotal += System.currentTimeMillis() - start; - + + if (VCardConfig.showPerformanceLog()) { + showPerformanceInfo(); + } + return true; } - public boolean parse(InputStream is, VBuilder builder) throws IOException, VCardException { - return parse(is, DEFAULT_CHARSET, builder); - } - - /** - * Cancel parsing. - * Actual cancel is done after the end of the current one vcard entry parsing. - */ - public void cancel() { - mCanceled = true; - } - - /** - * It is very, very rare case, but there is a case where - * canceled may be already true outside this object. - * @hide - */ - public void parse(InputStream is, String charset, VBuilder builder, boolean canceled) + @Override + public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled) throws IOException, VCardException { mCanceled = canceled; parse(is, charset, builder); } - - public void showDebugInfo() { - Log.d(LOG_TAG, "total parsing time: " + mTimeTotal + " ms"); + + private void showPerformanceInfo() { + Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); if (mReader instanceof CustomBufferedReader) { - Log.d(LOG_TAG, "total readLine time: " + + Log.d(LOG_TAG, "Total readLine time: " + ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); } - Log.d(LOG_TAG, "mTimeStartRecord: " + mTimeStartRecord + " ms"); - Log.d(LOG_TAG, "mTimeEndRecord: " + mTimeEndRecord + " ms"); - Log.d(LOG_TAG, "mTimeParseItem1: " + mTimeParseItem1 + " ms"); - Log.d(LOG_TAG, "mTimeParseItem2: " + mTimeParseItem2 + " ms"); - Log.d(LOG_TAG, "mTimeParseItem3: " + mTimeParseItem3 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue1: " + mTimeHandlePropertyValue1 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue2: " + mTimeHandlePropertyValue2 + " ms"); - Log.d(LOG_TAG, "mTimeHandlePropertyValue3: " + mTimeHandlePropertyValue3 + " ms"); + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + + mTimeReadStartRecord + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + + mTimeParseLineAndHandleGroup + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + + mTimeHandleMiscPropertyValue + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); } - + private boolean isLetter(char ch) { if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { return true; diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java similarity index 91% rename from core/java/android/syncml/pim/vcard/VCardParser_V30.java rename to core/java/android/pim/vcard/VCardParser_V30.java index e67525eec3326a2ff15f5f4c125272a38a3ef4f4..384649a61f654c7c0e3d2a395a7372fc4633aa9c 100644 --- a/core/java/android/syncml/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.pim.vcard; -package android.syncml.pim.vcard; - +import android.pim.vcard.exception.VCardException; import android.util.Log; import java.io.IOException; @@ -27,9 +27,9 @@ import java.util.HashSet; * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426) */ public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "VCardParser_V30"; + private static final String LOG_TAG = "vcard.VCardParser_V30"; - private static final HashSet acceptablePropsWithParam = new HashSet( + private static final HashSet sAcceptablePropsWithParam = new HashSet( Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", @@ -47,14 +47,16 @@ public class VCardParser_V30 extends VCardParser_V21 { private String mPreviousLine; + private boolean mEmittedAgentWarning = false; + @Override protected String getVersion() { - return "3.0"; + return Constants.VERSION_V30; } @Override protected boolean isValidPropertyName(String propertyName) { - if (!(acceptablePropsWithParam.contains(propertyName) || + if (!(sAcceptablePropsWithParam.contains(propertyName) || acceptablePropsWithoutParam.contains(propertyName) || propertyName.startsWith("X-")) && !mWarningValueMap.contains(propertyName)) { @@ -221,7 +223,7 @@ public class VCardParser_V30 extends VCardParser_V21 { } @Override - protected void handleAgent(String propertyValue) throws VCardException { + protected void handleAgent(String propertyValue) { // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.0. // // e.g. @@ -238,7 +240,12 @@ public class VCardParser_V30 extends VCardParser_V21 { // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com // // This is not VCARD. Should we support this? - throw new VCardException("AGENT in vCard 3.0 is not supported yet."); + // throw new VCardException("AGENT in vCard 3.0 is not supported yet."); + if (!mEmittedAgentWarning) { + Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); + mEmittedAgentWarning = true; + } + // Just ignore the line for now, since we cannot know how to handle it... } /** @@ -284,7 +291,7 @@ public class VCardParser_V30 extends VCardParser_V21 { if (ch == '\\' && i < length - 1) { char next_ch = text.charAt(++i); if (next_ch == 'n' || next_ch == 'N') { - builder.append("\r\n"); + builder.append("\n"); } else { builder.append(next_ch); } @@ -296,9 +303,9 @@ public class VCardParser_V30 extends VCardParser_V21 { } @Override - protected String maybeUnescape(char ch) { + protected String maybeUnescapeCharacter(char ch) { if (ch == 'n' || ch == 'N') { - return "\r\n"; + return "\n"; } else { return String.valueOf(ch); } diff --git a/core/java/android/syncml/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java similarity index 97% rename from core/java/android/syncml/pim/vcard/VCardSourceDetector.java rename to core/java/android/pim/vcard/VCardSourceDetector.java index 8c483912335b54d42c44d058b61d9d41acb2697a..7e2be2b12eed5f85ce97130b5c09d4f3eaf07991 100644 --- a/core/java/android/syncml/pim/vcard/VCardSourceDetector.java +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package android.syncml.pim.vcard; - -import android.syncml.pim.VBuilder; +package android.pim.vcard; import java.util.Arrays; import java.util.HashSet; @@ -28,7 +25,7 @@ import java.util.Set; * Currently this implementation is very premature. * @hide */ -public class VCardSourceDetector implements VBuilder { +public class VCardSourceDetector implements VCardBuilder { // Should only be used in package. static final int TYPE_UNKNOWN = 0; static final int TYPE_APPLE = 1; diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..dd44288a881a82596a88cc3f45eb5e1c8dbed92c --- /dev/null +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard; + +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for VCard handling codes. + */ +public class VCardUtils { + /* + * TODO: some of methods in this class should be placed to the more appropriate place... + */ + + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is + // converted to two attribute Strings. These only contain some minor fields valid in both + // vCard and current (as of 2009-08-07) Contacts structure. + private static final Map sKnownPhoneTypesMap_ItoS; + private static final Set sPhoneTypesSetUnknownToContacts; + + private static final Map sKnownPhoneTypesMap_StoI; + + static { + sKnownPhoneTypesMap_ItoS = new HashMap(); + sKnownPhoneTypesMap_StoI = new HashMap(); + + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.ATTR_TYPE_CAR); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.ATTR_TYPE_ISDN); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); + + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); + + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypesMap_StoI.put( + Constants.ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TELEX, Phone.TYPE_TELEX); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_ASSISTANT, Phone.TYPE_ASSISTANT); + + sPhoneTypesSetUnknownToContacts = new HashSet(); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MODEM); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MSG); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_BBS); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_VIDEO); + } + + public static String getPhoneAttributeString(Integer type) { + return sKnownPhoneTypesMap_ItoS.get(type); + } + + /** + * Returns Interger when the given types can be parsed as known type. Returns String object + * when not, which should be set to label. + */ + public static Object getPhoneTypeFromStrings(Collection types) { + int type = -1; + String label = null; + boolean isFax = false; + boolean hasPref = false; + + if (types != null) { + for (String typeString : types) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + hasPref = true; + } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + isFax = true; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + if (tmp != null) { + type = tmp; + } else if (type < 0) { + type = Phone.TYPE_CUSTOM; + label = typeString; + } + } + } + } + if (type < 0) { + if (hasPref) { + type = Phone.TYPE_MAIN; + } else { + // default to TYPE_HOME + type = Phone.TYPE_HOME; + } + } + if (isFax) { + if (type == Phone.TYPE_HOME) { + type = Phone.TYPE_FAX_HOME; + } else if (type == Phone.TYPE_WORK) { + type = Phone.TYPE_FAX_WORK; + } else if (type == Phone.TYPE_OTHER) { + type = Phone.TYPE_OTHER_FAX; + } + } + if (type == Phone.TYPE_CUSTOM) { + return label; + } else { + return type; + } + } + + public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { + // TODO: check the following. + // - it may violate vCard spec + // - it may contain non-ASCII characters + // + // TODO: use vcardType + return (phoneAttribute.startsWith("X-") || phoneAttribute.startsWith("x-") || + sPhoneTypesSetUnknownToContacts.contains(phoneAttribute)); + } + + public static String[] sortNameElements(int vcardType, + String familyName, String middleName, String givenName) { + String[] list = new String[3]; + switch (VCardConfig.getNameOrderType(vcardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + // TODO: Should handle Ascii case? + list[0] = familyName; + list[1] = middleName; + list[2] = givenName; + break; + case VCardConfig.NAME_ORDER_EUROPE: + list[0] = middleName; + list[1] = givenName; + list[2] = familyName; + break; + default: + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + break; + } + return list; + } + + public static int getPhoneNumberFormat(final int vcardType) { + if (VCardConfig.isJapaneseDevice(vcardType)) { + return PhoneNumberUtils.FORMAT_JAPAN; + } else { + return PhoneNumberUtils.FORMAT_NANP; + } + } + + /** + * Inserts postal data into the builder object. + * + * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. See also + * {{@link #getVCardPostalElements(ContentValues)} + */ + public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, + final ContentProviderOperation.Builder builder, + final ContactStruct.PostalData postalData) { + builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredPostal.TYPE, postalData.type); + if (postalData.type == StructuredPostal.TYPE_CUSTOM) { + builder.withValue(StructuredPostal.LABEL, postalData.label); + } + + builder.withValue(StructuredPostal.POBOX, postalData.pobox); + // Extended address is dropped since there's no relevant entry in ContactsContract. + builder.withValue(StructuredPostal.STREET, postalData.street); + builder.withValue(StructuredPostal.CITY, postalData.localty); + builder.withValue(StructuredPostal.REGION, postalData.region); + builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); + builder.withValue(StructuredPostal.COUNTRY, postalData.country); + + builder.withValue(StructuredPostal.FORMATTED_ADDRESS, + postalData.getFormattedAddress(vcardType)); + if (postalData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + + /** + * Returns String[] containing address information based on vCard spec + * (PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name). + * All String objects are non-null ("" is used when the relevant data is empty). + * + * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. See also + * {{@link #insertStructuredPostalDataUsingContactsStruct(int, + * android.content.ContentProviderOperation.Builder, + * android.pim.vcard.ContactStruct.PostalData)} + */ + public static String[] getVCardPostalElements(ContentValues contentValues) { + String[] dataArray = new String[7]; + dataArray[0] = contentValues.getAsString(StructuredPostal.POBOX); + if (dataArray[0] == null) { + dataArray[0] = ""; + } + // Extended addr. There's no relevant data in ContactsContract. + dataArray[1] = ""; + dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); + if (dataArray[2] == null) { + dataArray[2] = ""; + } + // Assume that localty == city + dataArray[3] = contentValues.getAsString(StructuredPostal.CITY); + if (dataArray[3] == null) { + dataArray[3] = ""; + } + String region = contentValues.getAsString(StructuredPostal.REGION); + if (!TextUtils.isEmpty(region)) { + dataArray[4] = region; + } else { + dataArray[4] = ""; + } + dataArray[5] = contentValues.getAsString(StructuredPostal.POSTCODE); + if (dataArray[5] == null) { + dataArray[5] = ""; + } + dataArray[6] = contentValues.getAsString(StructuredPostal.COUNTRY); + if (dataArray[6] == null) { + dataArray[6] = ""; + } + + return dataArray; + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName) { + return constructNameFromElements(nameOrderType, familyName, middleName, givenName, + null, null); + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName, + String prefix, String suffix) { + StringBuilder builder = new StringBuilder(); + String[] nameList = sortNameElements(nameOrderType, + familyName, middleName, givenName); + boolean first = true; + if (!TextUtils.isEmpty(prefix)) { + first = false; + builder.append(prefix); + } + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + if (!TextUtils.isEmpty(suffix)) { + if (!first) { + builder.append(' '); + } + builder.append(suffix); + } + return builder.toString(); + } + + public static boolean containsOnlyPrintableAscii(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int length = str.length(); + final int asciiFirst = 0x20; + final int asciiLast = 0x126; + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int c = str.codePointAt(i); + if (c < asciiFirst || asciiLast < c) { + return false; + } + } + return true; + } + + /** + * This is useful when checking the string should be encoded into quoted-printable + * or not, which is required by vCard 2.1. + * See the definition of "7bit" in vCard 2.1 spec for more information. + */ + public static boolean containsOnlyNonCrLfPrintableAscii(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int length = str.length(); + final int asciiFirst = 0x20; + final int asciiLast = 0x126; + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int c = str.codePointAt(i); + if (c < asciiFirst || asciiLast < c || c == '\n' || c == '\r') { + return false; + } + } + return true; + } + + /** + * This is useful since vCard 3.0 often requires the ("X-") properties and groups + * should contain only alphabets, digits, and hyphen. + * + * Note: It is already known some devices (wrongly) outputs properties with characters + * which should not be in the field. One example is "X-GOOGLE TALK". We accept + * such kind of input but must never output it unless the target is very specific + * to the device which is able to parse the malformed input. + */ + public static boolean containsOnlyAlphaDigitHyphen(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int lowerAlphabetFirst = 0x41; // included ('A') + final int lowerAlphabetLast = 0x5b; // not included ('[') + final int upperAlphabetFirst = 0x61; // included ('a') + final int upperAlphabetLast = 0x7b; // included ('{') + final int digitFirst = 0x30; // included ('0') + final int digitLast = 0x39; // included ('9') + final int hyphen = '-'; + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int codepoint = str.codePointAt(i); + if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetLast) || + (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetLast) || + (digitFirst <= codepoint && codepoint < digitLast) || + (codepoint == hyphen))) { + return false; + } + } + return true; + } + + // TODO: Replace wth the method in Base64 class. + private static char PAD = '='; + private static final char[] ENCODE64 = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' + }; + + static public String encodeBase64(byte[] data) { + if (data == null) { + return ""; + } + + char[] charBuffer = new char[(data.length + 2) / 3 * 4]; + int position = 0; + int _3byte = 0; + for (int i=0; i> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = ENCODE64[_3byte & 0x3F]; + } + switch(data.length % 3) { + case 1: // [111111][11 0000][0000 00][000000] + _3byte = ((data[data.length-1] & 0xFF) << 16); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = PAD; + charBuffer[position++] = PAD; + break; + case 2: // [111111][11 1111][1111 00][000000] + _3byte = ((data[data.length-2] & 0xFF) << 16) + ((data[data.length-1] & 0xFF) << 8); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = PAD; + break; + } + + return new String(charBuffer); + } + + static public String toHalfWidthString(String orgString) { + if (TextUtils.isEmpty(orgString)) { + return null; + } + StringBuilder builder = new StringBuilder(); + int length = orgString.length(); + for (int i = 0; i < length; i++) { + // All Japanese character is able to be expressed by char. + // Do not need to use String#codepPointAt(). + char ch = orgString.charAt(i); + CharSequence halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); + if (halfWidthText != null) { + builder.append(halfWidthText); + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + private VCardUtils() { + } +} + +/** + * TextUtils especially for Japanese. + * TODO: make this in android.text in the future + */ +class JapaneseUtils { + static private final Map sHalfWidthMap = + new HashMap(); + + static { + // There's no logical mapping rule in Unicode. Sigh. + sHalfWidthMap.put('\u3001', "\uFF64"); + sHalfWidthMap.put('\u3002', "\uFF61"); + sHalfWidthMap.put('\u300C', "\uFF62"); + sHalfWidthMap.put('\u300D', "\uFF63"); + sHalfWidthMap.put('\u301C', "~"); + sHalfWidthMap.put('\u3041', "\uFF67"); + sHalfWidthMap.put('\u3042', "\uFF71"); + sHalfWidthMap.put('\u3043', "\uFF68"); + sHalfWidthMap.put('\u3044', "\uFF72"); + sHalfWidthMap.put('\u3045', "\uFF69"); + sHalfWidthMap.put('\u3046', "\uFF73"); + sHalfWidthMap.put('\u3047', "\uFF6A"); + sHalfWidthMap.put('\u3048', "\uFF74"); + sHalfWidthMap.put('\u3049', "\uFF6B"); + sHalfWidthMap.put('\u304A', "\uFF75"); + sHalfWidthMap.put('\u304B', "\uFF76"); + sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u304D', "\uFF77"); + sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u304F', "\uFF78"); + sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u3051', "\uFF79"); + sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u3053', "\uFF7A"); + sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u3055', "\uFF7B"); + sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u3057', "\uFF7C"); + sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u3059', "\uFF7D"); + sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u305B', "\uFF7E"); + sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u305D', "\uFF7F"); + sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u305F', "\uFF80"); + sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u3061', "\uFF81"); + sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u3063', "\uFF6F"); + sHalfWidthMap.put('\u3064', "\uFF82"); + sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u3066', "\uFF83"); + sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u3068', "\uFF84"); + sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u306A', "\uFF85"); + sHalfWidthMap.put('\u306B', "\uFF86"); + sHalfWidthMap.put('\u306C', "\uFF87"); + sHalfWidthMap.put('\u306D', "\uFF88"); + sHalfWidthMap.put('\u306E', "\uFF89"); + sHalfWidthMap.put('\u306F', "\uFF8A"); + sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u3072', "\uFF8B"); + sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u3075', "\uFF8C"); + sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u3078', "\uFF8D"); + sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u307B', "\uFF8E"); + sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u307E', "\uFF8F"); + sHalfWidthMap.put('\u307F', "\uFF90"); + sHalfWidthMap.put('\u3080', "\uFF91"); + sHalfWidthMap.put('\u3081', "\uFF92"); + sHalfWidthMap.put('\u3082', "\uFF93"); + sHalfWidthMap.put('\u3083', "\uFF6C"); + sHalfWidthMap.put('\u3084', "\uFF94"); + sHalfWidthMap.put('\u3085', "\uFF6D"); + sHalfWidthMap.put('\u3086', "\uFF95"); + sHalfWidthMap.put('\u3087', "\uFF6E"); + sHalfWidthMap.put('\u3088', "\uFF96"); + sHalfWidthMap.put('\u3089', "\uFF97"); + sHalfWidthMap.put('\u308A', "\uFF98"); + sHalfWidthMap.put('\u308B', "\uFF99"); + sHalfWidthMap.put('\u308C', "\uFF9A"); + sHalfWidthMap.put('\u308D', "\uFF9B"); + sHalfWidthMap.put('\u308E', "\uFF9C"); + sHalfWidthMap.put('\u308F', "\uFF9C"); + sHalfWidthMap.put('\u3090', "\uFF72"); + sHalfWidthMap.put('\u3091', "\uFF74"); + sHalfWidthMap.put('\u3092', "\uFF66"); + sHalfWidthMap.put('\u3093', "\uFF9D"); + sHalfWidthMap.put('\u309B', "\uFF9E"); + sHalfWidthMap.put('\u309C', "\uFF9F"); + sHalfWidthMap.put('\u30A1', "\uFF67"); + sHalfWidthMap.put('\u30A2', "\uFF71"); + sHalfWidthMap.put('\u30A3', "\uFF68"); + sHalfWidthMap.put('\u30A4', "\uFF72"); + sHalfWidthMap.put('\u30A5', "\uFF69"); + sHalfWidthMap.put('\u30A6', "\uFF73"); + sHalfWidthMap.put('\u30A7', "\uFF6A"); + sHalfWidthMap.put('\u30A8', "\uFF74"); + sHalfWidthMap.put('\u30A9', "\uFF6B"); + sHalfWidthMap.put('\u30AA', "\uFF75"); + sHalfWidthMap.put('\u30AB', "\uFF76"); + sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u30AD', "\uFF77"); + sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u30AF', "\uFF78"); + sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u30B1', "\uFF79"); + sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u30B3', "\uFF7A"); + sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u30B5', "\uFF7B"); + sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u30B7', "\uFF7C"); + sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u30B9', "\uFF7D"); + sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u30BB', "\uFF7E"); + sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u30BD', "\uFF7F"); + sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u30BF', "\uFF80"); + sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u30C1', "\uFF81"); + sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u30C3', "\uFF6F"); + sHalfWidthMap.put('\u30C4', "\uFF82"); + sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u30C6', "\uFF83"); + sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u30C8', "\uFF84"); + sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u30CA', "\uFF85"); + sHalfWidthMap.put('\u30CB', "\uFF86"); + sHalfWidthMap.put('\u30CC', "\uFF87"); + sHalfWidthMap.put('\u30CD', "\uFF88"); + sHalfWidthMap.put('\u30CE', "\uFF89"); + sHalfWidthMap.put('\u30CF', "\uFF8A"); + sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u30D2', "\uFF8B"); + sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u30D5', "\uFF8C"); + sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u30D8', "\uFF8D"); + sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u30DB', "\uFF8E"); + sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u30DE', "\uFF8F"); + sHalfWidthMap.put('\u30DF', "\uFF90"); + sHalfWidthMap.put('\u30E0', "\uFF91"); + sHalfWidthMap.put('\u30E1', "\uFF92"); + sHalfWidthMap.put('\u30E2', "\uFF93"); + sHalfWidthMap.put('\u30E3', "\uFF6C"); + sHalfWidthMap.put('\u30E4', "\uFF94"); + sHalfWidthMap.put('\u30E5', "\uFF6D"); + sHalfWidthMap.put('\u30E6', "\uFF95"); + sHalfWidthMap.put('\u30E7', "\uFF6E"); + sHalfWidthMap.put('\u30E8', "\uFF96"); + sHalfWidthMap.put('\u30E9', "\uFF97"); + sHalfWidthMap.put('\u30EA', "\uFF98"); + sHalfWidthMap.put('\u30EB', "\uFF99"); + sHalfWidthMap.put('\u30EC', "\uFF9A"); + sHalfWidthMap.put('\u30ED', "\uFF9B"); + sHalfWidthMap.put('\u30EE', "\uFF9C"); + sHalfWidthMap.put('\u30EF', "\uFF9C"); + sHalfWidthMap.put('\u30F0', "\uFF72"); + sHalfWidthMap.put('\u30F1', "\uFF74"); + sHalfWidthMap.put('\u30F2', "\uFF66"); + sHalfWidthMap.put('\u30F3', "\uFF9D"); + sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); + sHalfWidthMap.put('\u30F5', "\uFF76"); + sHalfWidthMap.put('\u30F6', "\uFF79"); + sHalfWidthMap.put('\u30FB', "\uFF65"); + sHalfWidthMap.put('\u30FC', "\uFF70"); + sHalfWidthMap.put('\uFF01', "!"); + sHalfWidthMap.put('\uFF02', "\""); + sHalfWidthMap.put('\uFF03', "#"); + sHalfWidthMap.put('\uFF04', "$"); + sHalfWidthMap.put('\uFF05', "%"); + sHalfWidthMap.put('\uFF06', "&"); + sHalfWidthMap.put('\uFF07', "'"); + sHalfWidthMap.put('\uFF08', "("); + sHalfWidthMap.put('\uFF09', ")"); + sHalfWidthMap.put('\uFF0A', "*"); + sHalfWidthMap.put('\uFF0B', "+"); + sHalfWidthMap.put('\uFF0C', ","); + sHalfWidthMap.put('\uFF0D', "-"); + sHalfWidthMap.put('\uFF0E', "."); + sHalfWidthMap.put('\uFF0F', "/"); + sHalfWidthMap.put('\uFF10', "0"); + sHalfWidthMap.put('\uFF11', "1"); + sHalfWidthMap.put('\uFF12', "2"); + sHalfWidthMap.put('\uFF13', "3"); + sHalfWidthMap.put('\uFF14', "4"); + sHalfWidthMap.put('\uFF15', "5"); + sHalfWidthMap.put('\uFF16', "6"); + sHalfWidthMap.put('\uFF17', "7"); + sHalfWidthMap.put('\uFF18', "8"); + sHalfWidthMap.put('\uFF19', "9"); + sHalfWidthMap.put('\uFF1A', ":"); + sHalfWidthMap.put('\uFF1B', ";"); + sHalfWidthMap.put('\uFF1C', "<"); + sHalfWidthMap.put('\uFF1D', "="); + sHalfWidthMap.put('\uFF1E', ">"); + sHalfWidthMap.put('\uFF1F', "?"); + sHalfWidthMap.put('\uFF20', "@"); + sHalfWidthMap.put('\uFF21', "A"); + sHalfWidthMap.put('\uFF22', "B"); + sHalfWidthMap.put('\uFF23', "C"); + sHalfWidthMap.put('\uFF24', "D"); + sHalfWidthMap.put('\uFF25', "E"); + sHalfWidthMap.put('\uFF26', "F"); + sHalfWidthMap.put('\uFF27', "G"); + sHalfWidthMap.put('\uFF28', "H"); + sHalfWidthMap.put('\uFF29', "I"); + sHalfWidthMap.put('\uFF2A', "J"); + sHalfWidthMap.put('\uFF2B', "K"); + sHalfWidthMap.put('\uFF2C', "L"); + sHalfWidthMap.put('\uFF2D', "M"); + sHalfWidthMap.put('\uFF2E', "N"); + sHalfWidthMap.put('\uFF2F', "O"); + sHalfWidthMap.put('\uFF30', "P"); + sHalfWidthMap.put('\uFF31', "Q"); + sHalfWidthMap.put('\uFF32', "R"); + sHalfWidthMap.put('\uFF33', "S"); + sHalfWidthMap.put('\uFF34', "T"); + sHalfWidthMap.put('\uFF35', "U"); + sHalfWidthMap.put('\uFF36', "V"); + sHalfWidthMap.put('\uFF37', "W"); + sHalfWidthMap.put('\uFF38', "X"); + sHalfWidthMap.put('\uFF39', "Y"); + sHalfWidthMap.put('\uFF3A', "Z"); + sHalfWidthMap.put('\uFF3B', "["); + sHalfWidthMap.put('\uFF3C', "\\"); + sHalfWidthMap.put('\uFF3D', "]"); + sHalfWidthMap.put('\uFF3E', "^"); + sHalfWidthMap.put('\uFF3F', "_"); + sHalfWidthMap.put('\uFF41', "a"); + sHalfWidthMap.put('\uFF42', "b"); + sHalfWidthMap.put('\uFF43', "c"); + sHalfWidthMap.put('\uFF44', "d"); + sHalfWidthMap.put('\uFF45', "e"); + sHalfWidthMap.put('\uFF46', "f"); + sHalfWidthMap.put('\uFF47', "g"); + sHalfWidthMap.put('\uFF48', "h"); + sHalfWidthMap.put('\uFF49', "i"); + sHalfWidthMap.put('\uFF4A', "j"); + sHalfWidthMap.put('\uFF4B', "k"); + sHalfWidthMap.put('\uFF4C', "l"); + sHalfWidthMap.put('\uFF4D', "m"); + sHalfWidthMap.put('\uFF4E', "n"); + sHalfWidthMap.put('\uFF4F', "o"); + sHalfWidthMap.put('\uFF50', "p"); + sHalfWidthMap.put('\uFF51', "q"); + sHalfWidthMap.put('\uFF52', "r"); + sHalfWidthMap.put('\uFF53', "s"); + sHalfWidthMap.put('\uFF54', "t"); + sHalfWidthMap.put('\uFF55', "u"); + sHalfWidthMap.put('\uFF56', "v"); + sHalfWidthMap.put('\uFF57', "w"); + sHalfWidthMap.put('\uFF58', "x"); + sHalfWidthMap.put('\uFF59', "y"); + sHalfWidthMap.put('\uFF5A', "z"); + sHalfWidthMap.put('\uFF5B', "{"); + sHalfWidthMap.put('\uFF5C', "|"); + sHalfWidthMap.put('\uFF5D', "}"); + sHalfWidthMap.put('\uFF5E', "~"); + sHalfWidthMap.put('\uFF61', "\uFF61"); + sHalfWidthMap.put('\uFF62', "\uFF62"); + sHalfWidthMap.put('\uFF63', "\uFF63"); + sHalfWidthMap.put('\uFF64', "\uFF64"); + sHalfWidthMap.put('\uFF65', "\uFF65"); + sHalfWidthMap.put('\uFF66', "\uFF66"); + sHalfWidthMap.put('\uFF67', "\uFF67"); + sHalfWidthMap.put('\uFF68', "\uFF68"); + sHalfWidthMap.put('\uFF69', "\uFF69"); + sHalfWidthMap.put('\uFF6A', "\uFF6A"); + sHalfWidthMap.put('\uFF6B', "\uFF6B"); + sHalfWidthMap.put('\uFF6C', "\uFF6C"); + sHalfWidthMap.put('\uFF6D', "\uFF6D"); + sHalfWidthMap.put('\uFF6E', "\uFF6E"); + sHalfWidthMap.put('\uFF6F', "\uFF6F"); + sHalfWidthMap.put('\uFF70', "\uFF70"); + sHalfWidthMap.put('\uFF71', "\uFF71"); + sHalfWidthMap.put('\uFF72', "\uFF72"); + sHalfWidthMap.put('\uFF73', "\uFF73"); + sHalfWidthMap.put('\uFF74', "\uFF74"); + sHalfWidthMap.put('\uFF75', "\uFF75"); + sHalfWidthMap.put('\uFF76', "\uFF76"); + sHalfWidthMap.put('\uFF77', "\uFF77"); + sHalfWidthMap.put('\uFF78', "\uFF78"); + sHalfWidthMap.put('\uFF79', "\uFF79"); + sHalfWidthMap.put('\uFF7A', "\uFF7A"); + sHalfWidthMap.put('\uFF7B', "\uFF7B"); + sHalfWidthMap.put('\uFF7C', "\uFF7C"); + sHalfWidthMap.put('\uFF7D', "\uFF7D"); + sHalfWidthMap.put('\uFF7E', "\uFF7E"); + sHalfWidthMap.put('\uFF7F', "\uFF7F"); + sHalfWidthMap.put('\uFF80', "\uFF80"); + sHalfWidthMap.put('\uFF81', "\uFF81"); + sHalfWidthMap.put('\uFF82', "\uFF82"); + sHalfWidthMap.put('\uFF83', "\uFF83"); + sHalfWidthMap.put('\uFF84', "\uFF84"); + sHalfWidthMap.put('\uFF85', "\uFF85"); + sHalfWidthMap.put('\uFF86', "\uFF86"); + sHalfWidthMap.put('\uFF87', "\uFF87"); + sHalfWidthMap.put('\uFF88', "\uFF88"); + sHalfWidthMap.put('\uFF89', "\uFF89"); + sHalfWidthMap.put('\uFF8A', "\uFF8A"); + sHalfWidthMap.put('\uFF8B', "\uFF8B"); + sHalfWidthMap.put('\uFF8C', "\uFF8C"); + sHalfWidthMap.put('\uFF8D', "\uFF8D"); + sHalfWidthMap.put('\uFF8E', "\uFF8E"); + sHalfWidthMap.put('\uFF8F', "\uFF8F"); + sHalfWidthMap.put('\uFF90', "\uFF90"); + sHalfWidthMap.put('\uFF91', "\uFF91"); + sHalfWidthMap.put('\uFF92', "\uFF92"); + sHalfWidthMap.put('\uFF93', "\uFF93"); + sHalfWidthMap.put('\uFF94', "\uFF94"); + sHalfWidthMap.put('\uFF95', "\uFF95"); + sHalfWidthMap.put('\uFF96', "\uFF96"); + sHalfWidthMap.put('\uFF97', "\uFF97"); + sHalfWidthMap.put('\uFF98', "\uFF98"); + sHalfWidthMap.put('\uFF99', "\uFF99"); + sHalfWidthMap.put('\uFF9A', "\uFF9A"); + sHalfWidthMap.put('\uFF9B', "\uFF9B"); + sHalfWidthMap.put('\uFF9C', "\uFF9C"); + sHalfWidthMap.put('\uFF9D', "\uFF9D"); + sHalfWidthMap.put('\uFF9E', "\uFF9E"); + sHalfWidthMap.put('\uFF9F', "\uFF9F"); + sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); + } + + /** + * Return half-width version of that character if possible. Return null if not possible + * @param ch input character + * @return CharSequence object if the mapping for ch exists. Return null otherwise. + */ + public static CharSequence tryGetHalfWidthText(char ch) { + if (sHalfWidthMap.containsKey(ch)) { + return sHalfWidthMap.get(ch); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/VCardException.java b/core/java/android/pim/vcard/exception/VCardException.java similarity index 72% rename from core/java/android/syncml/pim/vcard/VCardException.java rename to core/java/android/pim/vcard/exception/VCardException.java index 35b31ecfc5e064085e9ed511d119c8f10863daa3..e557219f235a411549bf38b0c7cbb716ac90cb3b 100644 --- a/core/java/android/syncml/pim/vcard/VCardException.java +++ b/core/java/android/pim/vcard/exception/VCardException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.pim.vcard.exception; -package android.syncml.pim.vcard; - -public class VCardException extends java.lang.Exception{ - // constructors - +public class VCardException extends java.lang.Exception { /** * Constructs a VCardException object */ - - public VCardException() - { + public VCardException() { + super(); } /** @@ -32,10 +28,8 @@ public class VCardException extends java.lang.Exception{ * * @param message the error message */ - - public VCardException( String message ) - { - super( message ); + public VCardException(String message) { + super(message); } } diff --git a/core/java/com/android/internal/backup/SystemBackupAgent.java b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java similarity index 53% rename from core/java/com/android/internal/backup/SystemBackupAgent.java rename to core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java index 6b396d777a664647c1f7cade961935f325b368e2..67db62ce86b5143b6d11d77168f7d5ec08d030c1 100644 --- a/core/java/com/android/internal/backup/SystemBackupAgent.java +++ b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java @@ -14,22 +14,19 @@ * limitations under the License. */ -package com.android.internal.backup; - -import android.backup.AbsoluteFileBackupHelper; -import android.backup.BackupHelperAgent; +package android.pim.vcard.exception; /** - * Backup agent for various system-managed data + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. */ -public class SystemBackupAgent extends BackupHelperAgent { - // the set of files that we back up whole, as absolute paths - String[] mFiles = { - /* WallpaperService.WALLPAPER_FILE */ - "/data/data/com.android.settings/files/wallpaper", - }; +public class VCardInvalidCommentLineException extends VCardInvalidLineException { + public VCardInvalidCommentLineException() { + super(); + } - public void onCreate() { - addHelper("system_files", new AbsoluteFileBackupHelper(this, mFiles)); + public VCardInvalidCommentLineException(final String message) { + super(message); } } diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java new file mode 100644 index 0000000000000000000000000000000000000000..330153ec2638db6c800f6ad07d9f7311d8079487 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.pim.vcard.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidLineException extends VCardException { + public VCardInvalidLineException() { + super(); + } + + public VCardInvalidLineException(final String message) { + super(message); + } +} diff --git a/core/java/android/syncml/pim/vcard/VCardNestedException.java b/core/java/android/pim/vcard/exception/VCardNestedException.java similarity index 82% rename from core/java/android/syncml/pim/vcard/VCardNestedException.java rename to core/java/android/pim/vcard/exception/VCardNestedException.java index def6f3b785ffaecab38c225582285c65b782581d..503c2fbcf536a26046909363a8ee3518773ce015 100644 --- a/core/java/android/syncml/pim/vcard/VCardNestedException.java +++ b/core/java/android/pim/vcard/exception/VCardNestedException.java @@ -14,13 +14,15 @@ * limitations under the License. */ -package android.syncml.pim.vcard; +package android.pim.vcard.exception; /** * VCardException thrown when VCard is nested without VCardParser's being notified. */ -public class VCardNestedException extends VCardException { - public VCardNestedException() {} +public class VCardNestedException extends VCardNotSupportedException { + public VCardNestedException() { + super(); + } public VCardNestedException(String message) { super(message); } diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java new file mode 100644 index 0000000000000000000000000000000000000000..616aa7763b0e19f8c346b45d13958394245a86c3 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.pim.vcard.exception; + +/** + * The exception which tells that the input VCard is probably valid from the view of + * specification but not supported in the current framework for now. + * + * This is a kind of a good news from the view of development. + * It may be good to ask users to send a report with the VCard example + * for the future development. + */ +public class VCardNotSupportedException extends VCardException { + public VCardNotSupportedException() { + super(); + } + public VCardNotSupportedException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/VCardVersionException.java b/core/java/android/pim/vcard/exception/VCardVersionException.java similarity index 94% rename from core/java/android/syncml/pim/vcard/VCardVersionException.java rename to core/java/android/pim/vcard/exception/VCardVersionException.java index 1ca88d120095775efa66ebfae078c50d08f730a3..9fe8b7f92af92053b1759e99c30165004a88e461 100644 --- a/core/java/android/syncml/pim/vcard/VCardVersionException.java +++ b/core/java/android/pim/vcard/exception/VCardVersionException.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package android.syncml.pim.vcard; +package android.pim.vcard.exception; /** * VCardException used only when the version of the vCard is different. */ public class VCardVersionException extends VCardException { public VCardVersionException() { + super(); } - public VCardVersionException(String message) { super(message); } diff --git a/core/java/android/syncml/pim/vcard/package.html b/core/java/android/pim/vcard/exception/package.html similarity index 56% rename from core/java/android/syncml/pim/vcard/package.html rename to core/java/android/pim/vcard/exception/package.html index cb4ca466da01a6c1505c3a009638704294f96562..26b8a328b13212437892154b970ce092e6003af6 100644 --- a/core/java/android/syncml/pim/vcard/package.html +++ b/core/java/android/pim/vcard/exception/package.html @@ -1,6 +1,5 @@ -Support classes for SyncML. {@hide} \ No newline at end of file diff --git a/core/java/android/syncml/package.html b/core/java/android/pim/vcard/package.html similarity index 56% rename from core/java/android/syncml/package.html rename to core/java/android/pim/vcard/package.html index cb4ca466da01a6c1505c3a009638704294f96562..26b8a328b13212437892154b970ce092e6003af6 100644 --- a/core/java/android/syncml/package.html +++ b/core/java/android/pim/vcard/package.html @@ -1,6 +1,5 @@ -Support classes for SyncML. {@hide} \ No newline at end of file diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index cf5664c3814e9c8b071c621d81b51d5297684ff4..f16a7e479cb1970e64d09730e7bd9b95a3ee5ee6 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -149,14 +149,12 @@ public class CheckBoxPreference extends Preference { * @param checked The checked state. */ public void setChecked(boolean checked) { - - mChecked = checked; - - persistBoolean(checked); - - notifyDependencyChange(shouldDisableDependents()); - - notifyChanged(); + if (mChecked != checked) { + mChecked = checked; + persistBoolean(checked); + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } } /** diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index 666efaeaffd3713b9ec9455c399bfb10d3da1877..cc48aeb70844986fa47f923d4b921425309120a0 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -31,6 +31,9 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; import android.widget.TextView; /** @@ -297,10 +300,32 @@ public abstract class DialogPreference extends Preference implements if (state != null) { dialog.onRestoreInstanceState(state); } + if (needInputMethod()) { + requestInputMethod(dialog); + } dialog.setOnDismissListener(this); dialog.show(); } - + + /** + * Returns whether the preference needs to display a soft input method when the dialog + * is displayed. Default is false. Subclasses should override this method if they need + * the soft input method brought up automatically. + * @hide + */ + protected boolean needInputMethod() { + return false; + } + + /** + * Sets the required flags on the dialog window to enable input method window to show up. + */ + private void requestInputMethod(Dialog dialog) { + Window window = dialog.getWindow(); + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE | + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } + /** * Creates the content view for the dialog (if a custom content view is * required). By default, it inflates the dialog layout resource if it is diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index a12704f4820a3fcd45d25bcb9c0dd3983117006a..84ee950becee9e386b79b5df361162d3b0e508d0 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -169,6 +169,13 @@ public class EditTextPreference extends DialogPreference { return mEditText; } + /** @hide */ + @Override + protected boolean needInputMethod() { + // We want the input method to show, if possible, when dialog is displayed + return true; + } + @Override protected Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index fc395732f47e12ada63e9c0d550b306332745f5a..08a2a9f37aaa45901c93e4e4a2f6955fd278fe9b 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -1456,8 +1456,10 @@ public class Preference implements Comparable, OnDependencyChangeLis if (!TextUtils.isEmpty(summary)) { sb.append(summary).append(' '); } - // Drop the last space - sb.setLength(sb.length() - 1); + if (sb.length() > 0) { + // Drop the last space + sb.setLength(sb.length() - 1); + } return sb; } diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java index a7a3eef68b6f42f9bd8477b5d24b2d2e2d254261..fe3471dd94e327effca70319be155767a7afb842 100644 --- a/core/java/android/preference/PreferenceManager.java +++ b/core/java/android/preference/PreferenceManager.java @@ -134,7 +134,10 @@ public class PreferenceManager { private OnPreferenceTreeClickListener mOnPreferenceTreeClickListener; - PreferenceManager(Activity activity, int firstRequestCode) { + /** + * @hide + */ + public PreferenceManager(Activity activity, int firstRequestCode) { mActivity = activity; mNextRequestCode = firstRequestCode; @@ -240,8 +243,9 @@ public class PreferenceManager { * hierarchies into. * @return The root hierarchy (if one was not provided, the new hierarchy's * root). + * @hide */ - PreferenceScreen inflateFromResource(Context context, int resId, + public PreferenceScreen inflateFromResource(Context context, int resId, PreferenceScreen rootPreferences) { // Block commits setNoCommit(true); diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java index 6beb06dc470e9100d38f09d1e99b0669cc1b6c9f..b46f1804cb8f4358aed2c312cf75e7a22959d0cf 100644 --- a/core/java/android/preference/RingtonePreference.java +++ b/core/java/android/preference/RingtonePreference.java @@ -31,8 +31,9 @@ import android.util.Log; * The chosen ringtone's URI will be persisted as a string. *

    * If the user chooses the "Default" item, the saved string will be one of - * {@link System#DEFAULT_RINGTONE_URI} or - * {@link System#DEFAULT_NOTIFICATION_URI}. If the user chooses the "Silent" + * {@link System#DEFAULT_RINGTONE_URI}, + * {@link System#DEFAULT_NOTIFICATION_URI}, or + * {@link System#DEFAULT_ALARM_ALERT_URI}. If the user chooses the "Silent" * item, the saved string will be an empty string. * * @attr ref android.R.styleable#RingtonePreference_ringtoneType diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index 20702a1de4138a27e87a854e835e3fd49b9626a5..a2645949b8a55257615d380ee605bf552253a0cf 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -16,17 +16,21 @@ package android.preference; +import android.app.Dialog; import android.content.Context; import android.content.res.TypedArray; import android.database.ContentObserver; +import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; -import android.media.AudioManager; +import android.net.Uri; import android.os.Handler; -import android.preference.PreferenceManager; +import android.os.Parcel; +import android.os.Parcelable; import android.provider.Settings; import android.provider.Settings.System; import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.View; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; @@ -35,12 +39,12 @@ import android.widget.SeekBar.OnSeekBarChangeListener; * @hide */ public class VolumePreference extends SeekBarPreference implements - PreferenceManager.OnActivityStopListener { + PreferenceManager.OnActivityStopListener, View.OnKeyListener { private static final String TAG = "VolumePreference"; private int mStreamType; - + /** May be null if the dialog isn't visible. */ private SeekBarVolumizer mSeekBarVolumizer; @@ -52,7 +56,7 @@ public class VolumePreference extends SeekBarPreference implements mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0); a.recycle(); } - + public void setStreamType(int streamType) { mStreamType = streamType; } @@ -63,8 +67,34 @@ public class VolumePreference extends SeekBarPreference implements final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); mSeekBarVolumizer = new SeekBarVolumizer(getContext(), seekBar, mStreamType); - + getPreferenceManager().registerOnActivityStopListener(this); + + // grab focus and key events so that pressing the volume buttons in the + // dialog doesn't also show the normal volume adjust toast. + view.setOnKeyListener(this); + view.setFocusableInTouchMode(true); + view.requestFocus(); + } + + public boolean onKey(View v, int keyCode, KeyEvent event) { + // If key arrives immediately after the activity has been cleaned up. + if (mSeekBarVolumizer == null) return true; + boolean isdown = (event.getAction() == KeyEvent.ACTION_DOWN); + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (isdown) { + mSeekBarVolumizer.changeVolumeBy(-1); + } + return true; + case KeyEvent.KEYCODE_VOLUME_UP: + if (isdown) { + mSeekBarVolumizer.changeVolumeBy(1); + } + return true; + default: + return false; + } } @Override @@ -74,7 +104,7 @@ public class VolumePreference extends SeekBarPreference implements if (!positiveResult && mSeekBarVolumizer != null) { mSeekBarVolumizer.revertVolume(); } - + cleanup(); } @@ -87,19 +117,99 @@ public class VolumePreference extends SeekBarPreference implements */ private void cleanup() { getPreferenceManager().unregisterOnActivityStopListener(this); - + if (mSeekBarVolumizer != null) { + Dialog dialog = getDialog(); + if (dialog != null && dialog.isShowing()) { + View view = dialog.getWindow().getDecorView() + .findViewById(com.android.internal.R.id.seekbar); + if (view != null) view.setOnKeyListener(null); + // Stopped while dialog was showing, revert changes + mSeekBarVolumizer.revertVolume(); + } mSeekBarVolumizer.stop(); mSeekBarVolumizer = null; } + } - + protected void onSampleStarting(SeekBarVolumizer volumizer) { if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) { mSeekBarVolumizer.stopSample(); } } - + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + if (mSeekBarVolumizer != null) { + mSeekBarVolumizer.onSaveInstanceState(myState.getVolumeStore()); + } + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (mSeekBarVolumizer != null) { + mSeekBarVolumizer.onRestoreInstanceState(myState.getVolumeStore()); + } + } + + public static class VolumeStore { + public int volume = -1; + public int originalVolume = -1; + } + + private static class SavedState extends BaseSavedState { + VolumeStore mVolumeStore = new VolumeStore(); + + public SavedState(Parcel source) { + super(source); + mVolumeStore.volume = source.readInt(); + mVolumeStore.originalVolume = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mVolumeStore.volume); + dest.writeInt(mVolumeStore.originalVolume); + } + + VolumeStore getVolumeStore() { + return mVolumeStore; + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + /** * Turns a {@link SeekBar} into a volume control. */ @@ -113,7 +223,7 @@ public class VolumePreference extends SeekBarPreference implements private int mOriginalStreamVolume; private Ringtone mRingtone; - private int mLastProgress; + private int mLastProgress = -1; private SeekBar mSeekBar; private ContentObserver mVolumeObserver = new ContentObserver(mHandler) { @@ -127,7 +237,7 @@ public class VolumePreference extends SeekBarPreference implements } } }; - + public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType) { mContext = context; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -147,11 +257,19 @@ public class VolumePreference extends SeekBarPreference implements System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), false, mVolumeObserver); - mRingtone = RingtoneManager.getRingtone(mContext, - mStreamType == AudioManager.STREAM_NOTIFICATION - ? Settings.System.DEFAULT_NOTIFICATION_URI - : Settings.System.DEFAULT_RINGTONE_URI); - mRingtone.setStreamType(mStreamType); + Uri defaultUri = null; + if (mStreamType == AudioManager.STREAM_RING) { + defaultUri = Settings.System.DEFAULT_RINGTONE_URI; + } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) { + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; + } else { + defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI; + } + + mRingtone = RingtoneManager.getRingtone(mContext, defaultUri); + if (mRingtone != null) { + mRingtone.setStreamType(mStreamType); + } } public void stop() { @@ -173,7 +291,7 @@ public class VolumePreference extends SeekBarPreference implements postSetVolume(progress); } - private void postSetVolume(int progress) { + void postSetVolume(int progress) { // Do the volume changing separately to give responsive UI mLastProgress = progress; mHandler.removeCallbacks(this); @@ -208,5 +326,27 @@ public class VolumePreference extends SeekBarPreference implements return mSeekBar; } + public void changeVolumeBy(int amount) { + mSeekBar.incrementProgressBy(amount); + if (mRingtone != null && !mRingtone.isPlaying()) { + sample(); + } + postSetVolume(mSeekBar.getProgress()); + } + + public void onSaveInstanceState(VolumeStore volumeStore) { + if (mLastProgress >= 0) { + volumeStore.volume = mLastProgress; + volumeStore.originalVolume = mOriginalStreamVolume; + } + } + + public void onRestoreInstanceState(VolumeStore volumeStore) { + if (volumeStore.volume != -1) { + mOriginalStreamVolume = volumeStore.originalVolume; + mLastProgress = volumeStore.volume; + postSetVolume(mLastProgress); + } + } } } diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index 1ba5e25e1d54c3a201f6673c17b18542c5727e21..c8b7f99d64c5d90df6bd2d281f0ee0f0b51cbacf 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -107,7 +107,8 @@ public class Browser { public static final String[] HISTORY_PROJECTION = new String[] { BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, - BookmarkColumns.FAVICON }; + BookmarkColumns.FAVICON, BookmarkColumns.THUMBNAIL, + BookmarkColumns.TOUCH_ICON }; /* these indices dependent on HISTORY_PROJECTION */ public static final int HISTORY_PROJECTION_ID_INDEX = 0; @@ -117,6 +118,14 @@ public class Browser { public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4; public static final int HISTORY_PROJECTION_TITLE_INDEX = 5; public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6; + /** + * @hide + */ + public static final int HISTORY_PROJECTION_THUMBNAIL_INDEX = 7; + /** + * @hide + */ + public static final int HISTORY_PROJECTION_TOUCH_ICON_INDEX = 8; /* columns needed to determine whether to truncate history */ public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { @@ -165,13 +174,29 @@ public class Browser { } public static final void sendString(Context c, String s) { + sendString(c, s, + c.getText(com.android.internal.R.string.sendText).toString()); + } + + /** + * Find an application to handle the given string and, if found, invoke + * it with the given string as a parameter. + * @param c Context used to launch the new activity. + * @param stringToSend The string to be handled. + * @param chooserDialogTitle The title of the dialog that allows the user + * to select between multiple applications that are all capable of handling + * the string. + * @hide pending API council approval + */ + public static final void sendString(Context c, + String stringToSend, + String chooserDialogTitle) { Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); - send.putExtra(Intent.EXTRA_TEXT, s); - + send.putExtra(Intent.EXTRA_TEXT, stringToSend); + try { - c.startActivity(Intent.createChooser(send, - c.getText(com.android.internal.R.string.sendText))); + c.startActivity(Intent.createChooser(send, chooserDialogTitle)); } catch(android.content.ActivityNotFoundException ex) { // if no app handles it, do nothing } @@ -248,6 +273,32 @@ public class Browser { } } + /** + * Returns all the URLs in the history. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * @param cr The ContentResolver used to access the database. + * @hide pending API council approval + */ + public static final String[] getVisitedHistory(ContentResolver cr) { + try { + String[] projection = new String[] { + "url" + }; + Cursor c = cr.query(BOOKMARKS_URI, projection, "visits > 0", null, + null); + String[] str = new String[c.getCount()]; + int i = 0; + while (c.moveToNext()) { + str[i] = c.getString(0); + i++; + } + c.deactivate(); + return str; + } catch (IllegalStateException e) { + return new String[0]; + } + } + /** * If there are more than MAX_HISTORY_COUNT non-bookmark history * items in the bookmark/history table, delete TRUNCATE_N_OLDEST @@ -513,6 +564,14 @@ public class Browser { public static final String TITLE = "title"; public static final String CREATED = "created"; public static final String FAVICON = "favicon"; + /** + * @hide + */ + public static final String THUMBNAIL = "thumbnail"; + /** + * @hide + */ + public static final String TOUCH_ICON = "touch_icon"; } public static class SearchColumns implements BaseColumns { diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index 4a709f6c7517a8d9c07730c7ba76d2daf6bb7197..f046cefc70f40fc5404eadddeeefc63a92419d91 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -32,6 +32,7 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; import android.util.Log; +import android.accounts.Account; import com.android.internal.database.ArrayListCursor; import com.google.android.gdata.client.AndroidGDataClient; import com.google.android.gdata.client.AndroidXmlParserFactory; @@ -79,6 +80,11 @@ public final class Calendar { */ public interface CalendarsColumns { + /** + * A string that uniquely identifies this contact to its source + */ + public static final String SOURCE_ID = "sourceid"; + /** * The color of the calendar *

    Type: INTEGER (color value)

    @@ -104,6 +110,7 @@ public final class Calendar { public static final int EDITOR_ACCESS = 600; /** Full access to the calendar */ public static final int OWNER_ACCESS = 700; + /** Domain admin */ public static final int ROOT_ACCESS = 800; /** @@ -124,6 +131,12 @@ public final class Calendar { *

    Type: INTEGER (boolean)

    */ public static final String SYNC_EVENTS = "sync_events"; + + /** + * Sync state data. + *

    Type: String (blob)

    + */ + public static final String SYNC_STATE = "sync_state"; } /** @@ -157,11 +170,12 @@ public final class Calendar { * @param account the account whose rows should be deleted * @return the count of rows that were deleted */ - public static int deleteCalendarsForAccount(ContentResolver cr, - String account) { + public static int deleteCalendarsForAccount(ContentResolver cr, Account account) { // delete all calendars that match this account - return Calendar.Calendars.delete(cr, Calendar.Calendars._SYNC_ACCOUNT + "=?", - new String[] {account}); + return Calendar.Calendars.delete(cr, + Calendar.Calendars._SYNC_ACCOUNT + "=? AND " + + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=?", + new String[] {account.name, account.type}); } /** @@ -170,9 +184,6 @@ public final class Calendar { public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendars"); - public static final Uri LIVE_CONTENT_URI = - Uri.parse("content://calendar/calendars?update=1"); - /** * The default sort order for this table */ @@ -207,6 +218,13 @@ public final class Calendar { *

    Type: INTEGER (boolean)

    */ public static final String HIDDEN = "hidden"; + + /** + * The owner account for this calendar, based on the calendar feed. + * This will be different from the _SYNC_ACCOUNT for delegated calendars. + *

    Type: String

    + */ + public static final String OWNER_ACCOUNT = "ownerAccount"; } public interface AttendeesColumns { @@ -448,6 +466,54 @@ public final class Calendar { *

    Type: INTEGER (long; millis since epoch)

    */ public static final String LAST_DATE = "lastDate"; + + /** + * Whether the event has attendee information. True if the event + * has full attendee data, false if the event has information about + * self only. + *

    Type: INTEGER (boolean)

    + */ + public static final String HAS_ATTENDEE_DATA = "hasAttendeeData"; + + /** + * Whether guests can modify the event. + *

    Type: INTEGER (boolean)

    + */ + public static final String GUESTS_CAN_MODIFY = "guestsCanModify"; + + /** + * Whether guests can invite other guests. + *

    Type: INTEGER (boolean)

    + */ + public static final String GUESTS_CAN_INVITE_OTHERS = "guestsCanInviteOthers"; + + /** + * Whether guests can see the list of attendees. + *

    Type: INTEGER (boolean)

    + */ + public static final String GUESTS_CAN_SEE_GUESTS = "guestsCanSeeGuests"; + + /** + * Email of the organizer (owner) of the event. + *

    Type: STRING

    + */ + public static final String ORGANIZER = "organizer"; + + /** + * Whether the user can invite others to the event. + * The GUESTS_CAN_INVITE_OTHERS is a setting that applies to an arbitrary guest, + * while CAN_INVITE_OTHERS indicates if the user can invite others (either through + * GUESTS_CAN_INVITE_OTHERS or because the user has modify access to the event). + *

    Type: INTEGER (boolean, readonly)

    + */ + public static final String CAN_INVITE_OTHERS = "canInviteOthers"; + + /** + * The owner account for this calendar, based on the calendar (foreign + * key into the calendars table). + *

    Type: String

    + */ + public static final String OWNER_ACCOUNT = "ownerAccount"; } /** @@ -694,6 +760,8 @@ public final class Calendar { * The content:// style URL for this table */ public static final Uri CONTENT_URI = Uri.parse("content://calendar/instances/when"); + public static final Uri CONTENT_BY_DAY_URI = + Uri.parse("content://calendar/instances/whenbyday"); /** * The default sort order for this table. diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index afe219c8fe88d8a81b2222cf4eb8a9c4de1115eb..7854423ade4dc2949bc7b76bc984ff1aca432662 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -16,16 +16,14 @@ package android.provider; +import com.android.internal.telephony.CallerInfo; +import com.android.internal.telephony.Connection; + import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; -import android.provider.Contacts.People; -import com.android.internal.telephony.CallerInfo; -import com.android.internal.telephony.Connection; - import android.text.TextUtils; -import android.util.Log; /** * The CallLog provider contains information about placed and received calls. @@ -73,7 +71,7 @@ public class CallLog { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/calls"; /** - * The type of the the phone number. + * The type of the call (incoming, outgoing or missed). *

    Type: INTEGER (int)

    */ public static final String TYPE = "type"; @@ -179,7 +177,7 @@ public class CallLog { } if ((ci != null) && (ci.person_id > 0)) { - People.markAsContacted(resolver, ci.person_id); + ContactsContract.Contacts.markAsContacted(resolver, ci.person_id); } Uri result = resolver.insert(CONTENT_URI, values); diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 6b491ab6f89a7a13fec0a9cc46dfada11c46874e..84753ee56c4a2ac5d2da2fd6aecf117b1439a04e 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -59,6 +59,8 @@ public final class Checkin { /** Valid tag values. Extend as necessary for your needs. */ public enum Tag { + APANIC_CONSOLE, + APANIC_THREADS, AUTOTEST_FAILURE, AUTOTEST_SEQUENCE_BEGIN, AUTOTEST_SUITE_BEGIN, diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index 84fe1841888ec686c02a52db264bce1315a25338..1a381669785e3ccfca373e4e78e7659dfe9bd982 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -22,11 +22,11 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; -import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; @@ -36,27 +36,58 @@ import java.io.InputStream; /** * The Contacts provider stores all information about contacts. + * + * @deprecated The APIs have been superseded by {@link ContactsContract}. The newer APIs allow + * access multiple accounts and support aggregation of similar contacts. These APIs continue to + * work but will only return data for the first Google account created, which matches the original + * behavior. */ +@Deprecated public class Contacts { private static final String TAG = "Contacts"; - + + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final String AUTHORITY = "contacts"; /** * The content:// style URL for this provider + * @deprecated see {@link android.provider.ContactsContract} */ - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY); + @Deprecated + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); - /** Signifies an email address row that is stored in the ContactMethods table */ + /** + * Signifies an email address row that is stored in the ContactMethods table + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int KIND_EMAIL = 1; - /** Signifies a postal address row that is stored in the ContactMethods table */ + /** + * Signifies a postal address row that is stored in the ContactMethods table + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int KIND_POSTAL = 2; - /** Signifies an IM address row that is stored in the ContactMethods table */ + /** + * Signifies an IM address row that is stored in the ContactMethods table + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int KIND_IM = 3; - /** Signifies an Organization row that is stored in the Organizations table */ + /** + * Signifies an Organization row that is stored in the Organizations table + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int KIND_ORGANIZATION = 4; - /** Signifies an Phone row that is stored in the Phones table */ + /** + * Signifies an Phone row that is stored in the Phones table + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int KIND_PHONE = 5; /** @@ -66,30 +97,48 @@ public class Contacts { /** * Columns from the Settings table that other columns join into themselves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface SettingsColumns { /** * The _SYNC_ACCOUNT to which this setting corresponds. This may be null. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String _SYNC_ACCOUNT = "_sync_account"; + /** + * The _SYNC_ACCOUNT_TYPE to which this setting corresponds. This may be null. + *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + /** * The key of this setting. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String KEY = "key"; /** * The value of this setting. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String VALUE = "value"; } /** * The settings over all of the people + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Settings implements BaseColumns, SettingsColumns { /** * no public constructor since this is a utility class @@ -98,18 +147,24 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/settings"); /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "settings"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "key ASC"; /** @@ -120,9 +175,15 @@ public class Contacts { *

    * This is a boolean setting. It is true if it is set and it is anything other than the * emptry string or "0". + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SYNC_EVERYTHING = "syncEverything"; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static String getSetting(ContentResolver cr, String account, String key) { // For now we only support a single account and the UI doesn't know what // the account name is, so we're using a global setting for SYNC_EVERYTHING. @@ -134,6 +195,7 @@ public class Contacts { selectString = (account == null) ? "_sync_account is null AND key=?" : "_sync_account=? AND key=?"; +// : "_sync_account=? AND _sync_account_type=? AND key=?"; selectArgs = (account == null) ? new String[]{key} : new String[]{account, key}; @@ -151,6 +213,10 @@ public class Contacts { } } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static void setSetting(ContentResolver cr, String account, String key, String value) { ContentValues values = new ContentValues(); @@ -158,7 +224,8 @@ public class Contacts { // the account name is, so we're using a global setting for SYNC_EVERYTHING. // Some day when we add multiple accounts to the UI this should honor the account // that was asked for. - //values.put(_SYNC_ACCOUNT, account); + //values.put(_SYNC_ACCOUNT, account.mName); + //values.put(_SYNC_ACCOUNT_TYPE, account.mType); values.put(KEY, key); values.put(VALUE, value); cr.update(Settings.CONTENT_URI, values, null, null); @@ -167,12 +234,16 @@ public class Contacts { /** * Columns from the People table that other tables join into themselves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface PeopleColumns { /** * The person's name. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NAME = "name"; /** @@ -180,14 +251,18 @@ public class Contacts { * character set (e.g. hiragana for Japanese). * Used for pronunciation and/or collation in some languages. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PHONETIC_NAME = "phonetic_name"; - + /** * The display name. If name is not null name, else if number is not null number, * else if email is not null email. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DISPLAY_NAME = "display_name"; /** @@ -195,80 +270,105 @@ public class Contacts { * may not be human readable but phonetically sortable. *

    Type: TEXT

    * @hide Used only in Contacts application for now. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SORT_STRING = "sort_string"; - + /** * Notes about the person. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NOTES = "notes"; /** * The number of times a person has been contacted *

    Type: INTEGER

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TIMES_CONTACTED = "times_contacted"; /** * The last time a person was contacted. *

    Type: INTEGER

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LAST_TIME_CONTACTED = "last_time_contacted"; /** * A custom ringtone associated with a person. Not always present. *

    Type: TEXT (URI to the ringtone)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CUSTOM_RINGTONE = "custom_ringtone"; /** * Whether the person should always be sent to voicemail. Not always * present. *

    Type: INTEGER (0 for false, 1 for true)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SEND_TO_VOICEMAIL = "send_to_voicemail"; /** * Is the contact starred? *

    Type: INTEGER (boolean)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String STARRED = "starred"; /** * The server version of the photo *

    Type: TEXT (the version number portion of the photo URI)

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String PHOTO_VERSION = "photo_version"; + @Deprecated + public static final String PHOTO_VERSION = "photo_version"; } /** * This table contains people. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class People implements BaseColumns, SyncConstValue, PeopleColumns, PhonesColumns, PresenceColumns { /** * no public constructor since this is a utility class + * @deprecated see {@link android.provider.ContactsContract} */ private People() {} /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/people"); /** * The content:// style URL for filtering people by name. The filter * argument should be passed as an additional path segment after this URI. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_FILTER_URI = Uri.parse("content://contacts/people/filter"); /** * The content:// style URL for the table that holds the deleted * contacts. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri DELETED_CONTENT_URI = Uri.parse("content://contacts/deleted_people"); @@ -278,49 +378,63 @@ public class Contacts { * additional path segment after this URI. This matches any people with * at least one E-mail or IM {@link ContactMethods} that match the * filter. - * + * * Not exposed because we expect significant changes in the contacts * schema and do not want to have to support this. * @hide + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri WITH_EMAIL_OR_IM_FILTER_URI = Uri.parse("content://contacts/people/with_email_or_im_filter"); - + /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = People.NAME + " ASC"; /** * The ID of the persons preferred phone number. *

    Type: INTEGER (foreign key to phones table on the _ID field)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PRIMARY_PHONE_ID = "primary_phone"; /** * The ID of the persons preferred email. *

    Type: INTEGER (foreign key to contact_methods table on the * _ID field)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PRIMARY_EMAIL_ID = "primary_email"; /** * The ID of the persons preferred organization. *

    Type: INTEGER (foreign key to organizations table on the * _ID field)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PRIMARY_ORGANIZATION_ID = "primary_organization"; /** @@ -328,7 +442,9 @@ public class Contacts { * * @param resolver the ContentResolver to use * @param personId the person who was contacted + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static void markAsContacted(ContentResolver resolver, long personId) { Uri uri = ContentUris.withAppendedId(CONTENT_URI, personId); uri = Uri.withAppendedPath(uri, "update_contact_time"); @@ -341,7 +457,9 @@ public class Contacts { /** * @hide Used in vCard parser code. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static long tryGetMyContactsGroupId(ContentResolver resolver) { Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION, Groups.SYSTEM_ID + "='" + Groups.GROUP_MY_CONTACTS + "'", null, null); @@ -364,25 +482,29 @@ public class Contacts { * @param personId the person to add to the group * @return the URI of the group membership row * @throws IllegalStateException if the My Contacts group can't be found + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static Uri addToMyContactsGroup(ContentResolver resolver, long personId) { long groupId = tryGetMyContactsGroupId(resolver); if (groupId == 0) { throw new IllegalStateException("Failed to find the My Contacts group"); } - + return addToGroup(resolver, personId, groupId); } /** * Adds a person to a group referred to by name. - * + * * @param resolver the resolver to use * @param personId the person to add to the group * @param groupName the name of the group to add the contact to * @return the URI of the group membership row * @throws IllegalStateException if the group can't be found + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static Uri addToGroup(ContentResolver resolver, long personId, String groupName) { long groupId = 0; Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION, @@ -400,36 +522,40 @@ public class Contacts { if (groupId == 0) { throw new IllegalStateException("Failed to find the My Contacts group"); } - + return addToGroup(resolver, personId, groupId); } /** * Adds a person to a group. - * + * * @param resolver the resolver to use * @param personId the person to add to the group * @param groupId the group to add the person to * @return the URI of the group membership row + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static Uri addToGroup(ContentResolver resolver, long personId, long groupId) { ContentValues values = new ContentValues(); values.put(GroupMembership.PERSON_ID, personId); values.put(GroupMembership.GROUP_ID, groupId); return resolver.insert(GroupMembership.CONTENT_URI, values); } - + private static final String[] GROUPS_PROJECTION = new String[] { Groups._ID, }; /** * Creates a new contacts and adds it to the "My Contacts" group. - * + * * @param resolver the ContentResolver to use * @param values the values to use when creating the contact * @return the URI of the contact, or null if the operation fails + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static Uri createPersonInMyContactsGroup(ContentResolver resolver, ContentValues values) { @@ -446,6 +572,10 @@ public class Contacts { return contactUri; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static Cursor queryGroups(ContentResolver resolver, long person) { return resolver.query(GroupMembership.CONTENT_URI, null, "person=?", new String[]{String.valueOf(person)}, Groups.DEFAULT_SORT_ORDER); @@ -456,24 +586,28 @@ public class Contacts { * @param cr the ContentResolver to use * @param person the Uri of the person whose photo is to be updated * @param data the byte[] that represents the photo + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static void setPhotoData(ContentResolver cr, Uri person, byte[] data) { Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY); ContentValues values = new ContentValues(); values.put(Photos.DATA, data); cr.update(photoUri, values, null, null); } - + /** * Opens an InputStream for the person's photo and returns the photo as a Bitmap. * If the person's photo isn't present returns the placeholderImageResource instead. * @param person the person whose photo should be used + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri person) { Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY); Cursor cursor = cr.query(photoUri, new String[]{Photos.DATA}, null, null, null); try { - if (!cursor.moveToNext()) { + if (cursor == null || !cursor.moveToNext()) { return null; } byte[] data = cursor.getBlob(0); @@ -482,7 +616,7 @@ public class Contacts { } return new ByteArrayInputStream(data); } finally { - cursor.close(); + if (cursor != null) cursor.close(); } } @@ -494,7 +628,9 @@ public class Contacts { * @param placeholderImageResource the image resource to use if the person doesn't * have a photo * @param options the decoding options, can be set to null + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static Bitmap loadContactPhoto(Context context, Uri person, int placeholderImageResource, BitmapFactory.Options options) { if (person == null) { @@ -520,7 +656,9 @@ public class Contacts { /** * A sub directory of a single person that contains all of their Phones. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Phones implements BaseColumns, PhonesColumns, PeopleColumns { /** @@ -530,19 +668,25 @@ public class Contacts { /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "phones"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "number ASC"; } /** * A subdirectory of a single person that contains all of their * ContactMethods. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class ContactMethods implements BaseColumns, ContactMethodsColumns, PeopleColumns { /** @@ -552,75 +696,100 @@ public class Contacts { /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "contact_methods"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "data ASC"; } /** * The extensions for a person + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static class Extensions implements BaseColumns, ExtensionsColumns { /** * no public constructor since this is a utility class + * @deprecated see {@link android.provider.ContactsContract} */ private Extensions() {} /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "extensions"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "name ASC"; /** * The ID of the person this phone number is assigned to. *

    Type: INTEGER (long)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; } } /** * Columns from the groups table. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface GroupsColumns { /** * The group name. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NAME = "name"; /** * Notes about the group. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NOTES = "notes"; /** * Whether this group should be synced if the SYNC_EVERYTHING settings is false * for this group's account. *

    Type: INTEGER (boolean)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SHOULD_SYNC = "should_sync"; /** * The ID of this group if it is a System Group, null otherwise. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SYSTEM_ID = "system_id"; } /** * This table contains the groups for an account. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Groups implements BaseColumns, SyncConstValue, GroupsColumns { /** @@ -630,86 +799,143 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/groups"); /** * The content:// style URL for the table that holds the deleted * groups. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri DELETED_CONTENT_URI = Uri.parse("content://contacts/deleted_groups"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * groups. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroup"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * group. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contactsgroup"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = NAME + " ASC"; /** - * + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String GROUP_ANDROID_STARRED = "Starred in Android"; /** * The "My Contacts" system group. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String GROUP_MY_CONTACTS = "Contacts"; } /** * Columns from the Phones table that other columns join into themselves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface PhonesColumns { /** * The type of the the phone number. *

    Type: INTEGER (one of the constants below)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TYPE = "type"; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_CUSTOM = 0; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_HOME = 1; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_MOBILE = 2; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_WORK = 3; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_FAX_WORK = 4; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_FAX_HOME = 5; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_PAGER = 6; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_OTHER = 7; /** * The user provided label for the phone number, only used if TYPE is TYPE_CUSTOM. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LABEL = "label"; /** * The phone number as the user entered it. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NUMBER = "number"; /** * The normalized phone number *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NUMBER_KEY = "number_key"; /** * Whether this is the primary phone number *

    Type: INTEGER (if set, non-0 means true)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String ISPRIMARY = "isprimary"; } @@ -717,7 +943,9 @@ public class Contacts { * This table stores phone numbers and a reference to the person that the * contact method belongs to. Phone numbers are stored separately from * other contact methods to make caller ID lookup more efficient. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Phones implements BaseColumns, PhonesColumns, PeopleColumns { /** @@ -725,12 +953,16 @@ public class Contacts { */ private Phones() {} + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final CharSequence getDisplayLabel(Context context, int type, CharSequence label, CharSequence[] labelArray) { CharSequence display = ""; if (type != People.Phones.TYPE_CUSTOM) { - CharSequence[] labels = labelArray != null? labelArray + CharSequence[] labels = labelArray != null? labelArray : context.getResources().getTextArray( com.android.internal.R.array.phoneTypes); try { @@ -746,47 +978,67 @@ public class Contacts { return display; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final CharSequence getDisplayLabel(Context context, int type, CharSequence label) { return getDisplayLabel(context, type, label, null); } - + /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/phones"); /** * The content:// style URL for filtering phone numbers + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_FILTER_URL = Uri.parse("content://contacts/phones/filter"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * phones. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * phone. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "name ASC"; /** * The ID of the person this phone number is assigned to. *

    Type: INTEGER (long)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final class GroupMembership implements BaseColumns, GroupsColumns { /** * no public constructor since this is a utility class @@ -795,137 +1047,206 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/groupmembership"); /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri RAW_CONTENT_URI = Uri.parse("content://contacts/groupmembershipraw"); /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "groupmembership"; + /** * The MIME type of {@link #CONTENT_URI} providing a directory of all * person groups. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroupmembership"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person group. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contactsgroupmembership"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "group_id ASC"; /** * The row id of the accounts group. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String GROUP_ID = "group_id"; /** * The sync id of the group. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String GROUP_SYNC_ID = "group_sync_id"; /** * The account of the group. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String GROUP_SYNC_ACCOUNT = "group_sync_account"; + /** + * The account type of the group. + *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public static final String GROUP_SYNC_ACCOUNT_TYPE = "group_sync_account_type"; + /** * The row id of the person. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; } /** * Columns from the ContactMethods table that other tables join into * themseleves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface ContactMethodsColumns { /** * The kind of the the contact method. For example, email address, * postal address, etc. *

    Type: INTEGER (one of the values below)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String KIND = "kind"; /** * The type of the contact method, must be one of the types below. *

    Type: INTEGER (one of the values below)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TYPE = "type"; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_CUSTOM = 0; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_HOME = 1; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_WORK = 2; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_OTHER = 3; /** * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final int MOBILE_EMAIL_TYPE_INDEX = 2; /** * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future. * This is not "mobile" but "CELL" since vCard uses it for identifying mobile phone. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String MOBILE_EMAIL_TYPE_NAME = "_AUTO_CELL"; /** * The user defined label for the the contact method. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LABEL = "label"; /** * The data for the contact method. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DATA = "data"; /** * Auxiliary data for the contact method. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String AUX_DATA = "aux_data"; /** * Whether this is the primary organization *

    Type: INTEGER (if set, non-0 means true)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String ISPRIMARY = "isprimary"; } /** * This table stores all non-phone contact methods and a reference to the * person that the contact method belongs to. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class ContactMethods implements BaseColumns, ContactMethodsColumns, PeopleColumns { /** * The column with latitude data for postal locations *

    Type: REAL

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String POSTAL_LOCATION_LATITUDE = DATA; /** * The column with longitude data for postal locations *

    Type: REAL

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String POSTAL_LOCATION_LONGITUDE = AUX_DATA; /** @@ -935,24 +1256,66 @@ public class Contacts { * - null * - pre: * - custom: + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final int PROTOCOL_AIM = 0; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_MSN = 1; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_YAHOO = 2; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_SKYPE = 3; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_QQ = 4; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_GOOGLE_TALK = 5; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_ICQ = 6; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int PROTOCOL_JABBER = 7; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static String encodePredefinedImProtocol(int protocol) { return "pre:" + protocol; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static String encodeCustomImProtocol(String protocolString) { return "custom:" + protocolString; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static Object decodeImProtocol(String encodedString) { if (encodedString == null) { return null; @@ -969,7 +1332,7 @@ public class Contacts { throw new IllegalArgumentException( "the value is not a valid encoded protocol, " + encodedString); } - + /** * This looks up the provider name defined in * {@link android.provider.Im.ProviderNames} from the predefined IM protocol id. @@ -978,8 +1341,10 @@ public class Contacts { * @param protocol the protocol ID * @return the provider name the IM app uses for the given protocol, or null if no * provider is defined for the given protocol + * @deprecated see {@link android.provider.ContactsContract} * @hide */ + @Deprecated public static String lookupProviderNameFromId(int protocol) { switch (protocol) { case PROTOCOL_GOOGLE_TALK: @@ -1007,6 +1372,10 @@ public class Contacts { */ private ContactMethods() {} + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final CharSequence getDisplayLabel(Context context, int kind, int type, CharSequence label) { CharSequence display = ""; @@ -1022,13 +1391,7 @@ public class Contacts { } } else { if (!TextUtils.isEmpty(label)) { - if (label.toString().equals(MOBILE_EMAIL_TYPE_NAME)) { - display = - context.getString( - com.android.internal.R.string.mobileEmailTypeName); - } else { - display = label; - } + display = label; } } break; @@ -1064,7 +1427,9 @@ public class Contacts { * @param postalId the address to update * @param latitude the latitude for the address * @param longitude the longitude for the address + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public void addPostalLocation(Context context, long postalId, double latitude, double longitude) { final ContentResolver resolver = context.getContentResolver(); @@ -1083,130 +1448,166 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/contact_methods"); /** * The content:// style URL for sub-directory of e-mail addresses. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_EMAIL_URI = Uri.parse("content://contacts/contact_methods/email"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of + * @deprecated see {@link android.provider.ContactsContract} * phones. */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact-methods"; /** - * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\ + * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of * multiple {@link Contacts#KIND_EMAIL} entries. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_EMAIL_TYPE = "vnd.android.cursor.dir/email"; /** - * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\ + * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of * multiple {@link Contacts#KIND_POSTAL} entries. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_POSTAL_TYPE = "vnd.android.cursor.dir/postal-address"; /** * The MIME type of a {@link #CONTENT_URI} sub-directory of a single * {@link Contacts#KIND_EMAIL} entry. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_EMAIL_ITEM_TYPE = "vnd.android.cursor.item/email"; /** * The MIME type of a {@link #CONTENT_URI} sub-directory of a single * {@link Contacts#KIND_POSTAL} entry. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_POSTAL_ITEM_TYPE = "vnd.android.cursor.item/postal-address"; /** * The MIME type of a {@link #CONTENT_URI} sub-directory of a single * {@link Contacts#KIND_IM} entry. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_IM_ITEM_TYPE = "vnd.android.cursor.item/jabber-im"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "name ASC"; /** * The ID of the person this contact method is assigned to. *

    Type: INTEGER (long)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; } /** * The IM presence columns with some contacts specific columns mixed in. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface PresenceColumns extends Im.CommonPresenceColumns { /** * The IM service the presence is coming from. Formatted using either * {@link Contacts.ContactMethods#encodePredefinedImProtocol} or * {@link Contacts.ContactMethods#encodeCustomImProtocol}. *

    Type: STRING

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String IM_PROTOCOL = "im_protocol"; /** * The IM handle the presence item is for. The handle is scoped to * the {@link #IM_PROTOCOL}. *

    Type: STRING

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String IM_HANDLE = "im_handle"; /** * The IM account for the local user that the presence data came from. *

    Type: STRING

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String IM_ACCOUNT = "im_account"; } /** * Contains presence information about contacts. * @hide + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Presence implements BaseColumns, PresenceColumns, PeopleColumns { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/presence"); /** * The ID of the person this presence item is assigned to. *

    Type: INTEGER (long)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; /** * Gets the resource ID for the proper presence icon. - * + * * @param status the status to get the icon for * @return the resource ID for the proper presence icon + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final int getPresenceIconResourceId(int status) { switch (status) { case Contacts.People.AVAILABLE: return com.android.internal.R.drawable.presence_online; - + case Contacts.People.IDLE: case Contacts.People.AWAY: return com.android.internal.R.drawable.presence_away; - + case Contacts.People.DO_NOT_DISTURB: return com.android.internal.R.drawable.presence_busy; - + case Contacts.People.INVISIBLE: return com.android.internal.R.drawable.presence_invisible; - + case Contacts.People.OFFLINE: default: return com.android.internal.R.drawable.presence_offline; @@ -1218,7 +1619,9 @@ public class Contacts { * * @param icon the icon to to set * @param serverStatus that status + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final void setPresenceIcon(ImageView icon, int serverStatus) { icon.setImageResource(getPresenceIconResourceId(serverStatus)); } @@ -1226,58 +1629,90 @@ public class Contacts { /** * Columns from the Organizations table that other columns join into themselves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface OrganizationColumns { /** - * The type of the the phone number. + * The type of the organizations. *

    Type: INTEGER (one of the constants below)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TYPE = "type"; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_CUSTOM = 0; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_WORK = 1; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final int TYPE_OTHER = 2; /** * The user provided label, only used if TYPE is TYPE_CUSTOM. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LABEL = "label"; /** * The name of the company for this organization. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String COMPANY = "company"; /** * The title within this organization. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TITLE = "title"; /** * The person this organization is tied to. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; /** * Whether this is the primary organization *

    Type: INTEGER (if set, non-0 means true)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String ISPRIMARY = "isprimary"; } /** * A sub directory of a single person that contains all of their Phones. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Organizations implements BaseColumns, OrganizationColumns { /** * no public constructor since this is a utility class */ private Organizations() {} + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public static final CharSequence getDisplayLabel(Context context, int type, CharSequence label) { CharSequence display = ""; @@ -1300,68 +1735,90 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/organizations"); /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "organizations"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "company, title, isprimary ASC"; } /** * Columns from the Photos table that other columns join into themselves. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public interface PhotosColumns { /** * The _SYNC_VERSION of the photo that was last downloaded *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LOCAL_VERSION = "local_version"; /** * The person this photo is associated with. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; /** * non-zero if a download is required and the photo isn't marked as a bad resource. * You must specify this in the columns in order to use it in the where clause. *

    Type: INTEGER(boolean)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DOWNLOAD_REQUIRED = "download_required"; /** * non-zero if this photo is known to exist on the server *

    Type: INTEGER(boolean)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String EXISTS_ON_SERVER = "exists_on_server"; /** * Contains the description of the upload or download error from * the previous attempt. If null then the previous attempt succeeded. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SYNC_ERROR = "sync_error"; /** * The image data, or null if there is no image. *

    Type: BLOB

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DATA = "data"; } /** * The photos over all of the people + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Photos implements BaseColumns, PhotosColumns, SyncConstValue { /** * no public constructor since this is a utility class @@ -1370,38 +1827,53 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ - public static final Uri CONTENT_URI = - Uri.parse("content://contacts/photos"); + @Deprecated + public static final Uri CONTENT_URI = Uri.parse("content://contacts/photos"); /** * The directory twig for this sub-table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_DIRECTORY = "photo"; /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "person ASC"; } + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated public interface ExtensionsColumns { /** * The name of this extension. May not be null. There may be at most one row for each name. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String NAME = "name"; /** * The value of this extension. May not be null. *

    Type: TEXT

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String VALUE = "value"; } /** * The extensions for a person + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Extensions implements BaseColumns, ExtensionsColumns { /** * no public constructor since this is a utility class @@ -1410,64 +1882,91 @@ public class Contacts { /** * The content:// style URL for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://contacts/extensions"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * phones. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_extensions"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * phone. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_extensions"; + /** * The default sort order for this table + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String DEFAULT_SORT_ORDER = "person, name ASC"; /** * The ID of the person this phone number is assigned to. *

    Type: INTEGER (long)

    + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String PERSON_ID = "person"; } /** * Contains helper classes used to create or manage {@link android.content.Intent Intents} * that involve contacts. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Intents { + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public Intents() { + } + /** * This is the intent that is fired when a search suggestion is clicked on. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SEARCH_SUGGESTION_CLICKED = - "android.provider.Contacts.SEARCH_SUGGESTION_CLICKED"; + ContactsContract.Intents.SEARCH_SUGGESTION_CLICKED; /** - * This is the intent that is fired when a search suggestion for dialing a number + * This is the intent that is fired when a search suggestion for dialing a number * is clicked on. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED = - "android.provider.Contacts.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED"; + ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED; /** * This is the intent that is fired when a search suggestion for creating a contact * is clicked on. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED = - "android.provider.Contacts.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED"; + ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED; /** * Starts an Activity that lets the user pick a contact to attach an image to. * After picking the contact it launches the image cropper in face detection mode. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String ATTACH_IMAGE = - "com.android.contacts.action.ATTACH_IMAGE"; + @Deprecated + public static final String ATTACH_IMAGE = ContactsContract.Intents.ATTACH_IMAGE; /** * Takes as input a data URI with a mailto: or tel: scheme. If a single @@ -1491,9 +1990,11 @@ public class Contacts { *

    * Passing true for the {@link #EXTRA_FORCE_CREATE} extra will skip * prompting the user when the contact doesn't exist. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String SHOW_OR_CREATE_CONTACT = - "com.android.contacts.action.SHOW_OR_CREATE_CONTACT"; + ContactsContract.Intents.SHOW_OR_CREATE_CONTACT; /** * Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new @@ -1501,80 +2002,120 @@ public class Contacts { * to prompt user with dialog before creating. *

    * Type: BOOLEAN + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String EXTRA_FORCE_CREATE = - "com.android.contacts.action.FORCE_CREATE"; - + @Deprecated + public static final String EXTRA_FORCE_CREATE = ContactsContract.Intents.EXTRA_FORCE_CREATE; + /** * Used with {@link #SHOW_OR_CREATE_CONTACT} to specify an exact * description to be shown when prompting user about creating a new * contact. *

    * Type: STRING + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String EXTRA_CREATE_DESCRIPTION = - "com.android.contacts.action.CREATE_DESCRIPTION"; + ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION; + + /** + * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a + * dialog location using screen coordinates. When not specified, the + * dialog will be centered. + * + * @hide pending API council review + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public static final String EXTRA_TARGET_RECT = ContactsContract.Intents.EXTRA_TARGET_RECT; /** * Intents related to the Contacts app UI. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class UI { + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public UI() { + } + /** * The action for the default contacts list tab. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String LIST_DEFAULT = - "com.android.contacts.action.LIST_DEFAULT"; + @Deprecated + public static final String LIST_DEFAULT = ContactsContract.Intents.UI.LIST_DEFAULT; /** * The action for the contacts list tab. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_GROUP_ACTION = - "com.android.contacts.action.LIST_GROUP"; + ContactsContract.Intents.UI.LIST_GROUP_ACTION; /** * When in LIST_GROUP_ACTION mode, this is the group to display. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP"; - + @Deprecated + public static final String GROUP_NAME_EXTRA_KEY = + ContactsContract.Intents.UI.GROUP_NAME_EXTRA_KEY; /** * The action for the all contacts list tab. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_ALL_CONTACTS_ACTION = - "com.android.contacts.action.LIST_ALL_CONTACTS"; + ContactsContract.Intents.UI.LIST_ALL_CONTACTS_ACTION; /** * The action for the contacts with phone numbers list tab. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_CONTACTS_WITH_PHONES_ACTION = - "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES"; + ContactsContract.Intents.UI.LIST_CONTACTS_WITH_PHONES_ACTION; /** * The action for the starred contacts list tab. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_STARRED_ACTION = - "com.android.contacts.action.LIST_STARRED"; + ContactsContract.Intents.UI.LIST_STARRED_ACTION; /** * The action for the frequent contacts list tab. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_FREQUENT_ACTION = - "com.android.contacts.action.LIST_FREQUENT"; + ContactsContract.Intents.UI.LIST_FREQUENT_ACTION; /** * The action for the "strequent" contacts list tab. It first lists the starred * contacts in alphabetical order and then the frequent contacts in descending * order of the number of times they have been contacted. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String LIST_STREQUENT_ACTION = - "com.android.contacts.action.LIST_STREQUENT"; + ContactsContract.Intents.UI.LIST_STREQUENT_ACTION; /** * A key for to be used as an intent extra to set the activity * title to a custom String value. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final String TITLE_EXTRA_KEY = - "com.android.contacts.extra.TITLE_EXTRA"; - + ContactsContract.Intents.UI.TITLE_EXTRA_KEY; + /** * Activity Action: Display a filtered list of contacts *

    @@ -1582,188 +2123,267 @@ public class Contacts { * filtering *

    * Output: Nothing. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String FILTER_CONTACTS_ACTION = - "com.android.contacts.action.FILTER_CONTACTS"; - + @Deprecated + public static final String FILTER_CONTACTS_ACTION = + ContactsContract.Intents.UI.FILTER_CONTACTS_ACTION; + /** * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION} * intents to supply the text on which to filter. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String FILTER_TEXT_EXTRA_KEY = - "com.android.contacts.extra.FILTER_TEXT"; + @Deprecated + public static final String FILTER_TEXT_EXTRA_KEY = + ContactsContract.Intents.UI.FILTER_TEXT_EXTRA_KEY; } /** * Convenience class that contains string constants used * to create contact {@link android.content.Intent Intents}. + * @deprecated see {@link android.provider.ContactsContract} */ + @Deprecated public static final class Insert { - /** The action code to use when adding a contact */ - public static final String ACTION = Intent.ACTION_INSERT; + /** + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public Insert() { + } + + /** The action code to use when adding a contact + * @deprecated see {@link android.provider.ContactsContract} + */ + @Deprecated + public static final String ACTION = ContactsContract.Intents.Insert.ACTION; /** * If present, forces a bypass of quick insert mode. + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String FULL_MODE = "full_mode"; + @Deprecated + public static final String FULL_MODE = ContactsContract.Intents.Insert.FULL_MODE; /** * The extra field for the contact name. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String NAME = "name"; + @Deprecated + public static final String NAME = ContactsContract.Intents.Insert.NAME; /** * The extra field for the contact phonetic name. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String PHONETIC_NAME = "phonetic_name"; + @Deprecated + public static final String PHONETIC_NAME = + ContactsContract.Intents.Insert.PHONETIC_NAME; /** * The extra field for the contact company. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String COMPANY = "company"; + @Deprecated + public static final String COMPANY = ContactsContract.Intents.Insert.COMPANY; /** * The extra field for the contact job title. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String JOB_TITLE = "job_title"; + @Deprecated + public static final String JOB_TITLE = ContactsContract.Intents.Insert.JOB_TITLE; /** * The extra field for the contact notes. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String NOTES = "notes"; + @Deprecated + public static final String NOTES = ContactsContract.Intents.Insert.NOTES; /** * The extra field for the contact phone number. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String PHONE = "phone"; + @Deprecated + public static final String PHONE = ContactsContract.Intents.Insert.PHONE; /** * The extra field for the contact phone number type. *

    Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String PHONE_TYPE = "phone_type"; + @Deprecated + public static final String PHONE_TYPE = ContactsContract.Intents.Insert.PHONE_TYPE; /** * The extra field for the phone isprimary flag. *

    Type: boolean

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String PHONE_ISPRIMARY = "phone_isprimary"; + @Deprecated + public static final String PHONE_ISPRIMARY = + ContactsContract.Intents.Insert.PHONE_ISPRIMARY; /** * The extra field for an optional second contact phone number. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String SECONDARY_PHONE = "secondary_phone"; + @Deprecated + public static final String SECONDARY_PHONE = + ContactsContract.Intents.Insert.SECONDARY_PHONE; /** * The extra field for an optional second contact phone number type. *

    Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String SECONDARY_PHONE_TYPE = "secondary_phone_type"; + @Deprecated + public static final String SECONDARY_PHONE_TYPE = + ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE; /** * The extra field for an optional third contact phone number. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String TERTIARY_PHONE = "tertiary_phone"; + @Deprecated + public static final String TERTIARY_PHONE = + ContactsContract.Intents.Insert.TERTIARY_PHONE; /** * The extra field for an optional third contact phone number type. *

    Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns}, * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String TERTIARY_PHONE_TYPE = "tertiary_phone_type"; + @Deprecated + public static final String TERTIARY_PHONE_TYPE = + ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE; /** * The extra field for the contact email address. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String EMAIL = "email"; + @Deprecated + public static final String EMAIL = ContactsContract.Intents.Insert.EMAIL; /** * The extra field for the contact email type. *

    Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String EMAIL_TYPE = "email_type"; + @Deprecated + public static final String EMAIL_TYPE = ContactsContract.Intents.Insert.EMAIL_TYPE; /** * The extra field for the email isprimary flag. *

    Type: boolean

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String EMAIL_ISPRIMARY = "email_isprimary"; + @Deprecated + public static final String EMAIL_ISPRIMARY = + ContactsContract.Intents.Insert.EMAIL_ISPRIMARY; /** * The extra field for an optional second contact email address. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String SECONDARY_EMAIL = "secondary_email"; + @Deprecated + public static final String SECONDARY_EMAIL = + ContactsContract.Intents.Insert.SECONDARY_EMAIL; /** * The extra field for an optional second contact email type. *

    Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String SECONDARY_EMAIL_TYPE = "secondary_email_type"; + @Deprecated + public static final String SECONDARY_EMAIL_TYPE = + ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE; /** * The extra field for an optional third contact email address. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String TERTIARY_EMAIL = "tertiary_email"; + @Deprecated + public static final String TERTIARY_EMAIL = + ContactsContract.Intents.Insert.TERTIARY_EMAIL; /** * The extra field for an optional third contact email type. *

    Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String TERTIARY_EMAIL_TYPE = "tertiary_email_type"; + @Deprecated + public static final String TERTIARY_EMAIL_TYPE = + ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE; /** * The extra field for the contact postal address. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String POSTAL = "postal"; + @Deprecated + public static final String POSTAL = ContactsContract.Intents.Insert.POSTAL; /** * The extra field for the contact postal address type. *

    Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} * or a string specifying a custom label.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String POSTAL_TYPE = "postal_type"; + @Deprecated + public static final String POSTAL_TYPE = ContactsContract.Intents.Insert.POSTAL_TYPE; /** * The extra field for the postal isprimary flag. *

    Type: boolean

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String POSTAL_ISPRIMARY = "postal_isprimary"; + @Deprecated + public static final String POSTAL_ISPRIMARY = ContactsContract.Intents.Insert.POSTAL_ISPRIMARY; /** * The extra field for an IM handle. *

    Type: String

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String IM_HANDLE = "im_handle"; + @Deprecated + public static final String IM_HANDLE = ContactsContract.Intents.Insert.IM_HANDLE; /** * The extra field for the IM protocol *

    Type: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} * or {@link Contacts.ContactMethods#encodeCustomImProtocol}.

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String IM_PROTOCOL = "im_protocol"; + @Deprecated + public static final String IM_PROTOCOL = ContactsContract.Intents.Insert.IM_PROTOCOL; /** * The extra field for the IM isprimary flag. *

    Type: boolean

    + * @deprecated see {@link android.provider.ContactsContract} */ - public static final String IM_ISPRIMARY = "im_isprimary"; + @Deprecated + public static final String IM_ISPRIMARY = ContactsContract.Intents.Insert.IM_ISPRIMARY; } } } diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java new file mode 100644 index 0000000000000000000000000000000000000000..79044018fcf81fa4021080ff393534de148264b5 --- /dev/null +++ b/core/java/android/provider/ContactsContract.java @@ -0,0 +1,2722 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.provider; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Rect; +import android.net.Uri; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Pair; +import android.view.View; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * The contract between the contacts provider and applications. Contains definitions + * for the supported URIs and columns. These APIs supersede {@link Contacts}. + */ +@SuppressWarnings("unused") +public final class ContactsContract { + /** The authority for the contacts provider */ + public static final String AUTHORITY = "com.android.contacts"; + /** A content:// style uri to the authority for the contacts provider */ + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + /** + * An optional insert, update or delete URI parameter that allows the caller + * to specify that it is a sync adapter. The default value is false. If true + * the dirty flag is not automatically set and the "syncToNetwork" parameter + * is set to false when calling + * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + */ + public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + + /** + * A query parameter key used to specify the package that is requesting a query. + * This is used for restricting data based on package name. + * + * @hide + */ + public static final String REQUESTING_PACKAGE_PARAM_KEY = "requesting_package"; + + /** + * @hide should be removed when users are updated to refer to SyncState + * @deprecated use SyncState instead + */ + @Deprecated + public interface SyncStateColumns extends SyncStateContract.Columns { + } + + /** + * A table provided for sync adapters to use for storing private sync state data. + * + * @see SyncStateContract + */ + public static final class SyncState implements SyncStateContract.Columns { + /** + * This utility class cannot be instantiated + */ + private SyncState() {} + + public static final String CONTENT_DIRECTORY = + SyncStateContract.Constants.CONTENT_DIRECTORY; + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, CONTENT_DIRECTORY); + + /** + * @see android.provider.SyncStateContract.Helpers#get + */ + public static byte[] get(ContentProviderClient provider, Account account) + throws RemoteException { + return SyncStateContract.Helpers.get(provider, CONTENT_URI, account); + } + + /** + * @see android.provider.SyncStateContract.Helpers#get + */ + public static Pair getWithUri(ContentProviderClient provider, Account account) + throws RemoteException { + return SyncStateContract.Helpers.getWithUri(provider, CONTENT_URI, account); + } + + /** + * @see android.provider.SyncStateContract.Helpers#set + */ + public static void set(ContentProviderClient provider, Account account, byte[] data) + throws RemoteException { + SyncStateContract.Helpers.set(provider, CONTENT_URI, account, data); + } + + /** + * @see android.provider.SyncStateContract.Helpers#newSetOperation + */ + public static ContentProviderOperation newSetOperation(Account account, byte[] data) { + return SyncStateContract.Helpers.newSetOperation(CONTENT_URI, account, data); + } + } + + /** + * Generic columns for use by sync adapters. The specific functions of + * these columns are private to the sync adapter. Other clients of the API + * should not attempt to either read or write this column. + */ + protected interface BaseSyncColumns { + + /** Generic column for use by sync adapters. */ + public static final String SYNC1 = "sync1"; + /** Generic column for use by sync adapters. */ + public static final String SYNC2 = "sync2"; + /** Generic column for use by sync adapters. */ + public static final String SYNC3 = "sync3"; + /** Generic column for use by sync adapters. */ + public static final String SYNC4 = "sync4"; + } + + /** + * Columns that appear when each row of a table belongs to a specific + * account, including sync information that an account may need. + */ + protected interface SyncColumns extends BaseSyncColumns { + /** + * The name of the account instance to which this row belongs, which when paired with + * {@link #ACCOUNT_TYPE} identifies a specific account. + *

    Type: TEXT

    + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * The type of account to which this row belongs, which when paired with + * {@link #ACCOUNT_NAME} identifies a specific account. + *

    Type: TEXT

    + */ + public static final String ACCOUNT_TYPE = "account_type"; + + /** + * String that uniquely identifies this row to its source account. + *

    Type: TEXT

    + */ + public static final String SOURCE_ID = "sourceid"; + + /** + * Version number that is updated whenever this row or its related data + * changes. + *

    Type: INTEGER

    + */ + public static final String VERSION = "version"; + + /** + * Flag indicating that {@link #VERSION} has changed, and this row needs + * to be synchronized by its owning account. + *

    Type: INTEGER (boolean)

    + */ + public static final String DIRTY = "dirty"; + } + + protected interface ContactOptionsColumns { + /** + * The number of times a contact has been contacted + *

    Type: INTEGER

    + */ + public static final String TIMES_CONTACTED = "times_contacted"; + + /** + * The last time a contact was contacted. + *

    Type: INTEGER

    + */ + public static final String LAST_TIME_CONTACTED = "last_time_contacted"; + + /** + * Is the contact starred? + *

    Type: INTEGER (boolean)

    + */ + public static final String STARRED = "starred"; + + /** + * A custom ringtone associated with a contact. Not always present. + *

    Type: TEXT (URI to the ringtone)

    + */ + public static final String CUSTOM_RINGTONE = "custom_ringtone"; + + /** + * Whether the contact should always be sent to voicemail. Not always + * present. + *

    Type: INTEGER (0 for false, 1 for true)

    + */ + public static final String SEND_TO_VOICEMAIL = "send_to_voicemail"; + } + + protected interface ContactsColumns { + /** + * The display name for the contact. + *

    Type: TEXT

    + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * Reference to the row in the data table holding the photo. + *

    Type: INTEGER REFERENCES data(_id)

    + */ + public static final String PHOTO_ID = "photo_id"; + + /** + * Lookup value that reflects the {@link Groups#GROUP_VISIBLE} state of + * any {@link CommonDataKinds.GroupMembership} for this contact. + */ + public static final String IN_VISIBLE_GROUP = "in_visible_group"; + + /** + * An indicator of whether this contact has at least one phone number. "1" if there is + * at least one phone number, "0" otherwise. + *

    Type: INTEGER

    + */ + public static final String HAS_PHONE_NUMBER = "has_phone_number"; + + /** + * An opaque value that contains hints on how to find the contact if + * its row id changed as a result of a sync or aggregation. + */ + public static final String LOOKUP_KEY = "lookup"; + } + + protected interface ContactStatusColumns { + /** + * Contact presence status. See {@link StatusUpdates} for individual status + * definitions. + *

    Type: NUMBER

    + */ + public static final String CONTACT_PRESENCE = "contact_presence"; + + /** + * Contact's latest status update. + *

    Type: TEXT

    + */ + public static final String CONTACT_STATUS = "contact_status"; + + /** + * The absolute time in milliseconds when the latest status was + * inserted/updated. + *

    Type: NUMBER

    + */ + public static final String CONTACT_STATUS_TIMESTAMP = "contact_status_ts"; + + /** + * The package containing resources for this status: label and icon. + *

    Type: NUMBER

    + */ + public static final String CONTACT_STATUS_RES_PACKAGE = "contact_status_res_package"; + + /** + * The resource ID of the label describing the source of contact + * status, e.g. "Google Talk". This resource is scoped by the + * {@link #CONTACT_STATUS_RES_PACKAGE}. + *

    Type: NUMBER

    + */ + public static final String CONTACT_STATUS_LABEL = "contact_status_label"; + + /** + * The resource ID of the icon for the source of contact status. This + * resource is scoped by the {@link #CONTACT_STATUS_RES_PACKAGE}. + *

    Type: NUMBER

    + */ + public static final String CONTACT_STATUS_ICON = "contact_status_icon"; + } + + /** + * Constants for the contacts table, which contains a record per group + * of raw contacts representing the same person. + */ + public static class Contacts implements BaseColumns, ContactsColumns, + ContactOptionsColumns, ContactStatusColumns { + /** + * This utility class cannot be instantiated + */ + private Contacts() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "contacts"); + + /** + * A content:// style URI for this table that should be used to create + * shortcuts or otherwise create long-term links to contacts. This URI + * should always be followed by a "/" and the contact's {@link #LOOKUP_KEY}. + * It can optionally also have a "/" and last known contact ID appended after + * that. This "complete" format is an important optimization and is highly recommended. + *

    + * As long as the contact's row ID remains the same, this URI is + * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes + * as a result of a sync or aggregation, this URI will look up the + * contact using indirect information (sync IDs or constituent raw + * contacts). + *

    + * Lookup key should be appended unencoded - it is stored in the encoded + * form, ready for use in a URI. + */ + public static final Uri CONTENT_LOOKUP_URI = Uri.withAppendedPath(CONTENT_URI, + "lookup"); + + /** + * Base {@link Uri} for referencing a single {@link Contacts} entry, + * created by appending {@link #LOOKUP_KEY} using + * {@link Uri#withAppendedPath(Uri, String)}. Provides + * {@link OpenableColumns} columns when queried, or returns the + * referenced contact formatted as a vCard when opened through + * {@link ContentResolver#openAssetFileDescriptor(Uri, String)}. + */ + public static final Uri CONTENT_VCARD_URI = Uri.withAppendedPath(CONTENT_URI, + "as_vcard"); + + /** + * Builds a {@link #CONTENT_LOOKUP_URI} style {@link Uri} describing the + * requested {@link Contacts} entry. + * + * @param contactUri A {@link #CONTENT_URI} row, or an existing + * {@link #CONTENT_LOOKUP_URI} to attempt refreshing. + */ + public static Uri getLookupUri(ContentResolver resolver, Uri contactUri) { + final Cursor c = resolver.query(contactUri, new String[] { + Contacts.LOOKUP_KEY, Contacts._ID + }, null, null, null); + if (c == null) { + return null; + } + + try { + if (c.moveToFirst()) { + final String lookupKey = c.getString(0); + final long contactId = c.getLong(1); + return getLookupUri(contactId, lookupKey); + } + } finally { + c.close(); + } + return null; + } + + /** + * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the + * given {@link android.provider.ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. + */ + public static Uri getLookupUri(long contactId, String lookupKey) { + return ContentUris.withAppendedId(Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, + lookupKey), contactId); + } + + /** + * Computes a content URI (see {@link #CONTENT_URI}) given a lookup URI. + *

    + * Returns null if the contact cannot be found. + */ + public static Uri lookupContact(ContentResolver resolver, Uri lookupUri) { + if (lookupUri == null) { + return null; + } + + Cursor c = resolver.query(lookupUri, new String[]{Contacts._ID}, null, null, null); + if (c == null) { + return null; + } + + try { + if (c.moveToFirst()) { + long contactId = c.getLong(0); + return ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); + } + } finally { + c.close(); + } + return null; + } + + /** + * Mark a contact as having been contacted. + * + * @param resolver the ContentResolver to use + * @param contactId the person who was contacted + */ + public static void markAsContacted(ContentResolver resolver, long contactId) { + Uri uri = ContentUris.withAppendedId(CONTENT_URI, contactId); + ContentValues values = new ContentValues(); + // TIMES_CONTACTED will be incremented when LAST_TIME_CONTACTED is modified. + values.put(LAST_TIME_CONTACTED, System.currentTimeMillis()); + resolver.update(uri, values, null, null); + } + + /** + * The content:// style URI used for "type-to-filter" functionality on the + * {@link #CONTENT_URI} URI. The filter string will be used to match + * various parts of the contact name. The filter argument should be passed + * as an additional path segment after this URI. + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath( + CONTENT_URI, "filter"); + + /** + * The content:// style URI for this table joined with useful data from + * {@link Data}, filtered to include only starred contacts + * and the most frequently contacted contacts. + */ + public static final Uri CONTENT_STREQUENT_URI = Uri.withAppendedPath( + CONTENT_URI, "strequent"); + + /** + * The content:// style URI used for "type-to-filter" functionality on the + * {@link #CONTENT_STREQUENT_URI} URI. The filter string will be used to match + * various parts of the contact name. The filter argument should be passed + * as an additional path segment after this URI. + */ + public static final Uri CONTENT_STREQUENT_FILTER_URI = Uri.withAppendedPath( + CONTENT_STREQUENT_URI, "filter"); + + public static final Uri CONTENT_GROUP_URI = Uri.withAppendedPath( + CONTENT_URI, "group"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_VCARD_TYPE = "text/x-vcard"; + + /** + * A sub-directory of a single contact that contains all of the constituent raw contact + * {@link Data} rows. + */ + public static final class Data implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Data() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "data"; + } + + /** + * A sub-directory of a single contact aggregate that contains all aggregation suggestions + * (other contacts). The aggregation suggestions are computed based on approximate + * data matches with this contact. + */ + public static final class AggregationSuggestions implements BaseColumns, ContactsColumns { + /** + * No public constructor since this is a utility class + */ + private AggregationSuggestions() {} + + /** + * The directory twig for this sub-table. The URI can be followed by an optional + * type-to-filter, similar to + * {@link android.provider.ContactsContract.Contacts#CONTENT_FILTER_URI}. + */ + public static final String CONTENT_DIRECTORY = "suggestions"; + } + + /** + * A sub-directory of a single contact that contains the contact's primary photo. + */ + public static final class Photo implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Photo() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "photo"; + } + + /** + * Opens an InputStream for the contacts's default photo and returns the + * photo as a byte stream. If there is not photo null will be returned. + * + * @param contactUri the contact whose photo should be used + * @return an InputStream of the photo, or null if no photo is present + */ + public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri contactUri) { + Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY); + if (photoUri == null) { + return null; + } + Cursor cursor = cr.query(photoUri, + new String[]{ContactsContract.CommonDataKinds.Photo.PHOTO}, null, null, null); + try { + if (cursor == null || !cursor.moveToNext()) { + return null; + } + byte[] data = cursor.getBlob(0); + if (data == null) { + return null; + } + return new ByteArrayInputStream(data); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + + protected interface RawContactsColumns { + /** + * A reference to the {@link android.provider.ContactsContract.Contacts#_ID} that this + * data belongs to. + *

    Type: INTEGER

    + */ + public static final String CONTACT_ID = "contact_id"; + + /** + * Flag indicating that this {@link RawContacts} entry and its children have + * been restricted to specific platform apps. + *

    Type: INTEGER (boolean)

    + * + * @hide until finalized in future platform release + */ + public static final String IS_RESTRICTED = "is_restricted"; + + /** + * The aggregation mode for this contact. + *

    Type: INTEGER

    + */ + public static final String AGGREGATION_MODE = "aggregation_mode"; + + /** + * The "deleted" flag: "0" by default, "1" if the row has been marked + * for deletion. When {@link android.content.ContentResolver#delete} is + * called on a raw contact, it is marked for deletion and removed from its + * aggregate contact. The sync adaptor deletes the raw contact on the server and + * then calls ContactResolver.delete once more, this time passing the + * {@link ContactsContract#CALLER_IS_SYNCADAPTER} query parameter to finalize + * the data removal. + *

    Type: INTEGER

    + */ + public static final String DELETED = "deleted"; + } + + /** + * Constants for the raw contacts table, which contains the base contact + * information per sync source. Sync adapters and contact management apps + * are the primary consumers of this API. + */ + public static final class RawContacts implements BaseColumns, RawContactsColumns, + ContactOptionsColumns, SyncColumns { + /** + * This utility class cannot be instantiated + */ + private RawContacts() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "raw_contacts"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/raw_contact"; + + /** + * Aggregation mode: aggregate asynchronously. + */ + public static final int AGGREGATION_MODE_DEFAULT = 0; + + /** + * Aggregation mode: aggregate at the time the raw contact is inserted/updated. + */ + public static final int AGGREGATION_MODE_IMMEDIATE = 1; + + /** + * If {@link #AGGREGATION_MODE} is {@link #AGGREGATION_MODE_SUSPENDED}, changes + * to the raw contact do not cause its aggregation to be revisited. Note that changing + * {@link #AGGREGATION_MODE} from {@link #AGGREGATION_MODE_SUSPENDED} to + * {@link #AGGREGATION_MODE_DEFAULT} does not trigger an aggregation pass. Any subsequent + * change to the raw contact's data will. + */ + public static final int AGGREGATION_MODE_SUSPENDED = 2; + + /** + * Aggregation mode: never aggregate this raw contact (note that the raw contact will not + * have a corresponding Aggregate and therefore will not be included in Aggregates + * query results.) + */ + public static final int AGGREGATION_MODE_DISABLED = 3; + + /** + * Build a {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI} + * style {@link Uri} for the parent {@link android.provider.ContactsContract.Contacts} + * entry of the given {@link RawContacts} entry. + */ + public static Uri getContactLookupUri(ContentResolver resolver, Uri rawContactUri) { + // TODO: use a lighter query by joining rawcontacts with contacts in provider + final Uri dataUri = Uri.withAppendedPath(rawContactUri, Data.CONTENT_DIRECTORY); + final Cursor cursor = resolver.query(dataUri, new String[] { + RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY + }, null, null, null); + + Uri lookupUri = null; + try { + if (cursor != null && cursor.moveToFirst()) { + final long contactId = cursor.getLong(0); + final String lookupKey = cursor.getString(1); + return Contacts.getLookupUri(contactId, lookupKey); + } + } finally { + if (cursor != null) cursor.close(); + } + return lookupUri; + } + + /** + * A sub-directory of a single raw contact that contains all of their {@link Data} rows. + * To access this directory append {@link Data#CONTENT_DIRECTORY} to the contact URI. + */ + public static final class Data implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Data() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "data"; + } + + /** + * A sub-directory of a single raw contact that contains all of their {@link Data} rows. + * To access this directory append {@link Entity#CONTENT_DIRECTORY} to the contact URI. + */ + public static final class Entity implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Entity() { + } + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "entity"; + + /** + * The ID of the data column. The value will be null if this raw contact has no + * data rows. + *

    Type: INTEGER

    + */ + public static final String DATA_ID = "data_id"; + } + } + + protected interface StatusColumns extends Im.CommonPresenceColumns { + /** + * Contact's latest presence level. + *

    Type: INTEGER (one of the values below)

    + */ + public static final String PRESENCE = PRESENCE_STATUS; + + /** + * Contact latest status update. + *

    Type: TEXT

    + */ + public static final String STATUS = PRESENCE_CUSTOM_STATUS; + + /** + * The absolute time in milliseconds when the latest status was inserted/updated. + *

    Type: NUMBER

    + */ + public static final String STATUS_TIMESTAMP = "status_ts"; + + /** + * The package containing resources for this status: label and icon. + *

    Type: NUMBER

    + */ + public static final String STATUS_RES_PACKAGE = "status_res_package"; + + /** + * The resource ID of the label describing the source of the status update, e.g. "Google + * Talk". This resource should be scoped by the {@link #STATUS_RES_PACKAGE}. + *

    Type: NUMBER

    + */ + public static final String STATUS_LABEL = "status_label"; + + /** + * The resource ID of the icon for the source of the status update. + * This resource should be scoped by the {@link #STATUS_RES_PACKAGE}. + *

    Type: NUMBER

    + */ + public static final String STATUS_ICON = "status_icon"; + } + + protected interface DataColumns { + /** + * The package name to use when creating {@link Resources} objects for + * this data row. This value is only designed for use when building user + * interfaces, and should not be used to infer the owner. + * + * @hide + */ + public static final String RES_PACKAGE = "res_package"; + + /** + * The MIME type of the item represented by this row. + */ + public static final String MIMETYPE = "mimetype"; + + /** + * A reference to the {@link RawContacts#_ID} + * that this data belongs to. + */ + public static final String RAW_CONTACT_ID = "raw_contact_id"; + + /** + * Whether this is the primary entry of its kind for the raw contact it belongs to + *

    Type: INTEGER (if set, non-0 means true)

    + */ + public static final String IS_PRIMARY = "is_primary"; + + /** + * Whether this is the primary entry of its kind for the aggregate + * contact it belongs to. Any data record that is "super primary" must + * also be "primary". + *

    Type: INTEGER (if set, non-0 means true)

    + */ + public static final String IS_SUPER_PRIMARY = "is_super_primary"; + + /** + * The version of this data record. This is a read-only value. The data column is + * guaranteed to not change without the version going up. This value is monotonically + * increasing. + *

    Type: INTEGER

    + */ + public static final String DATA_VERSION = "data_version"; + + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA1 = "data1"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA2 = "data2"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA3 = "data3"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA4 = "data4"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA5 = "data5"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA6 = "data6"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA7 = "data7"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA8 = "data8"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA9 = "data9"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA10 = "data10"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA11 = "data11"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA12 = "data12"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA13 = "data13"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA14 = "data14"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA15 = "data15"; + + /** Generic column for use by sync adapters. */ + public static final String SYNC1 = "data_sync1"; + /** Generic column for use by sync adapters. */ + public static final String SYNC2 = "data_sync2"; + /** Generic column for use by sync adapters. */ + public static final String SYNC3 = "data_sync3"; + /** Generic column for use by sync adapters. */ + public static final String SYNC4 = "data_sync4"; + } + + /** + * Combines all columns returned by {@link Data} table queries. + */ + protected interface DataColumnsWithJoins extends BaseColumns, DataColumns, StatusColumns, + RawContactsColumns, ContactsColumns, ContactOptionsColumns, ContactStatusColumns { + + } + + /** + * Constants for the data table, which contains data points tied to a raw contact. + * For example, a phone number or email address. Each row in this table contains a type + * definition and some generic columns. Each data type can define the meaning for each of + * the generic columns. + */ + public final static class Data implements DataColumnsWithJoins { + /** + * This utility class cannot be instantiated + */ + private Data() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of data. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/data"; + + /** + * If {@link #FOR_EXPORT_ONLY} is explicitly set to "1", returned Cursor toward + * Data.CONTENT_URI contains only exportable data. + * + * This flag is useful (currently) only for vCard exporter in Contacts app, which + * needs to exclude "un-exportable" data from available data to export, while + * Contacts app itself has priviledge to access all data including "un-expotable" + * ones and providers return all of them regardless of the callers' intention. + *

    Type: INTEGER

    + * + * @hide Maybe available only in Eclair and not really ready for public use. + * TODO: remove, or implement this feature completely. As of now (Eclair), + * we only use this flag in queryEntities(), not query(). + */ + public static final String FOR_EXPORT_ONLY = "for_export_only"; + + /** + * Build a {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI} + * style {@link Uri} for the parent {@link android.provider.ContactsContract.Contacts} + * entry of the given {@link Data} entry. + */ + public static Uri getContactLookupUri(ContentResolver resolver, Uri dataUri) { + final Cursor cursor = resolver.query(dataUri, new String[] { + RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY + }, null, null, null); + + Uri lookupUri = null; + try { + if (cursor != null && cursor.moveToFirst()) { + final long contactId = cursor.getLong(0); + final String lookupKey = cursor.getString(1); + return Contacts.getLookupUri(contactId, lookupKey); + } + } finally { + if (cursor != null) cursor.close(); + } + return lookupUri; + } + } + + /** + * Constants for the raw contacts entities table, which can be though of as an outer join + * of the raw_contacts table with the data table. + */ + public final static class RawContactsEntity + implements BaseColumns, DataColumns, RawContactsColumns { + /** + * This utility class cannot be instantiated + */ + private RawContactsEntity() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "raw_contact_entities"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of raw contact entities. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/raw_contact_entity"; + + /** + * If {@link #FOR_EXPORT_ONLY} is explicitly set to "1", returned Cursor toward + * Data.CONTENT_URI contains only exportable data. + * + * This flag is useful (currently) only for vCard exporter in Contacts app, which + * needs to exclude "un-exportable" data from available data to export, while + * Contacts app itself has priviledge to access all data including "un-expotable" + * ones and providers return all of them regardless of the callers' intention. + *

    Type: INTEGER

    + * + * @hide Maybe available only in Eclair and not really ready for public use. + * TODO: remove, or implement this feature completely. As of now (Eclair), + * we only use this flag in queryEntities(), not query(). + */ + public static final String FOR_EXPORT_ONLY = "for_export_only"; + + /** + * The ID of the data column. The value will be null if this raw contact has no data rows. + *

    Type: INTEGER

    + */ + public static final String DATA_ID = "data_id"; + } + + protected interface PhoneLookupColumns { + /** + * The phone number as the user entered it. + *

    Type: TEXT

    + */ + public static final String NUMBER = "number"; + + /** + * The type of phone number, for example Home or Work. + *

    Type: INTEGER

    + */ + public static final String TYPE = "type"; + + /** + * The user defined label for the phone number. + *

    Type: TEXT

    + */ + public static final String LABEL = "label"; + } + + /** + * A table that represents the result of looking up a phone number, for + * example for caller ID. To perform a lookup you must append the number you + * want to find to {@link #CONTENT_FILTER_URI}. + */ + public static final class PhoneLookup implements BaseColumns, PhoneLookupColumns, + ContactsColumns, ContactOptionsColumns { + /** + * This utility class cannot be instantiated + */ + private PhoneLookup() {} + + /** + * The content:// style URI for this table. Append the phone number you want to lookup + * to this URI and query it to perform a lookup. For example: + * + * {@code + * Uri lookupUri = Uri.withAppendedPath(PhoneLookup.CONTENT_URI, phoneNumber); + * } + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(AUTHORITY_URI, + "phone_lookup"); + } + + /** + * Additional data mixed in with {@link StatusColumns} to link + * back to specific {@link ContactsContract.Data#_ID} entries. + */ + protected interface PresenceColumns { + + /** + * Reference to the {@link Data#_ID} entry that owns this presence. + *

    Type: INTEGER

    + */ + public static final String DATA_ID = "presence_data_id"; + + /** + *

    Type: NUMBER

    + */ + public static final String PROTOCOL = "protocol"; + + /** + * Name of the custom protocol. Should be supplied along with the {@link #PROTOCOL} value + * {@link ContactsContract.CommonDataKinds.Im#PROTOCOL_CUSTOM}. Should be null or + * omitted if {@link #PROTOCOL} value is not + * {@link ContactsContract.CommonDataKinds.Im#PROTOCOL_CUSTOM}. + * + *

    Type: NUMBER

    + */ + public static final String CUSTOM_PROTOCOL = "custom_protocol"; + + /** + * The IM handle the presence item is for. The handle is scoped to + * {@link #PROTOCOL}. + *

    Type: TEXT

    + */ + public static final String IM_HANDLE = "im_handle"; + + /** + * The IM account for the local user that the presence data came from. + *

    Type: TEXT

    + */ + public static final String IM_ACCOUNT = "im_account"; + } + + /** + * A status update is linked to a {@link Data} row and captures the user's latest status + * update via the corresponding source, e.g. "Having lunch" via "Google Talk". + */ + // TODO make final as soon as Presence is removed + public static /*final*/ class StatusUpdates implements StatusColumns, PresenceColumns { + + /** + * This utility class cannot be instantiated + */ + private StatusUpdates() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "status_updates"); + + /** + * Gets the resource ID for the proper presence icon. + * + * @param status the status to get the icon for + * @return the resource ID for the proper presence icon + */ + public static final int getPresenceIconResourceId(int status) { + switch (status) { + case AVAILABLE: + return android.R.drawable.presence_online; + case IDLE: + case AWAY: + return android.R.drawable.presence_away; + case DO_NOT_DISTURB: + return android.R.drawable.presence_busy; + case INVISIBLE: + return android.R.drawable.presence_invisible; + case OFFLINE: + default: + return android.R.drawable.presence_offline; + } + } + + /** + * Returns the precedence of the status code the higher number being the higher precedence. + * + * @param status The status code. + * @return An integer representing the precedence, 0 being the lowest. + */ + public static final int getPresencePrecedence(int status) { + // Keep this function here incase we want to enforce a different precedence than the + // natural order of the status constants. + return status; + } + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * status update details. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/status-update"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * status update detail. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/status-update"; + } + + @Deprecated + public static final class Presence extends StatusUpdates { + + } + + /** + * Container for definitions of common data types stored in the {@link Data} table. + */ + public static final class CommonDataKinds { + /** + * This utility class cannot be instantiated + */ + private CommonDataKinds() {} + + /** + * The {@link Data#RES_PACKAGE} value for common data that should be + * shown using a default style. + * + * @hide RES_PACKAGE is hidden + */ + public static final String PACKAGE_COMMON = "common"; + + /** + * The base types that all "Typed" data kinds support. + */ + public interface BaseTypes { + /** + * A custom type. The custom label should be supplied by user. + */ + public static int TYPE_CUSTOM = 0; + } + + /** + * Columns common across the specific types. + */ + protected interface CommonColumns extends BaseTypes { + /** + * The data for the contact method. + *

    Type: TEXT

    + */ + public static final String DATA = DataColumns.DATA1; + + /** + * The type of data, for example Home or Work. + *

    Type: INTEGER

    + */ + public static final String TYPE = DataColumns.DATA2; + + /** + * The user defined label for the the contact method. + *

    Type: TEXT

    + */ + public static final String LABEL = DataColumns.DATA3; + } + + /** + * Parts of the name. + */ + public static final class StructuredName implements DataColumnsWithJoins { + /** + * This utility class cannot be instantiated + */ + private StructuredName() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"; + + /** + * The name that should be used to display the contact. + * Unstructured component of the name should be consistent with + * its structured representation. + *

    + * Type: TEXT + */ + public static final String DISPLAY_NAME = DATA1; + + /** + * The given name for the contact. + *

    Type: TEXT

    + */ + public static final String GIVEN_NAME = DATA2; + + /** + * The family name for the contact. + *

    Type: TEXT

    + */ + public static final String FAMILY_NAME = DATA3; + + /** + * The contact's honorific prefix, e.g. "Sir" + *

    Type: TEXT

    + */ + public static final String PREFIX = DATA4; + + /** + * The contact's middle name + *

    Type: TEXT

    + */ + public static final String MIDDLE_NAME = DATA5; + + /** + * The contact's honorific suffix, e.g. "Jr" + */ + public static final String SUFFIX = DATA6; + + /** + * The phonetic version of the given name for the contact. + *

    Type: TEXT

    + */ + public static final String PHONETIC_GIVEN_NAME = DATA7; + + /** + * The phonetic version of the additional name for the contact. + *

    Type: TEXT

    + */ + public static final String PHONETIC_MIDDLE_NAME = DATA8; + + /** + * The phonetic version of the family name for the contact. + *

    Type: TEXT

    + */ + public static final String PHONETIC_FAMILY_NAME = DATA9; + } + + /** + * A nickname. + */ + public static final class Nickname implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Nickname() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/nickname"; + + public static final int TYPE_DEFAULT = 1; + public static final int TYPE_OTHER_NAME = 2; + public static final int TYPE_MAINDEN_NAME = 3; + public static final int TYPE_SHORT_NAME = 4; + public static final int TYPE_INITIALS = 5; + + /** + * The name itself + */ + public static final String NAME = DATA; + } + + /** + * Common data definition for telephone numbers. + */ + public static final class Phone implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Phone() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone_v2"; + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * phones. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone_v2"; + + /** + * The content:// style URI for all data records of the + * {@link #CONTENT_ITEM_TYPE} MIME type, combined with the + * associated raw contact and aggregate contact data. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI, + "phones"); + + /** + * The content:// style URL for phone lookup using a filter. The filter returns + * records of MIME type {@link #CONTENT_ITEM_TYPE}. The filter is applied + * to display names as well as phone numbers. The filter argument should be passed + * as an additional path segment after this URI. + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(CONTENT_URI, + "filter"); + + public static final int TYPE_HOME = 1; + public static final int TYPE_MOBILE = 2; + public static final int TYPE_WORK = 3; + public static final int TYPE_FAX_WORK = 4; + public static final int TYPE_FAX_HOME = 5; + public static final int TYPE_PAGER = 6; + public static final int TYPE_OTHER = 7; + public static final int TYPE_CALLBACK = 8; + public static final int TYPE_CAR = 9; + public static final int TYPE_COMPANY_MAIN = 10; + public static final int TYPE_ISDN = 11; + public static final int TYPE_MAIN = 12; + public static final int TYPE_OTHER_FAX = 13; + public static final int TYPE_RADIO = 14; + public static final int TYPE_TELEX = 15; + public static final int TYPE_TTY_TDD = 16; + public static final int TYPE_WORK_MOBILE = 17; + public static final int TYPE_WORK_PAGER = 18; + public static final int TYPE_ASSISTANT = 19; + public static final int TYPE_MMS = 20; + + /** + * The phone number as the user entered it. + *

    Type: TEXT

    + */ + public static final String NUMBER = DATA; + + /** + * @deprecated use {@link #getTypeLabel(Resources, int, CharSequence)} instead. + * @hide + */ + @Deprecated + public static final CharSequence getDisplayLabel(Context context, int type, + CharSequence label, CharSequence[] labelArray) { + return getTypeLabel(context.getResources(), type, label); + } + + /** + * @deprecated use {@link #getTypeLabel(Resources, int, CharSequence)} instead. + * @hide + */ + @Deprecated + public static final CharSequence getDisplayLabel(Context context, int type, + CharSequence label) { + return getTypeLabel(context.getResources(), type, label); + } + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static final int getTypeLabelResource(int type) { + switch (type) { + case TYPE_HOME: return com.android.internal.R.string.phoneTypeHome; + case TYPE_MOBILE: return com.android.internal.R.string.phoneTypeMobile; + case TYPE_WORK: return com.android.internal.R.string.phoneTypeWork; + case TYPE_FAX_WORK: return com.android.internal.R.string.phoneTypeFaxWork; + case TYPE_FAX_HOME: return com.android.internal.R.string.phoneTypeFaxHome; + case TYPE_PAGER: return com.android.internal.R.string.phoneTypePager; + case TYPE_OTHER: return com.android.internal.R.string.phoneTypeOther; + case TYPE_CALLBACK: return com.android.internal.R.string.phoneTypeCallback; + case TYPE_CAR: return com.android.internal.R.string.phoneTypeCar; + case TYPE_COMPANY_MAIN: return com.android.internal.R.string.phoneTypeCompanyMain; + case TYPE_ISDN: return com.android.internal.R.string.phoneTypeIsdn; + case TYPE_MAIN: return com.android.internal.R.string.phoneTypeMain; + case TYPE_OTHER_FAX: return com.android.internal.R.string.phoneTypeOtherFax; + case TYPE_RADIO: return com.android.internal.R.string.phoneTypeRadio; + case TYPE_TELEX: return com.android.internal.R.string.phoneTypeTelex; + case TYPE_TTY_TDD: return com.android.internal.R.string.phoneTypeTtyTdd; + case TYPE_WORK_MOBILE: return com.android.internal.R.string.phoneTypeWorkMobile; + case TYPE_WORK_PAGER: return com.android.internal.R.string.phoneTypeWorkPager; + case TYPE_ASSISTANT: return com.android.internal.R.string.phoneTypeAssistant; + case TYPE_MMS: return com.android.internal.R.string.phoneTypeMms; + default: return com.android.internal.R.string.phoneTypeCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given type, + * possibly substituting the given {@link #LABEL} value + * for {@link #TYPE_CUSTOM}. + */ + public static final CharSequence getTypeLabel(Resources res, int type, + CharSequence label) { + if ((type == TYPE_CUSTOM || type == TYPE_ASSISTANT) && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getTypeLabelResource(type); + return res.getText(labelRes); + } + } + } + + /** + * Common data definition for email addresses. + */ + public static final class Email implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Email() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email_v2"; + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of email addresses. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/email_v2"; + + /** + * The content:// style URI for all data records of the + * {@link #CONTENT_ITEM_TYPE} MIME type, combined with the + * associated raw contact and aggregate contact data. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI, + "emails"); + + /** + * The content:// style URL for looking up data rows by email address. The + * lookup argument, an email address, should be passed as an additional path segment + * after this URI. + */ + public static final Uri CONTENT_LOOKUP_URI = Uri.withAppendedPath(CONTENT_URI, + "lookup"); + + /** + * The content:// style URL for email lookup using a filter. The filter returns + * records of MIME type {@link #CONTENT_ITEM_TYPE}. The filter is applied + * to display names as well as email addresses. The filter argument should be passed + * as an additional path segment after this URI. + */ + public static final Uri CONTENT_FILTER_URI = Uri.withAppendedPath(CONTENT_URI, + "filter"); + + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + public static final int TYPE_MOBILE = 4; + + /** + * The display name for the email address + *

    Type: TEXT

    + */ + public static final String DISPLAY_NAME = DATA4; + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static final int getTypeLabelResource(int type) { + switch (type) { + case TYPE_HOME: return com.android.internal.R.string.emailTypeHome; + case TYPE_WORK: return com.android.internal.R.string.emailTypeWork; + case TYPE_OTHER: return com.android.internal.R.string.emailTypeOther; + case TYPE_MOBILE: return com.android.internal.R.string.emailTypeMobile; + default: return com.android.internal.R.string.emailTypeCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given type, + * possibly substituting the given {@link #LABEL} value + * for {@link #TYPE_CUSTOM}. + */ + public static final CharSequence getTypeLabel(Resources res, int type, + CharSequence label) { + if (type == TYPE_CUSTOM && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getTypeLabelResource(type); + return res.getText(labelRes); + } + } + } + + /** + * Common data definition for postal addresses. + */ + public static final class StructuredPostal implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private StructuredPostal() { + } + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/postal-address_v2"; + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * postal addresses. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/postal-address_v2"; + + /** + * The content:// style URI for all data records of the + * {@link StructuredPostal#CONTENT_ITEM_TYPE} MIME type. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI, + "postals"); + + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + + /** + * The full, unstructured postal address. This field must be + * consistent with any structured data. + *

    + * Type: TEXT + */ + public static final String FORMATTED_ADDRESS = DATA; + + /** + * Can be street, avenue, road, etc. This element also includes the + * house number and room/apartment/flat/floor number. + *

    + * Type: TEXT + */ + public static final String STREET = DATA4; + + /** + * Covers actual P.O. boxes, drawers, locked bags, etc. This is + * usually but not always mutually exclusive with street. + *

    + * Type: TEXT + */ + public static final String POBOX = DATA5; + + /** + * This is used to disambiguate a street address when a city + * contains more than one street with the same name, or to specify a + * small place whose mail is routed through a larger postal town. In + * China it could be a county or a minor city. + *

    + * Type: TEXT + */ + public static final String NEIGHBORHOOD = DATA6; + + /** + * Can be city, village, town, borough, etc. This is the postal town + * and not necessarily the place of residence or place of business. + *

    + * Type: TEXT + */ + public static final String CITY = DATA7; + + /** + * A state, province, county (in Ireland), Land (in Germany), + * departement (in France), etc. + *

    + * Type: TEXT + */ + public static final String REGION = DATA8; + + /** + * Postal code. Usually country-wide, but sometimes specific to the + * city (e.g. "2" in "Dublin 2, Ireland" addresses). + *

    + * Type: TEXT + */ + public static final String POSTCODE = DATA9; + + /** + * The name or code of the country. + *

    + * Type: TEXT + */ + public static final String COUNTRY = DATA10; + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static final int getTypeLabelResource(int type) { + switch (type) { + case TYPE_HOME: return com.android.internal.R.string.postalTypeHome; + case TYPE_WORK: return com.android.internal.R.string.postalTypeWork; + case TYPE_OTHER: return com.android.internal.R.string.postalTypeOther; + default: return com.android.internal.R.string.postalTypeCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given type, + * possibly substituting the given {@link #LABEL} value + * for {@link #TYPE_CUSTOM}. + */ + public static final CharSequence getTypeLabel(Resources res, int type, + CharSequence label) { + if (type == TYPE_CUSTOM && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getTypeLabelResource(type); + return res.getText(labelRes); + } + } + } + + /** + * Common data definition for IM addresses. + */ + public static final class Im implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Im() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im"; + + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + + /** + * This column should be populated with one of the defined + * constants, e.g. {@link #PROTOCOL_YAHOO}. If the value of this + * column is {@link #PROTOCOL_CUSTOM}, the {@link #CUSTOM_PROTOCOL} + * should contain the name of the custom protocol. + */ + public static final String PROTOCOL = DATA5; + + public static final String CUSTOM_PROTOCOL = DATA6; + + /* + * The predefined IM protocol types. + */ + public static final int PROTOCOL_CUSTOM = -1; + public static final int PROTOCOL_AIM = 0; + public static final int PROTOCOL_MSN = 1; + public static final int PROTOCOL_YAHOO = 2; + public static final int PROTOCOL_SKYPE = 3; + public static final int PROTOCOL_QQ = 4; + public static final int PROTOCOL_GOOGLE_TALK = 5; + public static final int PROTOCOL_ICQ = 6; + public static final int PROTOCOL_JABBER = 7; + public static final int PROTOCOL_NETMEETING = 8; + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static final int getTypeLabelResource(int type) { + switch (type) { + case TYPE_HOME: return com.android.internal.R.string.imTypeHome; + case TYPE_WORK: return com.android.internal.R.string.imTypeWork; + case TYPE_OTHER: return com.android.internal.R.string.imTypeOther; + default: return com.android.internal.R.string.imTypeCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given type, + * possibly substituting the given {@link #LABEL} value + * for {@link #TYPE_CUSTOM}. + */ + public static final CharSequence getTypeLabel(Resources res, int type, + CharSequence label) { + if (type == TYPE_CUSTOM && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getTypeLabelResource(type); + return res.getText(labelRes); + } + } + + /** + * Return the string resource that best describes the given + * {@link #PROTOCOL}. Will always return a valid resource. + */ + public static final int getProtocolLabelResource(int type) { + switch (type) { + case PROTOCOL_AIM: return com.android.internal.R.string.imProtocolAim; + case PROTOCOL_MSN: return com.android.internal.R.string.imProtocolMsn; + case PROTOCOL_YAHOO: return com.android.internal.R.string.imProtocolYahoo; + case PROTOCOL_SKYPE: return com.android.internal.R.string.imProtocolSkype; + case PROTOCOL_QQ: return com.android.internal.R.string.imProtocolQq; + case PROTOCOL_GOOGLE_TALK: return com.android.internal.R.string.imProtocolGoogleTalk; + case PROTOCOL_ICQ: return com.android.internal.R.string.imProtocolIcq; + case PROTOCOL_JABBER: return com.android.internal.R.string.imProtocolJabber; + case PROTOCOL_NETMEETING: return com.android.internal.R.string.imProtocolNetMeeting; + default: return com.android.internal.R.string.imProtocolCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given + * protocol, possibly substituting the given + * {@link #CUSTOM_PROTOCOL} value for {@link #PROTOCOL_CUSTOM}. + */ + public static final CharSequence getProtocolLabel(Resources res, int type, + CharSequence label) { + if (type == PROTOCOL_CUSTOM && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getProtocolLabelResource(type); + return res.getText(labelRes); + } + } + } + + /** + * Common data definition for organizations. + */ + public static final class Organization implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Organization() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"; + + public static final int TYPE_WORK = 1; + public static final int TYPE_OTHER = 2; + + /** + * The company as the user entered it. + *

    Type: TEXT

    + */ + public static final String COMPANY = DATA; + + /** + * The position title at this company as the user entered it. + *

    Type: TEXT

    + */ + public static final String TITLE = DATA4; + + /** + * The department at this company as the user entered it. + *

    Type: TEXT

    + */ + public static final String DEPARTMENT = DATA5; + + /** + * The job description at this company as the user entered it. + *

    Type: TEXT

    + */ + public static final String JOB_DESCRIPTION = DATA6; + + /** + * The symbol of this company as the user entered it. + *

    Type: TEXT

    + */ + public static final String SYMBOL = DATA7; + + /** + * The phonetic name of this company as the user entered it. + *

    Type: TEXT

    + */ + public static final String PHONETIC_NAME = DATA8; + + /** + * The office location of this organization. + *

    Type: TEXT

    + */ + public static final String OFFICE_LOCATION = DATA9; + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static final int getTypeLabelResource(int type) { + switch (type) { + case TYPE_WORK: return com.android.internal.R.string.orgTypeWork; + case TYPE_OTHER: return com.android.internal.R.string.orgTypeOther; + default: return com.android.internal.R.string.orgTypeCustom; + } + } + + /** + * Return a {@link CharSequence} that best describes the given type, + * possibly substituting the given {@link #LABEL} value + * for {@link #TYPE_CUSTOM}. + */ + public static final CharSequence getTypeLabel(Resources res, int type, + CharSequence label) { + if (type == TYPE_CUSTOM && !TextUtils.isEmpty(label)) { + return label; + } else { + final int labelRes = getTypeLabelResource(type); + return res.getText(labelRes); + } + } + } + + /** + * Common data definition for relations. + */ + public static final class Relation implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Relation() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/relation"; + + public static final int TYPE_ASSISTANT = 1; + public static final int TYPE_BROTHER = 2; + public static final int TYPE_CHILD = 3; + public static final int TYPE_DOMESTIC_PARTNER = 4; + public static final int TYPE_FATHER = 5; + public static final int TYPE_FRIEND = 6; + public static final int TYPE_MANAGER = 7; + public static final int TYPE_MOTHER = 8; + public static final int TYPE_PARENT = 9; + public static final int TYPE_PARTNER = 10; + public static final int TYPE_REFERRED_BY = 11; + public static final int TYPE_RELATIVE = 12; + public static final int TYPE_SISTER = 13; + public static final int TYPE_SPOUSE = 14; + + /** + * The name of the relative as the user entered it. + *

    Type: TEXT

    + */ + public static final String NAME = DATA; + } + + /** + * Common data definition for events. + */ + public static final class Event implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Event() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_event"; + + public static final int TYPE_ANNIVERSARY = 1; + public static final int TYPE_OTHER = 2; + public static final int TYPE_BIRTHDAY = 3; + + /** + * The event start date as the user entered it. + *

    Type: TEXT

    + */ + public static final String START_DATE = DATA; + + /** + * Return the string resource that best describes the given + * {@link #TYPE}. Will always return a valid resource. + */ + public static int getTypeResource(Integer type) { + if (type == null) { + return com.android.internal.R.string.eventTypeOther; + } + switch (type) { + case TYPE_ANNIVERSARY: + return com.android.internal.R.string.eventTypeAnniversary; + case TYPE_BIRTHDAY: return com.android.internal.R.string.eventTypeBirthday; + case TYPE_OTHER: return com.android.internal.R.string.eventTypeOther; + default: return com.android.internal.R.string.eventTypeOther; + } + } + } + + /** + * Photo of the contact. + */ + public static final class Photo implements DataColumnsWithJoins { + /** + * This utility class cannot be instantiated + */ + private Photo() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/photo"; + + /** + * Thumbnail photo of the raw contact. This is the raw bytes of an image + * that could be inflated using {@link android.graphics.BitmapFactory}. + *

    + * Type: BLOB + */ + public static final String PHOTO = DATA15; + } + + /** + * Notes about the contact. + */ + public static final class Note implements DataColumnsWithJoins { + /** + * This utility class cannot be instantiated + */ + private Note() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/note"; + + /** + * The note text. + *

    Type: TEXT

    + */ + public static final String NOTE = DATA1; + } + + /** + * Group Membership. + */ + public static final class GroupMembership implements DataColumnsWithJoins { + /** + * This utility class cannot be instantiated + */ + private GroupMembership() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/group_membership"; + + /** + * The row id of the group that this group membership refers to. Exactly one of + * this or {@link #GROUP_SOURCE_ID} must be set when inserting a row. + *

    Type: INTEGER

    + */ + public static final String GROUP_ROW_ID = DATA1; + + /** + * The sourceid of the group that this group membership refers to. Exactly one of + * this or {@link #GROUP_ROW_ID} must be set when inserting a row. + *

    Type: TEXT

    + */ + public static final String GROUP_SOURCE_ID = "group_sourceid"; + } + + /** + * Website related to the contact. + */ + public static final class Website implements DataColumnsWithJoins, CommonColumns { + /** + * This utility class cannot be instantiated + */ + private Website() {} + + /** MIME type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/website"; + + public static final int TYPE_HOMEPAGE = 1; + public static final int TYPE_BLOG = 2; + public static final int TYPE_PROFILE = 3; + public static final int TYPE_HOME = 4; + public static final int TYPE_WORK = 5; + public static final int TYPE_FTP = 6; + public static final int TYPE_OTHER = 7; + + /** + * The website URL string. + *

    Type: TEXT

    + */ + public static final String URL = DATA; + } + } + + protected interface GroupsColumns { + /** + * The display title of this group. + *

    + * Type: TEXT + */ + public static final String TITLE = "title"; + + /** + * The package name to use when creating {@link Resources} objects for + * this group. This value is only designed for use when building user + * interfaces, and should not be used to infer the owner. + * + * @hide + */ + public static final String RES_PACKAGE = "res_package"; + + /** + * The display title of this group to load as a resource from + * {@link #RES_PACKAGE}, which may be localized. + *

    Type: TEXT

    + * + * @hide + */ + public static final String TITLE_RES = "title_res"; + + /** + * Notes about the group. + *

    + * Type: TEXT + */ + public static final String NOTES = "notes"; + + /** + * The ID of this group if it is a System Group, i.e. a group that has a special meaning + * to the sync adapter, null otherwise. + *

    Type: TEXT

    + */ + public static final String SYSTEM_ID = "system_id"; + + /** + * The total number of {@link Contacts} that have + * {@link CommonDataKinds.GroupMembership} in this group. Read-only value that is only + * present when querying {@link Groups#CONTENT_SUMMARY_URI}. + *

    + * Type: INTEGER + */ + public static final String SUMMARY_COUNT = "summ_count"; + + /** + * The total number of {@link Contacts} that have both + * {@link CommonDataKinds.GroupMembership} in this group, and also have phone numbers. + * Read-only value that is only present when querying + * {@link Groups#CONTENT_SUMMARY_URI}. + *

    + * Type: INTEGER + */ + public static final String SUMMARY_WITH_PHONES = "summ_phones"; + + /** + * Flag indicating if the contacts belonging to this group should be + * visible in any user interface. + *

    + * Type: INTEGER (boolean) + */ + public static final String GROUP_VISIBLE = "group_visible"; + + /** + * The "deleted" flag: "0" by default, "1" if the row has been marked + * for deletion. When {@link android.content.ContentResolver#delete} is + * called on a raw contact, it is marked for deletion and removed from its + * aggregate contact. The sync adaptor deletes the raw contact on the server and + * then calls ContactResolver.delete once more, this time setting the the + * {@link ContactsContract#CALLER_IS_SYNCADAPTER} query parameter to finalize + * the data removal. + *

    Type: INTEGER

    + */ + public static final String DELETED = "deleted"; + + /** + * Whether this group should be synced if the SYNC_EVERYTHING settings + * is false for this group's account. + *

    + * Type: INTEGER (boolean) + */ + public static final String SHOULD_SYNC = "should_sync"; + } + + /** + * Constants for the groups table. + */ + public static final class Groups implements BaseColumns, GroupsColumns, SyncColumns { + /** + * This utility class cannot be instantiated + */ + private Groups() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "groups"); + + /** + * The content:// style URI for this table joined with details data from + * {@link Data}. + */ + public static final Uri CONTENT_SUMMARY_URI = Uri.withAppendedPath(AUTHORITY_URI, + "groups_summary"); + + /** + * The MIME type of a directory of groups. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/group"; + + /** + * The MIME type of a single group. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/group"; + } + + /** + * Constants for the contact aggregation exceptions table, which contains + * aggregation rules overriding those used by automatic aggregation. This type only + * supports query and update. Neither insert nor delete are supported. + */ + public static final class AggregationExceptions implements BaseColumns { + /** + * This utility class cannot be instantiated + */ + private AggregationExceptions() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "aggregation_exceptions"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of data. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/aggregation_exception"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of an aggregation exception + */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/aggregation_exception"; + + /** + * The type of exception: {@link #TYPE_KEEP_TOGETHER}, {@link #TYPE_KEEP_SEPARATE} or + * {@link #TYPE_AUTOMATIC}. + * + *

    Type: INTEGER

    + */ + public static final String TYPE = "type"; + + /** + * Allows the provider to automatically decide whether the specified raw contacts should + * be included in the same aggregate contact or not. + */ + public static final int TYPE_AUTOMATIC = 0; + + /** + * Makes sure that the specified raw contacts are included in the same + * aggregate contact. + */ + public static final int TYPE_KEEP_TOGETHER = 1; + + /** + * Makes sure that the specified raw contacts are NOT included in the same + * aggregate contact. + */ + public static final int TYPE_KEEP_SEPARATE = 2; + + /** + * A reference to the {@link RawContacts#_ID} of the raw contact that the rule applies to. + */ + public static final String RAW_CONTACT_ID1 = "raw_contact_id1"; + + /** + * A reference to the other {@link RawContacts#_ID} of the raw contact that the rule + * applies to. + */ + public static final String RAW_CONTACT_ID2 = "raw_contact_id2"; + } + + protected interface SettingsColumns { + /** + * The name of the account instance to which this row belongs. + *

    Type: TEXT

    + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * The type of account to which this row belongs, which when paired with + * {@link #ACCOUNT_NAME} identifies a specific account. + *

    Type: TEXT

    + */ + public static final String ACCOUNT_TYPE = "account_type"; + + /** + * Depending on the mode defined by the sync-adapter, this flag controls + * the top-level sync behavior for this data source. + *

    + * Type: INTEGER (boolean) + */ + public static final String SHOULD_SYNC = "should_sync"; + + /** + * Flag indicating if contacts without any {@link CommonDataKinds.GroupMembership} + * entries should be visible in any user interface. + *

    + * Type: INTEGER (boolean) + */ + public static final String UNGROUPED_VISIBLE = "ungrouped_visible"; + + /** + * Read-only flag indicating if this {@link #SHOULD_SYNC} or any + * {@link Groups#SHOULD_SYNC} under this account have been marked as + * unsynced. + */ + public static final String ANY_UNSYNCED = "any_unsynced"; + + /** + * Read-only count of {@link Contacts} from a specific source that have + * no {@link CommonDataKinds.GroupMembership} entries. + *

    + * Type: INTEGER + */ + public static final String UNGROUPED_COUNT = "summ_count"; + + /** + * Read-only count of {@link Contacts} from a specific source that have + * no {@link CommonDataKinds.GroupMembership} entries, and also have phone numbers. + *

    + * Type: INTEGER + */ + public static final String UNGROUPED_WITH_PHONES = "summ_phones"; + } + + /** + * Contacts-specific settings for various {@link Account}. + */ + public static final class Settings implements SettingsColumns { + /** + * This utility class cannot be instantiated + */ + private Settings() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "settings"); + + /** + * The MIME-type of {@link #CONTENT_URI} providing a directory of + * settings. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/setting"; + + /** + * The MIME-type of {@link #CONTENT_URI} providing a single setting. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/setting"; + } + + /** + * Helper methods to display QuickContact dialogs that allow users to pivot on + * a specific {@link Contacts} entry. + */ + public static final class QuickContact { + /** + * Action used to trigger person pivot dialog. + * @hide + */ + public static final String ACTION_QUICK_CONTACT = + "com.android.contacts.action.QUICK_CONTACT"; + + /** + * Extra used to specify pivot dialog location in screen coordinates. + * @hide + */ + public static final String EXTRA_TARGET_RECT = "target_rect"; + + /** + * Extra used to specify size of pivot dialog. + * @hide + */ + public static final String EXTRA_MODE = "mode"; + + /** + * Extra used to indicate a list of specific MIME-types to exclude and + * not display. Stored as a {@link String} array. + * @hide + */ + public static final String EXTRA_EXCLUDE_MIMES = "exclude_mimes"; + + /** + * Small QuickContact mode, usually presented with minimal actions. + */ + public static final int MODE_SMALL = 1; + + /** + * Medium QuickContact mode, includes actions and light summary describing + * the {@link Contacts} entry being shown. This may include social + * status and presence details. + */ + public static final int MODE_MEDIUM = 2; + + /** + * Large QuickContact mode, includes actions and larger, card-like summary + * of the {@link Contacts} entry being shown. This may include detailed + * information, such as a photo. + */ + public static final int MODE_LARGE = 3; + + /** + * Trigger a dialog that lists the various methods of interacting with + * the requested {@link Contacts} entry. This may be based on available + * {@link Data} rows under that contact, and may also include social + * status and presence details. + * + * @param context The parent {@link Context} that may be used as the + * parent for this dialog. + * @param target Specific {@link View} from your layout that this dialog + * should be centered around. In particular, if the dialog + * has a "callout" arrow, it will be pointed and centered + * around this {@link View}. + * @param lookupUri A {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style + * {@link Uri} that describes a specific contact to feature + * in this dialog. + * @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or + * {@link #MODE_LARGE}, indicating the desired dialog size, + * when supported. + * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types + * to exclude when showing this dialog. For example, when + * already viewing the contact details card, this can be used + * to omit the details entry from the dialog. + */ + public static void showQuickContact(Context context, View target, Uri lookupUri, int mode, + String[] excludeMimes) { + // Find location and bounds of target view + final int[] location = new int[2]; + target.getLocationOnScreen(location); + + final Rect rect = new Rect(); + rect.left = location[0]; + rect.top = location[1]; + rect.right = rect.left + target.getWidth(); + rect.bottom = rect.top + target.getHeight(); + + // Trigger with obtained rectangle + showQuickContact(context, rect, lookupUri, mode, excludeMimes); + } + + /** + * Trigger a dialog that lists the various methods of interacting with + * the requested {@link Contacts} entry. This may be based on available + * {@link Data} rows under that contact, and may also include social + * status and presence details. + * + * @param context The parent {@link Context} that may be used as the + * parent for this dialog. + * @param target Specific {@link Rect} that this dialog should be + * centered around, in screen coordinates. In particular, if + * the dialog has a "callout" arrow, it will be pointed and + * centered around this {@link Rect}. + * @param lookupUri A {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style + * {@link Uri} that describes a specific contact to feature + * in this dialog. + * @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or + * {@link #MODE_LARGE}, indicating the desired dialog size, + * when supported. + * @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types + * to exclude when showing this dialog. For example, when + * already viewing the contact details card, this can be used + * to omit the details entry from the dialog. + */ + public static void showQuickContact(Context context, Rect target, Uri lookupUri, int mode, + String[] excludeMimes) { + // Launch pivot dialog through intent for now + final Intent intent = new Intent(ACTION_QUICK_CONTACT); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + intent.setData(lookupUri); + intent.putExtra(EXTRA_TARGET_RECT, target); + intent.putExtra(EXTRA_MODE, mode); + intent.putExtra(EXTRA_EXCLUDE_MIMES, excludeMimes); + context.startActivity(intent); + } + } + + /** + * Contains helper classes used to create or manage {@link android.content.Intent Intents} + * that involve contacts. + */ + public static final class Intents { + /** + * This is the intent that is fired when a search suggestion is clicked on. + */ + public static final String SEARCH_SUGGESTION_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_CLICKED"; + + /** + * This is the intent that is fired when a search suggestion for dialing a number + * is clicked on. + */ + public static final String SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED"; + + /** + * This is the intent that is fired when a search suggestion for creating a contact + * is clicked on. + */ + public static final String SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED = + "android.provider.Contacts.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED"; + + /** + * Starts an Activity that lets the user pick a contact to attach an image to. + * After picking the contact it launches the image cropper in face detection mode. + */ + public static final String ATTACH_IMAGE = + "com.android.contacts.action.ATTACH_IMAGE"; + + /** + * Takes as input a data URI with a mailto: or tel: scheme. If a single + * contact exists with the given data it will be shown. If no contact + * exists, a dialog will ask the user if they want to create a new + * contact with the provided details filled in. If multiple contacts + * share the data the user will be prompted to pick which contact they + * want to view. + *

    + * For mailto: URIs, the scheme specific portion must be a + * raw email address, such as one built using + * {@link Uri#fromParts(String, String, String)}. + *

    + * For tel: URIs, the scheme specific portion is compared + * to existing numbers using the standard caller ID lookup algorithm. + * The number must be properly encoded, for example using + * {@link Uri#fromParts(String, String, String)}. + *

    + * Any extras from the {@link Insert} class will be passed along to the + * create activity if there are no contacts to show. + *

    + * Passing true for the {@link #EXTRA_FORCE_CREATE} extra will skip + * prompting the user when the contact doesn't exist. + */ + public static final String SHOW_OR_CREATE_CONTACT = + "com.android.contacts.action.SHOW_OR_CREATE_CONTACT"; + + /** + * Used with {@link #SHOW_OR_CREATE_CONTACT} to force creating a new + * contact if no matching contact found. Otherwise, default behavior is + * to prompt user with dialog before creating. + *

    + * Type: BOOLEAN + */ + public static final String EXTRA_FORCE_CREATE = + "com.android.contacts.action.FORCE_CREATE"; + + /** + * Used with {@link #SHOW_OR_CREATE_CONTACT} to specify an exact + * description to be shown when prompting user about creating a new + * contact. + *

    + * Type: STRING + */ + public static final String EXTRA_CREATE_DESCRIPTION = + "com.android.contacts.action.CREATE_DESCRIPTION"; + + /** + * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a + * dialog location using screen coordinates. When not specified, the + * dialog will be centered. + * + * @hide + */ + @Deprecated + public static final String EXTRA_TARGET_RECT = "target_rect"; + + /** + * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to specify a + * desired dialog style, usually a variation on size. One of + * {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or {@link #MODE_LARGE}. + * + * @hide + */ + @Deprecated + public static final String EXTRA_MODE = "mode"; + + /** + * Value for {@link #EXTRA_MODE} to show a small-sized dialog. + * + * @hide + */ + @Deprecated + public static final int MODE_SMALL = 1; + + /** + * Value for {@link #EXTRA_MODE} to show a medium-sized dialog. + * + * @hide + */ + @Deprecated + public static final int MODE_MEDIUM = 2; + + /** + * Value for {@link #EXTRA_MODE} to show a large-sized dialog. + * + * @hide + */ + @Deprecated + public static final int MODE_LARGE = 3; + + /** + * Optional extra used with {@link #SHOW_OR_CREATE_CONTACT} to indicate + * a list of specific MIME-types to exclude and not display. Stored as a + * {@link String} array. + * + * @hide + */ + @Deprecated + public static final String EXTRA_EXCLUDE_MIMES = "exclude_mimes"; + + /** + * Intents related to the Contacts app UI. + * + * @hide + */ + public static final class UI { + /** + * The action for the default contacts list tab. + */ + public static final String LIST_DEFAULT = + "com.android.contacts.action.LIST_DEFAULT"; + + /** + * The action for the contacts list tab. + */ + public static final String LIST_GROUP_ACTION = + "com.android.contacts.action.LIST_GROUP"; + + /** + * When in LIST_GROUP_ACTION mode, this is the group to display. + */ + public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP"; + + /** + * The action for the all contacts list tab. + */ + public static final String LIST_ALL_CONTACTS_ACTION = + "com.android.contacts.action.LIST_ALL_CONTACTS"; + + /** + * The action for the contacts with phone numbers list tab. + */ + public static final String LIST_CONTACTS_WITH_PHONES_ACTION = + "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES"; + + /** + * The action for the starred contacts list tab. + */ + public static final String LIST_STARRED_ACTION = + "com.android.contacts.action.LIST_STARRED"; + + /** + * The action for the frequent contacts list tab. + */ + public static final String LIST_FREQUENT_ACTION = + "com.android.contacts.action.LIST_FREQUENT"; + + /** + * The action for the "strequent" contacts list tab. It first lists the starred + * contacts in alphabetical order and then the frequent contacts in descending + * order of the number of times they have been contacted. + */ + public static final String LIST_STREQUENT_ACTION = + "com.android.contacts.action.LIST_STREQUENT"; + + /** + * A key for to be used as an intent extra to set the activity + * title to a custom String value. + */ + public static final String TITLE_EXTRA_KEY = + "com.android.contacts.extra.TITLE_EXTRA"; + + /** + * Activity Action: Display a filtered list of contacts + *

    + * Input: Extra field {@link #FILTER_TEXT_EXTRA_KEY} is the text to use for + * filtering + *

    + * Output: Nothing. + */ + public static final String FILTER_CONTACTS_ACTION = + "com.android.contacts.action.FILTER_CONTACTS"; + + /** + * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION} + * intents to supply the text on which to filter. + */ + public static final String FILTER_TEXT_EXTRA_KEY = + "com.android.contacts.extra.FILTER_TEXT"; + } + + /** + * Convenience class that contains string constants used + * to create contact {@link android.content.Intent Intents}. + */ + public static final class Insert { + /** The action code to use when adding a contact */ + public static final String ACTION = Intent.ACTION_INSERT; + + /** + * If present, forces a bypass of quick insert mode. + */ + public static final String FULL_MODE = "full_mode"; + + /** + * The extra field for the contact name. + *

    Type: String

    + */ + public static final String NAME = "name"; + + // TODO add structured name values here. + + /** + * The extra field for the contact phonetic name. + *

    Type: String

    + */ + public static final String PHONETIC_NAME = "phonetic_name"; + + /** + * The extra field for the contact company. + *

    Type: String

    + */ + public static final String COMPANY = "company"; + + /** + * The extra field for the contact job title. + *

    Type: String

    + */ + public static final String JOB_TITLE = "job_title"; + + /** + * The extra field for the contact notes. + *

    Type: String

    + */ + public static final String NOTES = "notes"; + + /** + * The extra field for the contact phone number. + *

    Type: String

    + */ + public static final String PHONE = "phone"; + + /** + * The extra field for the contact phone number type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a custom label.

    + */ + public static final String PHONE_TYPE = "phone_type"; + + /** + * The extra field for the phone isprimary flag. + *

    Type: boolean

    + */ + public static final String PHONE_ISPRIMARY = "phone_isprimary"; + + /** + * The extra field for an optional second contact phone number. + *

    Type: String

    + */ + public static final String SECONDARY_PHONE = "secondary_phone"; + + /** + * The extra field for an optional second contact phone number type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a custom label.

    + */ + public static final String SECONDARY_PHONE_TYPE = "secondary_phone_type"; + + /** + * The extra field for an optional third contact phone number. + *

    Type: String

    + */ + public static final String TERTIARY_PHONE = "tertiary_phone"; + + /** + * The extra field for an optional third contact phone number type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.PhonesColumns PhonesColumns}, + * or a string specifying a custom label.

    + */ + public static final String TERTIARY_PHONE_TYPE = "tertiary_phone_type"; + + /** + * The extra field for the contact email address. + *

    Type: String

    + */ + public static final String EMAIL = "email"; + + /** + * The extra field for the contact email type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a custom label.

    + */ + public static final String EMAIL_TYPE = "email_type"; + + /** + * The extra field for the email isprimary flag. + *

    Type: boolean

    + */ + public static final String EMAIL_ISPRIMARY = "email_isprimary"; + + /** + * The extra field for an optional second contact email address. + *

    Type: String

    + */ + public static final String SECONDARY_EMAIL = "secondary_email"; + + /** + * The extra field for an optional second contact email type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a custom label.

    + */ + public static final String SECONDARY_EMAIL_TYPE = "secondary_email_type"; + + /** + * The extra field for an optional third contact email address. + *

    Type: String

    + */ + public static final String TERTIARY_EMAIL = "tertiary_email"; + + /** + * The extra field for an optional third contact email type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a custom label.

    + */ + public static final String TERTIARY_EMAIL_TYPE = "tertiary_email_type"; + + /** + * The extra field for the contact postal address. + *

    Type: String

    + */ + public static final String POSTAL = "postal"; + + /** + * The extra field for the contact postal address type. + *

    Type: Either an integer value from + * {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns} + * or a string specifying a custom label.

    + */ + public static final String POSTAL_TYPE = "postal_type"; + + /** + * The extra field for the postal isprimary flag. + *

    Type: boolean

    + */ + public static final String POSTAL_ISPRIMARY = "postal_isprimary"; + + /** + * The extra field for an IM handle. + *

    Type: String

    + */ + public static final String IM_HANDLE = "im_handle"; + + /** + * The extra field for the IM protocol + */ + public static final String IM_PROTOCOL = "im_protocol"; + + /** + * The extra field for the IM isprimary flag. + *

    Type: boolean

    + */ + public static final String IM_ISPRIMARY = "im_isprimary"; + } + } + +} diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java index 4c58e0d951bd80e4e68a203b611561f39a516e4f..790fe5c3af6662af72b771cd691c72b64fa5c267 100644 --- a/core/java/android/provider/Downloads.java +++ b/core/java/android/provider/Downloads.java @@ -63,7 +63,7 @@ public final class Downloads implements BaseColumns { * that had initiated a download when that download completes. The * download's content: uri is specified in the intent's data. */ - public static final String DOWNLOAD_COMPLETED_ACTION = + public static final String ACTION_DOWNLOAD_COMPLETED = "android.intent.action.DOWNLOAD_COMPLETED"; /** @@ -76,7 +76,7 @@ public final class Downloads implements BaseColumns { * Note: this is not currently sent for downloads that have completed * successfully. */ - public static final String NOTIFICATION_CLICKED_ACTION = + public static final String ACTION_NOTIFICATION_CLICKED = "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"; /** @@ -84,14 +84,14 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read

    */ - public static final String URI = "uri"; + public static final String COLUMN_URI = "uri"; /** * The name of the column containing application-specific data. *

    Type: TEXT

    *

    Owner can Init/Read/Write

    */ - public static final String APP_DATA = "entity"; + public static final String COLUMN_APP_DATA = "entity"; /** * The name of the column containing the flags that indicates whether @@ -104,7 +104,7 @@ public final class Downloads implements BaseColumns { *

    Type: BOOLEAN

    *

    Owner can Init

    */ - public static final String NO_INTEGRITY = "no_integrity"; + public static final String COLUMN_NO_INTEGRITY = "no_integrity"; /** * The name of the column containing the filename that the initiating @@ -113,7 +113,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init

    */ - public static final String FILENAME_HINT = "hint"; + public static final String COLUMN_FILE_NAME_HINT = "hint"; /** * The name of the column containing the filename where the downloaded data @@ -128,7 +128,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read

    */ - public static final String MIMETYPE = "mimetype"; + public static final String COLUMN_MIME_TYPE = "mimetype"; /** * The name of the column containing the flag that controls the destination @@ -136,7 +136,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Init

    */ - public static final String DESTINATION = "destination"; + public static final String COLUMN_DESTINATION = "destination"; /** * The name of the column containing the flags that controls whether the @@ -145,7 +145,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Init/Read/Write

    */ - public static final String VISIBILITY = "visibility"; + public static final String COLUMN_VISIBILITY = "visibility"; /** * The name of the column containing the current control state of the download. @@ -154,7 +154,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Read

    */ - public static final String CONTROL = "control"; + public static final String COLUMN_CONTROL = "control"; /** * The name of the column containing the current status of the download. @@ -163,7 +163,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Read

    */ - public static final String STATUS = "status"; + public static final String COLUMN_STATUS = "status"; /** * The name of the column containing the date at which some interesting @@ -172,7 +172,7 @@ public final class Downloads implements BaseColumns { *

    Type: BIGINT

    *

    Owner can Read

    */ - public static final String LAST_MODIFICATION = "lastmod"; + public static final String COLUMN_LAST_MODIFICATION = "lastmod"; /** * The name of the column containing the package name of the application @@ -181,7 +181,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read

    */ - public static final String NOTIFICATION_PACKAGE = "notificationpackage"; + public static final String COLUMN_NOTIFICATION_PACKAGE = "notificationpackage"; /** * The name of the column containing the component name of the class that @@ -191,7 +191,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read

    */ - public static final String NOTIFICATION_CLASS = "notificationclass"; + public static final String COLUMN_NOTIFICATION_CLASS = "notificationclass"; /** * If extras are specified when requesting a download they will be provided in the intent that @@ -199,7 +199,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init

    */ - public static final String NOTIFICATION_EXTRAS = "notificationextras"; + public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; /** * The name of the column contain the values of the cookie to be used for @@ -208,7 +208,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init

    */ - public static final String COOKIE_DATA = "cookiedata"; + public static final String COLUMN_COOKIE_DATA = "cookiedata"; /** * The name of the column containing the user agent that the initiating @@ -216,7 +216,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init

    */ - public static final String USER_AGENT = "useragent"; + public static final String COLUMN_USER_AGENT = "useragent"; /** * The name of the column containing the referer (sic) that the initiating @@ -224,7 +224,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init

    */ - public static final String REFERER = "referer"; + public static final String COLUMN_REFERER = "referer"; /** * The name of the column containing the total size of the file being @@ -232,7 +232,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Read

    */ - public static final String TOTAL_BYTES = "total_bytes"; + public static final String COLUMN_TOTAL_BYTES = "total_bytes"; /** * The name of the column containing the size of the part of the file that @@ -240,7 +240,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Read

    */ - public static final String CURRENT_BYTES = "current_bytes"; + public static final String COLUMN_CURRENT_BYTES = "current_bytes"; /** * The name of the column where the initiating application can provide the @@ -252,7 +252,7 @@ public final class Downloads implements BaseColumns { *

    Type: INTEGER

    *

    Owner can Init

    */ - public static final String OTHER_UID = "otheruid"; + public static final String COLUMN_OTHER_UID = "otheruid"; /** * The name of the column where the initiating application can provided the @@ -261,7 +261,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read/Write

    */ - public static final String TITLE = "title"; + public static final String COLUMN_TITLE = "title"; /** * The name of the column where the initiating application can provide the @@ -270,7 +270,7 @@ public final class Downloads implements BaseColumns { *

    Type: TEXT

    *

    Owner can Init/Read/Write

    */ - public static final String DESCRIPTION = "description"; + public static final String COLUMN_DESCRIPTION = "description"; /* * Lists the destinations that an application can specify for a download. diff --git a/core/java/android/provider/DrmStore.java b/core/java/android/provider/DrmStore.java index db71854c469d26c595054bf0cc0ad7316824f840..c438ac4aa5990215d16abf72cf7f20ef8d62b092 100644 --- a/core/java/android/provider/DrmStore.java +++ b/core/java/android/provider/DrmStore.java @@ -35,7 +35,7 @@ import java.io.OutputStream; /** * The DRM provider contains forward locked DRM content. - * + * * @hide */ public final class DrmStore @@ -43,13 +43,13 @@ public final class DrmStore private static final String TAG = "DrmStore"; public static final String AUTHORITY = "drm"; - + /** * This is in the Manifest class of the drm provider, but that isn't visible * in the framework. */ private static final String ACCESS_DRM_PERMISSION = "android.permission.ACCESS_DRM"; - + /** * Fields for DRM database */ @@ -82,18 +82,18 @@ public final class DrmStore } public interface Images extends Columns { - + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/images"); } - + public interface Audio extends Columns { - + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/audio"); } /** * Utility function for inserting a file into the DRM content provider. - * + * * @param cr The content resolver to use * @param file The file to insert * @param title The title for the content (or null) @@ -101,12 +101,46 @@ public final class DrmStore */ public static final Intent addDrmFile(ContentResolver cr, File file, String title) { FileInputStream fis = null; - OutputStream os = null; Intent result = null; try { fis = new FileInputStream(file); - DrmRawContent content = new DrmRawContent(fis, (int) file.length(), + if (title == null) { + title = file.getName(); + int lastDot = title.lastIndexOf('.'); + if (lastDot > 0) { + title = title.substring(0, lastDot); + } + } + result = addDrmFile(cr, fis, title); + } catch (Exception e) { + Log.e(TAG, "pushing file failed", e); + } finally { + try { + if (fis != null) + fis.close(); + } catch (IOException e) { + Log.e(TAG, "IOException in DrmStore.addDrmFile()", e); + } + } + + return result; + } + + /** + * Utility function for inserting a file stream into the DRM content provider. + * + * @param cr The content resolver to use + * @param fileStream The FileInputStream to insert + * @param title The title for the content (or null) + * @return uri to the DRM record or null + */ + public static final Intent addDrmFile(ContentResolver cr, FileInputStream fis, String title) { + OutputStream os = null; + Intent result = null; + + try { + DrmRawContent content = new DrmRawContent(fis, (int) fis.available(), DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING); String mimeType = content.getContentType(); @@ -126,14 +160,6 @@ public final class DrmStore if (contentUri != null) { ContentValues values = new ContentValues(3); - // compute title from file name, if it is not specified - if (title == null) { - title = file.getName(); - int lastDot = title.lastIndexOf('.'); - if (lastDot > 0) { - title = title.substring(0, lastDot); - } - } values.put(DrmStore.Columns.TITLE, title); values.put(DrmStore.Columns.SIZE, size); values.put(DrmStore.Columns.MIME_TYPE, mimeType); @@ -162,7 +188,7 @@ public final class DrmStore if (os != null) os.close(); } catch (IOException e) { - Log.e(TAG, "IOException in DrmTest.onCreate()", e); + Log.e(TAG, "IOException in DrmStore.addDrmFile()", e); } } @@ -172,7 +198,7 @@ public final class DrmStore /** * Utility function to enforce any permissions required to access DRM * content. - * + * * @param context A context used for checking calling permission. */ public static void enforceAccessDrmPermission(Context context) { @@ -181,5 +207,5 @@ public final class DrmStore throw new SecurityException("Requires DRM permission"); } } - + } diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java index c4b29ae440fafd5b5518b97feaa536bbf7831b4a..073ae6c294411ce3f689d8ed88380918359e248d 100644 --- a/core/java/android/provider/Gmail.java +++ b/core/java/android/provider/Gmail.java @@ -83,7 +83,7 @@ public final class Gmail { public static final String LABEL_OUTBOX = "^^out"; public static final String AUTHORITY = "gmail-ls"; - private static final String TAG = "gmail-ls"; + private static final String TAG = "Gmail"; private static final String AUTHORITY_PLUS_CONVERSATIONS = "content://" + AUTHORITY + "/conversations/"; private static final String AUTHORITY_PLUS_LABELS = @@ -1521,8 +1521,9 @@ public final class Gmail { /** * Returns the number of conversation with a given label. - * @deprecated + * @deprecated Use {@link #getLabelId} instead. */ + @Deprecated public int getNumConversations(String label) { return getNumConversations(getLabelId(label)); } @@ -1534,8 +1535,9 @@ public final class Gmail { /** * Returns the number of unread conversation with a given label. - * @deprecated + * @deprecated Use {@link #getLabelId} instead. */ + @Deprecated public int getNumUnreadConversations(String label) { return getNumUnreadConversations(getLabelId(label)); } @@ -1546,11 +1548,12 @@ public final class Gmail { getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS); // There seems to be a race condition here that can get the label maps into a bad // state and lose state on a particular label. - if (unreadConversations == null) { - return 0; - } else { - return unreadConversations; + int result = 0; + if (unreadConversations != null) { + result = unreadConversations < 0 ? 0 : unreadConversations; } + + return result; } /** @@ -2040,8 +2043,9 @@ public final class Gmail { } /** - * @deprecated + * @deprecated Always returns true. */ + @Deprecated public boolean getExpanded() { return true; } diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java index 19ad15874dcffd16a273ac1ca62e06e2db8e93f8..025d5c266783b82740016b7bbeac217672c542b0 100644 --- a/core/java/android/provider/Im.java +++ b/core/java/android/provider/Im.java @@ -27,7 +27,7 @@ import android.os.Handler; import java.util.HashMap; /** - * The IM provider stores all information about roster contacts, chat messages, presence, etc. + * The GTalk provider stores all information about roster contacts, chat messages, presence, etc. * * @hide */ @@ -38,7 +38,7 @@ public class Im { private Im() {} /** - * The Columns for IM providers (i.e. AIM, Y!, GTalk) + * The Columns for IM providers */ public interface ProviderColumns { /** @@ -138,6 +138,7 @@ public class Im { public static final String ACTIVE_ACCOUNT_USERNAME = "account_username"; public static final String ACTIVE_ACCOUNT_PW = "account_pw"; public static final String ACTIVE_ACCOUNT_LOCKED = "account_locked"; + public static final String ACTIVE_ACCOUNT_KEEP_SIGNED_IN = "account_keepSignedIn"; public static final String ACCOUNT_PRESENCE_STATUS = "account_presenceStatus"; public static final String ACCOUNT_CONNECTION_STATUS = "account_connStatus"; @@ -145,20 +146,20 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/providers"); + Uri.parse("content://com.google.android.providers.talk/providers"); public static final Uri CONTENT_URI_WITH_ACCOUNT = - Uri.parse("content://im/providers/account"); + Uri.parse("content://com.google.android.providers.talk/providers/account"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-providers"; + "vnd.android.cursor.dir/gtalk-providers"; public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-providers"; + "vnd.android.cursor.item/gtalk-providers"; /** * The default sort order for this table @@ -252,21 +253,21 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/accounts"); + Uri.parse("content://com.google.android.providers.talk/accounts"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * account. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-accounts"; + "vnd.android.cursor.dir/gtalk-accounts"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * account. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-accounts"; + "vnd.android.cursor.item/gtalk-accounts"; /** * The default sort order for this table @@ -325,19 +326,19 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/accountStatus"); + Uri.parse("content://com.google.android.providers.talk/accountStatus"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of account status. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-account-status"; + "vnd.android.cursor.dir/gtalk-account-status"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single account status. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-account-status"; + "vnd.android.cursor.item/gtalk-account-status"; /** * The default sort order for this table @@ -521,83 +522,83 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/contacts"); + Uri.parse("content://com.google.android.providers.talk/contacts"); /** * The content:// style URL for contacts joined with presence */ public static final Uri CONTENT_URI_WITH_PRESENCE = - Uri.parse("content://im/contactsWithPresence"); + Uri.parse("content://com.google.android.providers.talk/contactsWithPresence"); /** * The content:// style URL for barebone contacts, not joined with any other table */ public static final Uri CONTENT_URI_CONTACTS_BAREBONE = - Uri.parse("content://im/contactsBarebone"); + Uri.parse("content://com.google.android.providers.talk/contactsBarebone"); /** * The content:// style URL for contacts who have an open chat session */ public static final Uri CONTENT_URI_CHAT_CONTACTS = - Uri.parse("content://im/contacts/chatting"); + Uri.parse("content://com.google.android.providers.talk/contacts_chatting"); /** * The content:// style URL for contacts who have been blocked */ public static final Uri CONTENT_URI_BLOCKED_CONTACTS = - Uri.parse("content://im/contacts/blocked"); + Uri.parse("content://com.google.android.providers.talk/contacts/blocked"); /** * The content:// style URL for contacts by provider and account */ public static final Uri CONTENT_URI_CONTACTS_BY = - Uri.parse("content://im/contacts"); + Uri.parse("content://com.google.android.providers.talk/contacts"); /** * The content:// style URL for contacts by provider and account, * and who have an open chat session */ public static final Uri CONTENT_URI_CHAT_CONTACTS_BY = - Uri.parse("content://im/contacts/chatting"); + Uri.parse("content://com.google.android.providers.talk/contacts/chatting"); /** * The content:// style URL for contacts by provider and account, * and who are online */ public static final Uri CONTENT_URI_ONLINE_CONTACTS_BY = - Uri.parse("content://im/contacts/online"); + Uri.parse("content://com.google.android.providers.talk/contacts/online"); /** * The content:// style URL for contacts by provider and account, * and who are offline */ public static final Uri CONTENT_URI_OFFLINE_CONTACTS_BY = - Uri.parse("content://im/contacts/offline"); + Uri.parse("content://com.google.android.providers.talk/contacts/offline"); /** * The content:// style URL for operations on bulk contacts */ public static final Uri BULK_CONTENT_URI = - Uri.parse("content://im/bulk_contacts"); + Uri.parse("content://com.google.android.providers.talk/bulk_contacts"); /** * The content:// style URL for the count of online contacts in each * contact list by provider and account. */ public static final Uri CONTENT_URI_ONLINE_COUNT = - Uri.parse("content://im/contacts/onlineCount"); + Uri.parse("content://com.google.android.providers.talk/contacts/onlineCount"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-contacts"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-contacts"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. */ - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-contacts"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/gtalk-contacts"; /** * The default sort order for this table @@ -633,21 +634,21 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/contactLists"); + Uri.parse("content://com.google.android.providers.talk/contactLists"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-contactLists"; + "vnd.android.cursor.dir/gtalk-contactLists"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-contactLists"; + "vnd.android.cursor.item/gtalk-contactLists"; /** * The default sort order for this table @@ -698,21 +699,21 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/blockedList"); + Uri.parse("content://com.google.android.providers.talk/blockedList"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-blockedList"; + "vnd.android.cursor.dir/gtalk-blockedList"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-blockedList"; + "vnd.android.cursor.item/gtalk-blockedList"; /** * The default sort order for this table @@ -821,21 +822,21 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/contactsEtag"); + Uri.parse("content://com.google.android.providers.talk/contactsEtag"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-contactsEtag"; + "vnd.android.cursor.dir/gtalk-contactsEtag"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-contactsEtag"; + "vnd.android.cursor.item/gtalk-contactsEtag"; } /** @@ -871,14 +872,22 @@ public class Im { } /** - * The common columns for both one-to-one chat messages or group chat messages. + * The common columns for messages table */ - public interface BaseMessageColumns { + public interface MessageColumns { /** - * The user this message belongs to - *

    Type: TEXT

    + * The thread_id column stores the contact id of the contact the message belongs to. + * For groupchat messages, the thread_id stores the group id, which is the contact id + * of the temporary group contact created for the groupchat. So there should be no + * collision between groupchat message thread id and regular message thread id. */ - String CONTACT = "contact"; + String THREAD_ID = "thread_id"; + + /** + * The nickname. This is used for groupchat messages to indicate the participant's + * nickname. For non groupchat messages, this field should be left empty. + */ + String NICKNAME = "nickname"; /** * The body @@ -887,11 +896,20 @@ public class Im { String BODY = "body"; /** - * The date this message is sent or received + * The date this message is sent or received. This represents the display date for + * the message. *

    Type: INTEGER

    */ String DATE = "date"; + /** + * The real date for this message. While 'date' can be modified by the client + * to account for server time skew, the real_date is the original timestamp set + * by the server for incoming messages. + *

    Type: INTEGER

    + */ + String REAL_DATE = "real_date"; + /** * Message Type, see {@link MessageType} *

    Type: INTEGER

    @@ -917,81 +935,217 @@ public class Im { *

    Type: STRING

    */ String PACKET_ID = "packet_id"; - } - /** - * Columns from the Messages table. - */ - public interface MessagesColumns extends BaseMessageColumns{ /** - * The provider id - *

    Type: INTEGER

    + * Is groupchat message or not + *

    Type: INTEGER

    */ - String PROVIDER = "provider"; + String IS_GROUP_CHAT = "is_muc"; /** - * The account id - *

    Type: INTEGER

    + * A hint that the UI should show the sent time of this message + *

    Type: INTEGER

    */ - String ACCOUNT = "account"; + String DISPLAY_SENT_TIME = "show_ts"; } /** * This table contains messages. */ - public static final class Messages implements BaseColumns, MessagesColumns { + public static final class Messages implements BaseColumns, MessageColumns { /** * no public constructor since this is a utility class */ private Messages() {} /** - * Gets the Uri to query messages by contact. + * Gets the Uri to query messages by thread id. + * + * @param threadId the thread id of the message. + * @return the Uri + */ + public static final Uri getContentUriByThreadId(long threadId) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_THREAD_ID.buildUpon(); + ContentUris.appendId(builder, threadId); + return builder.build(); + } + + /** + * @deprecated + * + * Gets the Uri to query messages by account and contact. * - * @param providerId the provider id of the contact. * @param accountId the account id of the contact. * @param username the user name of the contact. * @return the Uri */ - public static final Uri getContentUriByContact(long providerId, - long accountId, String username) { - Uri.Builder builder = CONTENT_URI_MESSAGES_BY.buildUpon(); + public static final Uri getContentUriByContact(long accountId, String username) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT.buildUpon(); + ContentUris.appendId(builder, accountId); + builder.appendPath(username); + return builder.build(); + } + + /** + * Gets the Uri to query messages by provider. + * + * @param providerId the service provider id. + * @return the Uri + */ + public static final Uri getContentUriByProvider(long providerId) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_PROVIDER.buildUpon(); ContentUris.appendId(builder, providerId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by account. + * + * @param accountId the account id. + * @return the Uri + */ + public static final Uri getContentUriByAccount(long accountId) { + Uri.Builder builder = CONTENT_URI_BY_ACCOUNT.buildUpon(); + ContentUris.appendId(builder, accountId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by thread id. + * + * @param threadId the thread id of the message. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByThreadId(long threadId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID.buildUpon(); + ContentUris.appendId(builder, threadId); + return builder.build(); + } + + /** + * @deprecated + * + * Gets the Uri to query off the record messages by account and contact. + * + * @param accountId the account id of the contact. + * @param username the user name of the contact. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByContact(long accountId, String username) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT.buildUpon(); ContentUris.appendId(builder, accountId); builder.appendPath(username); return builder.build(); } + /** + * Gets the Uri to query off the record messages by provider. + * + * @param providerId the service provider id. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByProvider(long providerId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_PROVIDER.buildUpon(); + ContentUris.appendId(builder, providerId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by account. + * + * @param accountId the account id. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByAccount(long accountId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT.buildUpon(); + ContentUris.appendId(builder, accountId); + return builder.build(); + } + /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/messages"); + Uri.parse("content://com.google.android.providers.talk/messages"); + + /** + * The content:// style URL for messages by thread id + */ + public static final Uri CONTENT_URI_MESSAGES_BY_THREAD_ID = + Uri.parse("content://com.google.android.providers.talk/messagesByThreadId"); + + /** + * The content:// style URL for messages by account and contact + */ + public static final Uri CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT = + Uri.parse("content://com.google.android.providers.talk/messagesByAcctAndContact"); + + /** + * The content:// style URL for messages by provider + */ + public static final Uri CONTENT_URI_MESSAGES_BY_PROVIDER = + Uri.parse("content://com.google.android.providers.talk/messagesByProvider"); + + /** + * The content:// style URL for messages by account + */ + public static final Uri CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://com.google.android.providers.talk/messagesByAccount"); + + /** + * The content:// style url for off the record messages + */ + public static final Uri OTR_MESSAGES_CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/otrMessages"); + + /** + * The content:// style url for off the record messages by thread id + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID = + Uri.parse("content://com.google.android.providers.talk/otrMessagesByThreadId"); + + /** + * The content:// style url for off the record messages by account and contact + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT = + Uri.parse("content://com.google.android.providers.talk/otrMessagesByAcctAndContact"); + + /** + * The content:// style URL for off the record messages by provider + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_PROVIDER = + Uri.parse("content://com.google.android.providers.talk/otrMessagesByProvider"); /** - * The content:// style URL for messages by provider and account + * The content:// style URL for off the record messages by account */ - public static final Uri CONTENT_URI_MESSAGES_BY = - Uri.parse("content://im/messagesBy"); + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://com.google.android.providers.talk/otrMessagesByAccount"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-messages"; + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/gtalk-messages"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * person. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-messages"; + "vnd.android.cursor.item/gtalk-messages"; /** * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = "date ASC"; + /** + * The "contact" column. This is not a real column in the messages table, but a + * temoprary column created when querying for messages (joined with the contacts table) + */ + public static final String CONTACT = "contact"; } /** @@ -1021,21 +1175,21 @@ public class Im { private GroupMembers(){} public static final Uri CONTENT_URI = - Uri.parse("content://im/groupMembers"); + Uri.parse("content://com.google.android.providers.talk/groupMembers"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * group members. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-groupMembers"; + "vnd.android.cursor.dir/gtalk-groupMembers"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * group member. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-groupMembers"; + "vnd.android.cursor.item/gtalk-groupMembers"; } /** @@ -1101,82 +1255,21 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/invitations"); + Uri.parse("content://com.google.android.providers.talk/invitations"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * invitations. */ public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/im-invitations"; + "vnd.android.cursor.dir/gtalk-invitations"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single * invitation. */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-invitations"; - } - - /** - * Columns from the GroupMessages table - */ - public interface GroupMessageColumns extends BaseMessageColumns { - /** - * The group this message belongs to - *

    Type: TEXT

    - */ - String GROUP = "groupId"; - } - - /** - * This table contains group messages. - */ - public final static class GroupMessages implements BaseColumns, - GroupMessageColumns { - private GroupMessages() {} - - /** - * Gets the Uri to query group messages by group. - * - * @param groupId the group id. - * @return the Uri - */ - public static final Uri getContentUriByGroup(long groupId) { - Uri.Builder builder = CONTENT_URI_GROUP_MESSAGES_BY.buildUpon(); - ContentUris.appendId(builder, groupId); - return builder.build(); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://im/groupMessages"); - - /** - * The content:// style URL for group messages by provider and account - */ - public static final Uri CONTENT_URI_GROUP_MESSAGES_BY = - Uri.parse("content://im/groupMessagesBy"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * group messages. - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-groupMessages"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * group message. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-groupMessages"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "date ASC"; + "vnd.android.cursor.item/gtalk-invitations"; } /** @@ -1218,24 +1311,25 @@ public class Im { /** * The content:// style URL for this table */ - public static final Uri CONTENT_URI = Uri.parse("content://im/avatars"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/avatars"); /** * The content:// style URL for avatars by provider, account and contact */ public static final Uri CONTENT_URI_AVATARS_BY = - Uri.parse("content://im/avatarsBy"); + Uri.parse("content://com.google.android.providers.talk/avatarsBy"); /** * The MIME type of {@link #CONTENT_URI} providing the avatars */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-avatars"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-avatars"; /** * The MIME type of a {@link #CONTENT_URI} */ public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-avatars"; + "vnd.android.cursor.item/gtalk-avatars"; /** * The default sort order for this table @@ -1313,28 +1407,31 @@ public class Im { /** * The content:// style URL for this table */ - public static final Uri CONTENT_URI = Uri.parse("content://im/presence"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/presence"); /** - * The content URL for IM presences for an account + * The content URL for Talk presences for an account */ - public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/presence/account"); + public static final Uri CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://com.google.android.providers.talk/presence/account"); /** * The content:// style URL for operations on bulk contacts */ - public static final Uri BULK_CONTENT_URI = Uri.parse("content://im/bulk_presence"); + public static final Uri BULK_CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/bulk_presence"); /** * The content:// style URL for seeding presences for a given account id. */ public static final Uri SEED_PRESENCE_BY_ACCOUNT_CONTENT_URI = - Uri.parse("content://im/seed_presence/account"); + Uri.parse("content://com.google.android.providers.talk/seed_presence/account"); /** * The MIME type of a {@link #CONTENT_URI} providing a directory of presence */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-presence"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-presence"; /** * The default sort order for this table @@ -1384,7 +1481,7 @@ public class Im { *

    Type: TEXT

    */ String UNSENT_COMPOSED_MESSAGE = "unsent_composed_message"; - + /** * A value from 0-9 indicating which quick-switch chat screen slot this * chat is occupying. If none (for instance, this is the 12th active chat) @@ -1407,22 +1504,23 @@ public class Im { * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/chats"); + Uri.parse("content://com.google.android.providers.talk/chats"); /** * The content URL for all chats that belong to the account */ - public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/chats/account"); + public static final Uri CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://com.google.android.providers.talk/chats/account"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of chats. */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-chats"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-chats"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single chat. */ - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-chats"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/gtalk-chats"; /** * The default sort order for this table @@ -1450,19 +1548,20 @@ public class Im { /** * The content:// style URI for this table */ - public static final Uri CONTENT_URI = Uri.parse("content://im/sessionCookies"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/sessionCookies"); /** * The content:// style URL for session cookies by provider and account */ public static final Uri CONTENT_URI_SESSION_COOKIES_BY = - Uri.parse("content://im/sessionCookiesBy"); + Uri.parse("content://com.google.android.providers.talk/sessionCookiesBy"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ - public static final String CONTENT_TYPE = "vnd.android-dir/im-sessionCookies"; + public static final String CONTENT_TYPE = "vnd.android-dir/gtalk-sessionCookies"; } /** @@ -1497,12 +1596,12 @@ public class Im { * The content:// style URI for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/providerSettings"); + Uri.parse("content://com.google.android.providers.talk/providerSettings"); /** * The MIME type of {@link #CONTENT_URI} providing provider settings */ - public static final String CONTENT_TYPE = "vnd.android-dir/im-providerSettings"; + public static final String CONTENT_TYPE = "vnd.android-dir/gtalk-providerSettings"; /** * A boolean value to indicate whether this provider should show the offline contacts @@ -1512,13 +1611,13 @@ public class Im { /** controls whether or not the GTalk service automatically connect to server. */ public static final String SETTING_AUTOMATICALLY_CONNECT_GTALK = "gtalk_auto_connect"; - /** controls whether or not the IM service will be automatically started after boot */ + /** controls whether or not the GTalk service will be automatically started after boot */ public static final String SETTING_AUTOMATICALLY_START_SERVICE = "auto_start_service"; /** controls whether or not the offline contacts will be hided */ public static final String SETTING_HIDE_OFFLINE_CONTACTS = "hide_offline_contacts"; - /** controls whether or not enable the IM notification */ + /** controls whether or not enable the GTalk notification */ public static final String SETTING_ENABLE_NOTIFICATION = "enable_notification"; /** specifies whether or not to vibrate */ @@ -1534,6 +1633,18 @@ public class Im { /** specifies whether or not to show mobile indicator to friends */ public static final String SETTING_SHOW_MOBILE_INDICATOR = "mobile_indicator"; + /** specifies whether or not to show as away when device is idle */ + public static final String SETTING_SHOW_AWAY_ON_IDLE = "show_away_on_idle"; + + /** specifies whether or not to upload heartbeat stat upon login */ + public static final String SETTING_UPLOAD_HEARTBEAT_STAT = "upload_heartbeat_stat"; + + /** specifies the last heartbeat interval received from the server */ + public static final String SETTING_HEARTBEAT_INTERVAL = "heartbeat_interval"; + + /** specifiy the JID resource used for Google Talk connection */ + public static final String SETTING_JID_RESOURCE = "jid_resource"; + /** * Used for reliable message queue (RMQ). This is for storing the last rmq id received * from the GTalk server @@ -1698,10 +1809,10 @@ public class Im { } /** - * A convenience method to set whether or not enable the IM notification. + * A convenience method to set whether or not enable the GTalk notification. * * @param contentResolver The ContentResolver to use to access the setting table. - * @param enable Whether enable the IM notification + * @param enable Whether enable the GTalk notification */ public static void setEnableNotification(ContentResolver contentResolver, long providerId, boolean enable) { @@ -1742,6 +1853,47 @@ public class Im { showMobileIndicator); } + /** + * A convenience method to set whether or not to show as away when device is idle. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param showAway Whether or not to show as away when device is idle. + */ + public static void setShowAwayOnIdle(ContentResolver contentResolver, + long providerId, boolean showAway) { + putBooleanValue(contentResolver, providerId, SETTING_SHOW_AWAY_ON_IDLE, showAway); + } + + /** + * A convenience method to set whether or not to upload heartbeat stat. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param uploadStat Whether or not to upload heartbeat stat. + */ + public static void setUploadHeartbeatStat(ContentResolver contentResolver, + long providerId, boolean uploadStat) { + putBooleanValue(contentResolver, providerId, SETTING_UPLOAD_HEARTBEAT_STAT, uploadStat); + } + + /** + * A convenience method to set the heartbeat interval last received from the server. + * + * @param contentResolver The ContentResolver to use to access the setting table. + * @param interval The heartbeat interval last received from the server. + */ + public static void setHeartbeatInterval(ContentResolver contentResolver, + long providerId, long interval) { + putLongValue(contentResolver, providerId, SETTING_HEARTBEAT_INTERVAL, interval); + } + + /** + * A convenience method to set the jid resource. + */ + public static void setJidResource(ContentResolver contentResolver, + long providerId, String jidResource) { + putStringValue(contentResolver, providerId, SETTING_JID_RESOURCE, jidResource); + } + public static class QueryMap extends ContentQueryMap { private ContentResolver mContentResolver; private long mProviderId; @@ -1798,18 +1950,18 @@ public class Im { } /** - * Set whether or not enable the IM notification. + * Set whether or not enable the GTalk notification. * - * @param enable Whether or not enable the IM notification. + * @param enable Whether or not enable the GTalk notification. */ public void setEnableNotification(boolean enable) { ProviderSettings.setEnableNotification(mContentResolver, mProviderId, enable); } /** - * Check if the IM notification is enabled. + * Check if the GTalk notification is enabled. * - * @return Whether or not enable the IM notification. + * @return Whether or not enable the GTalk notification. */ public boolean getEnableNotification() { return getBoolean(SETTING_ENABLE_NOTIFICATION, @@ -1817,7 +1969,7 @@ public class Im { } /** - * Set whether or not to vibrate on IM notification. + * Set whether or not to vibrate on GTalk notification. * * @param vibrate Whether or not to vibrate. */ @@ -1826,7 +1978,7 @@ public class Im { } /** - * Gets whether or not to vibrate on IM notification. + * Gets whether or not to vibrate on GTalk notification. * * @return Whether or not to vibrate. */ @@ -1871,6 +2023,79 @@ public class Im { true /* by default show mobile indicator */); } + /** + * Set whether or not to show as away when device is idle. + * + * @param showAway whether or not to show as away when device is idle. + */ + public void setShowAwayOnIdle(boolean showAway) { + ProviderSettings.setShowAwayOnIdle(mContentResolver, mProviderId, showAway); + } + + /** + * Get whether or not to show as away when device is idle. + * + * @return Whether or not to show as away when device is idle. + */ + public boolean getShowAwayOnIdle() { + return getBoolean(SETTING_SHOW_AWAY_ON_IDLE, + true /* by default show as away on idle*/); + } + + /** + * Set whether or not to upload heartbeat stat. + * + * @param uploadStat whether or not to upload heartbeat stat. + */ + public void setUploadHeartbeatStat(boolean uploadStat) { + ProviderSettings.setUploadHeartbeatStat(mContentResolver, mProviderId, uploadStat); + } + + /** + * Get whether or not to upload heartbeat stat. + * + * @return Whether or not to upload heartbeat stat. + */ + public boolean getUploadHeartbeatStat() { + return getBoolean(SETTING_UPLOAD_HEARTBEAT_STAT, + false /* by default do not upload */); + } + + /** + * Set the last received heartbeat interval from the server. + * + * @param interval the last received heartbeat interval from the server. + */ + public void setHeartbeatInterval(long interval) { + ProviderSettings.setHeartbeatInterval(mContentResolver, mProviderId, interval); + } + + /** + * Get the last received heartbeat interval from the server. + * + * @return the last received heartbeat interval from the server. + */ + public long getHeartbeatInterval() { + return getLong(SETTING_HEARTBEAT_INTERVAL, 0L /* an invalid default interval */); + } + + /** + * Set the JID resource. + * + * @param jidResource the jid resource to be stored. + */ + public void setJidResource(String jidResource) { + ProviderSettings.setJidResource(mContentResolver, mProviderId, jidResource); + } + /** + * Get the JID resource used for the Google Talk connection + * + * @return the JID resource stored. + */ + public String getJidResource() { + return getString(SETTING_JID_RESOURCE, null); + } + /** * Convenience function for retrieving a single settings value * as a boolean. @@ -1909,21 +2134,78 @@ public class Im { ContentValues values = getValues(name); return values != null ? values.getAsInteger(VALUE) : def; } + + /** + * Convenience function for retrieving a single settings value + * as a Long. + * + * @param name The name of the setting to retrieve. + * @param def The value to return if the setting is not defined. + * @return The setting's current value or 'def' if it is not defined. + */ + private long getLong(String name, long def) { + ContentValues values = getValues(name); + return values != null ? values.getAsLong(VALUE) : def; + } } } + + /** + * Columns for GTalk branding resource map cache table. This table caches the result of + * loading the branding resources to speed up GTalk landing page start. + */ + public interface BrandingResourceMapCacheColumns { + /** + * The provider ID + *

    Type: INTEGER

    + */ + String PROVIDER_ID = "provider_id"; + /** + * The application resource ID + *

    Type: INTEGER

    + */ + String APP_RES_ID = "app_res_id"; + /** + * The plugin resource ID + *

    Type: INTEGER

    + */ + String PLUGIN_RES_ID = "plugin_res_id"; + } + + /** + * The table for caching the result of loading GTalk branding resources. + */ + public static final class BrandingResourceMapCache + implements BaseColumns, BrandingResourceMapCacheColumns { + /** + * The content:// style URL for this table. + */ + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/brandingResMapCache"); + } + + + + /** + * //TODO: move these to MCS specific provider. + * The following are MCS stuff, and should really live in a separate provider specific to + * MCS code. + */ + /** * Columns from OutgoingRmq table */ public interface OutgoingRmqColumns { String RMQ_ID = "rmq_id"; - String TYPE = "type"; String TIMESTAMP = "ts"; String DATA = "data"; + String PROTOBUF_TAG = "type"; } /** + * //TODO: we should really move these to their own provider and database. * The table for storing outgoing rmq packets. */ public static final class OutgoingRmq implements BaseColumns, OutgoingRmqColumns { @@ -1963,13 +2245,14 @@ public class Im { /** * The content:// style URL for this table. */ - public static final Uri CONTENT_URI = Uri.parse("content://im/outgoingRmqMessages"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/outgoingRmqMessages"); /** * The content:// style URL for the highest rmq id for the outgoing rmq messages */ public static final Uri CONTENT_URI_FOR_HIGHEST_RMQ_ID = - Uri.parse("content://im/outgoingHighestRmqId"); + Uri.parse("content://com.google.android.providers.talk/outgoingHighestRmqId"); /** * The default sort order for this table. @@ -1986,6 +2269,7 @@ public class Im { } /** + * //TODO: move these out into their own provider and database * The table for storing the last client rmq id sent to the server. */ public static final class LastRmqId implements BaseColumns, LastRmqIdColumns { @@ -2042,39 +2326,27 @@ public class Im { /** * The content:// style URL for this table. */ - public static final Uri CONTENT_URI = Uri.parse("content://im/lastRmqId"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/lastRmqId"); } /** - * Columns for IM branding resource map cache table. This table caches the result of - * loading the branding resources to speed up IM landing page start. + * Columns for the s2dRmqIds table, which stores the server-to-device message + * persistent ids. These are used in the RMQ2 protocol, where in the login request, the + * client selective acks these s2d ids to the server. */ - public interface BrandingResourceMapCacheColumns { - /** - * The provider ID - *

    Type: INTEGER

    - */ - String PROVIDER_ID = "provider_id"; - /** - * The application resource ID - *

    Type: INTEGER

    - */ - String APP_RES_ID = "app_res_id"; - /** - * The plugin resource ID - *

    Type: INTEGER

    - */ - String PLUGIN_RES_ID = "plugin_res_id"; + public interface ServerToDeviceRmqIdsColumn { + String RMQ_ID = "rmq_id"; } - /** - * The table for caching the result of loading IM branding resources. - */ - public static final class BrandingResourceMapCache - implements BaseColumns, BrandingResourceMapCacheColumns { + public static final class ServerToDeviceRmqIds implements BaseColumns, + ServerToDeviceRmqIdsColumn { + /** * The content:// style URL for this table. */ - public static final Uri CONTENT_URI = Uri.parse("content://im/brandingResMapCache"); + public static final Uri CONTENT_URI = + Uri.parse("content://com.google.android.providers.talk/s2dids"); } + } diff --git a/core/java/android/provider/LiveFolders.java b/core/java/android/provider/LiveFolders.java index 6e95fb7fc115fd2cf83d31ad68329a574b029be6..7856bab016cbd7043cae9f42c3473b14c6869772 100644 --- a/core/java/android/provider/LiveFolders.java +++ b/core/java/android/provider/LiveFolders.java @@ -40,12 +40,11 @@ import android.annotation.SdkConstant; * to retrieve the folder's content.

    * *

    Setting up the live folder activity

    - *

    The following code sample shows how to write an activity that creates a live fodler:

    + *

    The following code sample shows how to write an activity that creates a live folder:

    *
      * public static class MyLiveFolder extends Activity {
      *     public static final Uri CONTENT_URI = Uri.parse("content://my.app/live");
      *
    - *     @Override
      *     protected void onCreate(Bundle savedInstanceState) {
      *         super.onCreate(savedInstanceState);
      *
    diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
    index 21e5865fef70129748942da94cf852f4098df41a..062080d6665d7dee10b3f176afd8cd9961920419 100644
    --- a/core/java/android/provider/MediaStore.java
    +++ b/core/java/android/provider/MediaStore.java
    @@ -23,11 +23,15 @@ import android.content.ContentValues;
     import android.content.ContentUris;
     import android.database.Cursor;
     import android.database.DatabaseUtils;
    +import android.database.sqlite.SQLiteException;
     import android.graphics.Bitmap;
     import android.graphics.BitmapFactory;
     import android.graphics.Matrix;
    +import android.media.MiniThumbFile;
    +import android.media.ThumbnailUtil;
     import android.net.Uri;
     import android.os.Environment;
    +import android.os.ParcelFileDescriptor;
     import android.util.Log;
     
     import java.io.FileInputStream;
    @@ -42,8 +46,7 @@ import java.text.Collator;
      * The Media provider contains meta data for all available media on both internal
      * and external storage devices.
      */
    -public final class MediaStore
    -{
    +public final class MediaStore {
         private final static String TAG = "MediaStore";
     
         public static final String AUTHORITY = "media";
    @@ -163,6 +166,12 @@ public final class MediaStore
          */
         public final static String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit";
     
    +    /**
    +     * Specify the maximum allowed recording duration in seconds.
    +     * @hide
    +     */
    +    public final static String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
    +
         /**
          * The name of the Intent-extra used to indicate a content resolver Uri to be used to
          * store the requested image or video.
    @@ -173,7 +182,7 @@ public final class MediaStore
          * Common fields for most MediaProvider tables
          */
     
    -     public interface MediaColumns extends BaseColumns {
    +    public interface MediaColumns extends BaseColumns {
             /**
              * The data stream for the file
              * 

    Type: DATA STREAM

    @@ -220,11 +229,155 @@ public final class MediaStore public static final String MIME_TYPE = "mime_type"; } + /** + * This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended + * to be accessed elsewhere. + */ + private static class InternalThumbnails implements BaseColumns { + private static final int MINI_KIND = 1; + private static final int FULL_SCREEN_KIND = 2; + private static final int MICRO_KIND = 3; + private static final String[] PROJECTION = new String[] {_ID, MediaColumns.DATA}; + + /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original image or video id. use -1 to cancel all requests. + * @param baseUri the base URI of requested thumbnails + */ + static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri) { + Uri cancelUri = baseUri.buildUpon().appendQueryParameter("cancel", "1") + .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + Cursor c = null; + try { + c = cr.query(cancelUri, PROJECTION, null, null, null); + } + finally { + if (c != null) c.close(); + } + } + /** + * This method ensure thumbnails associated with origId are generated and decode the byte + * stream from database (MICRO_KIND) or file (MINI_KIND). + * + * Special optimization has been done to avoid further IPC communication for MICRO_KIND + * thumbnails. + * + * @param cr ContentResolver + * @param origId original image or video id + * @param kind could be MINI_KIND or MICRO_KIND + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @param baseUri the base URI of requested thumbnails + * @return Bitmap bitmap of specified thumbnail kind + */ + static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + BitmapFactory.Options options, Uri baseUri, boolean isVideo) { + Bitmap bitmap = null; + String filePath = null; + // Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo); + // some optimization for MICRO_KIND: if the magic is non-zero, we don't bother + // querying MediaProvider and simply return thumbnail. + if (kind == MICRO_KIND) { + MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); + if (thumbFile.getMagic(origId) != 0) { + byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; + if (thumbFile.getMiniThumbFromFile(origId, data) != null) { + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + if (bitmap == null) { + Log.w(TAG, "couldn't decode byte array."); + } + } + return bitmap; + } + } + + Cursor c = null; + try { + Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1") + .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + c = cr.query(blockingUri, PROJECTION, null, null, null); + // This happens when original image/video doesn't exist. + if (c == null) return null; + + // Assuming thumbnail has been generated, at least original image exists. + if (kind == MICRO_KIND) { + MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); + byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; + if (thumbFile.getMiniThumbFromFile(origId, data) != null) { + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + if (bitmap == null) { + Log.w(TAG, "couldn't decode byte array."); + } + } + } else if (kind == MINI_KIND) { + if (c.moveToFirst()) { + ParcelFileDescriptor pfdInput; + Uri thumbUri = null; + try { + long thumbId = c.getLong(0); + filePath = c.getString(1); + thumbUri = ContentUris.withAppendedId(baseUri, thumbId); + pfdInput = cr.openFileDescriptor(thumbUri, "r"); + bitmap = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + pfdInput.close(); + } catch (FileNotFoundException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (IOException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "failed to allocate memory for thumbnail " + + thumbUri + "; " + ex); + } + } + } else { + throw new IllegalArgumentException("Unsupported kind: " + kind); + } + + // We probably run out of space, so create the thumbnail in memory. + if (bitmap == null) { + Log.v(TAG, "We probably run out of space, so create the thumbnail in memory."); + + Uri uri = Uri.parse( + baseUri.buildUpon().appendPath(String.valueOf(origId)) + .toString().replaceFirst("thumbnails", "media")); + if (filePath == null) { + if (c != null) c.close(); + c = cr.query(uri, PROJECTION, null, null, null); + if (c == null || !c.moveToFirst()) { + return null; + } + filePath = c.getString(1); + } + if (isVideo) { + bitmap = ThumbnailUtil.createVideoThumbnail(filePath); + if (kind == MICRO_KIND) { + bitmap = ThumbnailUtil.extractMiniThumb(bitmap, + ThumbnailUtil.MINI_THUMB_TARGET_SIZE, + ThumbnailUtil.MINI_THUMB_TARGET_SIZE, + ThumbnailUtil.RECYCLE_INPUT); + } + } else { + bitmap = ThumbnailUtil.createImageThumbnail(cr, filePath, uri, origId, + kind, false); + } + } + } catch (SQLiteException ex) { + Log.w(TAG, ex); + } finally { + if (c != null) c.close(); + } + return bitmap; + } + } + /** * Contains meta data for all available images. */ - public static final class Images - { + public static final class Images { public interface ImageColumns extends MediaColumns { /** * The description of the image @@ -292,21 +445,18 @@ public final class MediaStore } public static final class Media implements ImageColumns { - public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) - { + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) { return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); } public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, - String where, String orderBy) - { + String where, String orderBy) { return cr.query(uri, projection, where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } public static final Cursor query(ContentResolver cr, Uri uri, String[] projection, - String selection, String [] selectionArgs, String orderBy) - { + String selection, String [] selectionArgs, String orderBy) { return cr.query(uri, projection, selection, selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } @@ -320,8 +470,7 @@ public final class MediaStore * @throws IOException */ public static final Bitmap getBitmap(ContentResolver cr, Uri url) - throws FileNotFoundException, IOException - { + throws FileNotFoundException, IOException { InputStream input = cr.openInputStream(url); Bitmap bitmap = BitmapFactory.decodeStream(input); input.close(); @@ -338,9 +487,8 @@ public final class MediaStore * @return The URL to the newly created image * @throws FileNotFoundException */ - public static final String insertImage(ContentResolver cr, String imagePath, String name, - String description) throws FileNotFoundException - { + public static final String insertImage(ContentResolver cr, String imagePath, + String name, String description) throws FileNotFoundException { // Check if file exists with a FileInputStream FileInputStream stream = new FileInputStream(imagePath); try { @@ -409,8 +557,7 @@ public final class MediaStore * for any reason. */ public static final String insertImage(ContentResolver cr, Bitmap source, - String title, String description) - { + String title, String description) { ContentValues values = new ContentValues(); values.put(Images.Media.TITLE, title); values.put(Images.Media.DESCRIPTION, description); @@ -419,8 +566,7 @@ public final class MediaStore Uri url = null; String stringUrl = null; /* value to be returned */ - try - { + try { url = cr.insert(EXTERNAL_CONTENT_URI, values); if (source != null) { @@ -490,27 +636,59 @@ public final class MediaStore * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME; - } + } - public static class Thumbnails implements BaseColumns - { - public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) - { + /** + * This class allows developers to query and get two kinds of thumbnails: + * MINI_KIND: 512 x 384 thumbnail + * MICRO_KIND: 96 x 96 thumbnail + */ + public static class Thumbnails implements BaseColumns { + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) { return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); } - public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, String[] projection) - { + public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, + String[] projection) { return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER); } - public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, String[] projection) - { + public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, + String[] projection) { return cr.query(EXTERNAL_CONTENT_URI, projection, IMAGE_ID + " = " + origId + " AND " + KIND + " = " + kind, null, null); } + /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original image id + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image + * associated with origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, kind, options, + EXTERNAL_CONTENT_URI, false); + } + /** * Get the content:// style URI for the image media table on the * given volume. @@ -562,6 +740,11 @@ public final class MediaStore public static final int MINI_KIND = 1; public static final int FULL_SCREEN_KIND = 2; public static final int MICRO_KIND = 3; + /** + * The blob raw data of thumbnail + *

    Type: DATA STREAM

    + */ + public static final String THUMB_DATA = "thumb_data"; /** * The width of the thumbnal @@ -1176,7 +1359,7 @@ public final class MediaStore *

    Type: INTEGER

    */ public static final String FIRST_YEAR = "minyear"; - + /** * The year in which the latest songs * on this album were released. This will often @@ -1253,8 +1436,7 @@ public final class MediaStore */ public static final String DEFAULT_SORT_ORDER = MediaColumns.DISPLAY_NAME; - public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) - { + public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) { return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER); } @@ -1399,6 +1581,107 @@ public final class MediaStore */ public static final String DEFAULT_SORT_ORDER = TITLE; } + + /** + * This class allows developers to query and get two kinds of thumbnails: + * MINI_KIND: 512 x 384 thumbnail + * MICRO_KIND: 96 x 96 thumbnail + * + */ + public static class Thumbnails implements BaseColumns { + /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original video id + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image associated with + * origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, kind, options, + EXTERNAL_CONTENT_URI, true); + } + + /** + * Get the content:// style URI for the image media table on the + * given volume. + * + * @param volumeName the name of the volume to get the URI for + * @return the URI to the image media table on the given volume + */ + public static Uri getContentUri(String volumeName) { + return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + + "/video/thumbnails"); + } + + /** + * The content:// style URI for the internal storage. + */ + public static final Uri INTERNAL_CONTENT_URI = + getContentUri("internal"); + + /** + * The content:// style URI for the "primary" external storage + * volume. + */ + public static final Uri EXTERNAL_CONTENT_URI = + getContentUri("external"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "video_id ASC"; + + /** + * The data stream for the thumbnail + *

    Type: DATA STREAM

    + */ + public static final String DATA = "_data"; + + /** + * The original image for the thumbnal + *

    Type: INTEGER (ID from Video table)

    + */ + public static final String VIDEO_ID = "video_id"; + + /** + * The kind of the thumbnail + *

    Type: INTEGER (One of the values below)

    + */ + public static final String KIND = "kind"; + + public static final int MINI_KIND = 1; + public static final int FULL_SCREEN_KIND = 2; + public static final int MICRO_KIND = 3; + + /** + * The width of the thumbnal + *

    Type: INTEGER (long)

    + */ + public static final String WIDTH = "width"; + + /** + * The height of the thumbnail + *

    Type: INTEGER (long)

    + */ + public static final String HEIGHT = "height"; + } } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4a4d2de92dcc89549471df64a03113d183691e80..cb3dc166f7a4b01e2a1c89cbd3a44e7cf4238692 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -118,6 +118,20 @@ public final class Settings { public static final String ACTION_AIRPLANE_MODE_SETTINGS = "android.settings.AIRPLANE_MODE_SETTINGS"; + /** + * Activity Action: Show settings for accessibility modules. + *

    + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + *

    + * Input: Nothing. + *

    + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ACCESSIBILITY_SETTINGS = + "android.settings.ACCESSIBILITY_SETTINGS"; + /** * Activity Action: Show settings to allow configuration of security and * location privacy. @@ -133,6 +147,20 @@ public final class Settings { public static final String ACTION_SECURITY_SETTINGS = "android.settings.SECURITY_SETTINGS"; + /** + * Activity Action: Show settings to allow configuration of privacy options. + *

    + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + *

    + * Input: Nothing. + *

    + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PRIVACY_SETTINGS = + "android.settings.PRIVACY_SETTINGS"; + /** * Activity Action: Show settings to allow configuration of Wi-Fi. @@ -413,8 +441,6 @@ public final class Settings { private static final String TAG = "Settings"; - private static String sJidResource = null; - public static class SettingNotFoundException extends AndroidException { public SettingNotFoundException(String msg) { super(msg); @@ -438,7 +464,7 @@ public final class Settings { resolver.insert(uri, values); return true; } catch (SQLException e) { - Log.e(TAG, "Can't set key " + name + " in " + uri, e); + Log.w(TAG, "Can't set key " + name + " in " + uri, e); return false; } } @@ -475,7 +501,7 @@ public final class Settings { mValues.put(name, value); } catch (SQLException e) { // SQL error: return null, but don't cache it. - Log.e(TAG, "Can't get key " + name + " from " + mUri, e); + Log.w(TAG, "Can't get key " + name + " from " + mUri, e); } finally { if (c != null) c.close(); } @@ -878,6 +904,17 @@ public final class Settings { */ public static final String AIRPLANE_MODE_RADIOS = "airplane_mode_radios"; + /** + * A comma separated list of radios that should to be disabled when airplane mode + * is on, but can be manually reenabled by the user. For example, if RADIO_WIFI is + * added to both AIRPLANE_MODE_RADIOS and AIRPLANE_MODE_TOGGLEABLE_RADIOS, then Wifi + * will be turned off when entering airplane mode, but the user will be able to reenable + * Wifi in the Settings app. + * + * {@hide} + */ + public static final String AIRPLANE_MODE_TOGGLEABLE_RADIOS = "airplane_mode_toggleable_radios"; + /** * The policy for deciding when Wi-Fi should go to sleep (which will in * turn switch to using the mobile data as an Internet connection). @@ -1037,6 +1074,24 @@ public final class Settings { */ public static final String SCREEN_BRIGHTNESS = "screen_brightness"; + /** + * Control whether to enable automatic brightness mode. + * @hide + */ + public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode"; + + /** + * SCREEN_BRIGHTNESS_MODE value for manual mode. + * @hide + */ + public static final int SCREEN_BRIGHTNESS_MODE_MANUAL = 0; + + /** + * SCREEN_BRIGHTNESS_MODE value for manual mode. + * @hide + */ + public static final int SCREEN_BRIGHTNESS_MODE_AUTOMATIC = 1; + /** * Control whether the process CPU usage meter should be shown. */ @@ -1181,6 +1236,22 @@ public final class Settings { */ public static final Uri DEFAULT_NOTIFICATION_URI = getUriFor(NOTIFICATION_SOUND); + /** + * Persistent store for the system-wide default alarm alert. + * + * @see #RINGTONE + * @see #DEFAULT_ALARM_ALERT_URI + */ + public static final String ALARM_ALERT = "alarm_alert"; + + /** + * A {@link Uri} that will point to the current default alarm alert at + * any given time. + * + * @see #DEFAULT_ALARM_ALERT_URI + */ + public static final Uri DEFAULT_ALARM_ALERT_URI = getUriFor(ALARM_ALERT); + /** * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off */ @@ -1357,6 +1428,7 @@ public final class Settings { DIM_SCREEN, SCREEN_OFF_TIMEOUT, SCREEN_BRIGHTNESS, + SCREEN_BRIGHTNESS_MODE, VIBRATE_ON, NOTIFICATIONS_USE_RING_VOLUME, MODE_RINGER, @@ -1497,18 +1569,17 @@ public final class Settings { @Deprecated public static final String USE_GOOGLE_MAIL = Secure.USE_GOOGLE_MAIL; -// /** -// * @deprecated Use {@link android.provider.Settings.Secure#WIFI_MAX_DHCP_RETRY_COUNT} -// * instead -// */ + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_MAX_DHCP_RETRY_COUNT} instead + */ @Deprecated public static final String WIFI_MAX_DHCP_RETRY_COUNT = Secure.WIFI_MAX_DHCP_RETRY_COUNT; -// /** -// * @deprecated Use -// * {@link android.provider.Settings.Secure#WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS} -// * instead -// */ + /** + * @deprecated Use + * {@link android.provider.Settings.Secure#WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS} instead + */ @Deprecated public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS = Secure.WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS; @@ -1967,6 +2038,12 @@ public final class Settings { */ public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; + /** + * Whether assisted GPS should be enabled or not. + * @hide + */ + public static final String ASSISTED_GPS_ENABLED = "assisted_gps_enabled"; + /** * The Logging ID (a unique 64-bit value) as a hex string. * Used as a pseudonymous identifier for logging. @@ -2200,6 +2277,32 @@ public final class Settings { */ public static final String BACKGROUND_DATA = "background_data"; + /** + * The time in msec, when the LAST_KMSG file was send to the checkin server. + * We will only send the LAST_KMSG file if it was modified after this time. + * + * @hide + */ + public static final String CHECKIN_SEND_LAST_KMSG_TIME = "checkin_kmsg_time"; + + /** + * The time in msec, when the apanic_console file was send to the checkin server. + * We will only send the apanic_console file if it was modified after this time. + * + * @hide + */ + public static final String CHECKIN_SEND_APANIC_CONSOLE_TIME = + "checkin_apanic_console_time"; + + /** + * The time in msec, when the apanic_thread file was send to the checkin server. + * We will only send the apanic_thread file if it was modified after this time. + * + * @hide + */ + public static final String CHECKIN_SEND_APANIC_THREAD_TIME = + "checkin_apanic_thread_time"; + /** * The CDMA roaming mode 0 = Home Networks, CDMA default * 1 = Roaming on Affiliated networks @@ -2314,7 +2417,6 @@ public final class Settings { public static final String[] SETTINGS_TO_BACKUP = { ADB_ENABLED, ALLOW_MOCK_LOCATION, - INSTALL_NON_MARKET_APPS, PARENTAL_CONTROL_ENABLED, PARENTAL_CONTROL_REDIRECT_URL, USB_MASS_STORAGE_ENABLED, @@ -2330,12 +2432,6 @@ public final class Settings { WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, WIFI_NUM_ALLOWED_CHANNELS, WIFI_NUM_OPEN_NETWORKS_KEPT, - BACKGROUND_DATA, - PREFERRED_NETWORK_MODE, - PREFERRED_TTY_MODE, - CDMA_CELL_BROADCAST_SMS, - PREFERRED_CDMA_SUBSCRIPTION, - ENHANCED_VOICE_PRIVACY_ENABLED }; /** @@ -2687,15 +2783,30 @@ public final class Settings { /** * Controls whether Gmail will discard uphill operations that repeatedly fail. Value must be - * an integer where non-zero means true. Defaults to 1. + * an integer where non-zero means true. Defaults to 1. This flag controls Donut devices. */ public static final String GMAIL_DISCARD_ERROR_UPHILL_OP = "gmail_discard_error_uphill_op"; + /** + * Controls whether Gmail will discard uphill operations that repeatedly fail. Value must be + * an integer where non-zero means true. Defaults to 1. This flag controls Eclair and + * future devices. + */ + public static final String GMAIL_DISCARD_ERROR_UPHILL_OP_NEW = + "gmail_discard_error_uphill_op_new"; + /** * Controls how many attempts Gmail will try to upload an uphill operations before it * abandons the operation. Defaults to 20. */ - public static final String GMAIL_NUM_RETRY_UPHILL_OP = "gmail_discard_error_uphill_op"; + public static final String GMAIL_NUM_RETRY_UPHILL_OP = "gmail_num_retry_uphill_op"; + + /** + * How much time in seconds Gmail will try to upload an uphill operations before it + * abandons the operation. Defaults to 36400 (one day). + */ + public static final String GMAIL_WAIT_TIME_RETRY_UPHILL_OP = + "gmail_wait_time_retry_uphill_op"; /** * Controls if the protocol buffer version of the protocol will use a multipart request for @@ -2803,6 +2914,12 @@ public final class Settings { public static final String GTALK_SERVICE_NOSYNC_HEARTBEAT_INTERVAL_MS = "gtalk_nosync_heartbeat_ping_interval_ms"; + /** + * The maximum heartbeat interval used while on the WIFI network. + */ + public static final String GTALK_SERVICE_WIFI_MAX_HEARTBEAT_INTERVAL_MS = + "gtalk_wifi_max_heartbeat_ping_interval_ms"; + /** * How long we wait to receive a heartbeat ping acknowledgement (or another packet) * from the GTalk server, before deeming the connection dead. @@ -2855,6 +2972,113 @@ public final class Settings { */ public static final String GTALK_USE_BARE_JID_TIMEOUT_MS = "gtalk_use_barejid_timeout_ms"; + /** + * This is the threshold of retry number when there is an authentication expired failure + * for Google Talk. In some situation, e.g. when a Google Apps account is disabled chat + * service, the connection keeps failing. This threshold controls when we should stop + * the retrying. + */ + public static final String GTALK_MAX_RETRIES_FOR_AUTH_EXPIRED = + "gtalk_max_retries_for_auth_expired"; + + /** + * a boolean setting indicating whether the GTalkService should use RMQ2 protocol or not. + */ + public static final String GTALK_USE_RMQ2_PROTOCOL = + "gtalk_use_rmq2"; + + /** + * a boolean setting indicating whether the GTalkService should support both RMQ and + * RMQ2 protocols. This setting is true for the transitional period when we need to + * support both protocols. + */ + public static final String GTALK_SUPPORT_RMQ_AND_RMQ2_PROTOCOLS = + "gtalk_support_rmq_and_rmq2"; + + /** + * a boolean setting controlling whether the rmq2 protocol will include stream ids in + * the protobufs. This is used for debugging. + */ + public static final String GTALK_RMQ2_INCLUDE_STREAM_ID = + "gtalk_rmq2_include_stream_id"; + + /** + * when receiving a chat message from the server, the message could be an older message + * whose "time sent" is x seconds from now. If x is significant enough, we want to flag + * it so the UI can give it some special treatment when displaying the "time sent" for + * it. This setting is to control what x is. + */ + public static final String GTALK_OLD_CHAT_MESSAGE_THRESHOLD_IN_SEC = + "gtalk_old_chat_msg_threshold_in_sec"; + + /** + * a setting to control the max connection history record GTalkService stores. + */ + public static final String GTALK_MAX_CONNECTION_HISTORY_RECORDS = + "gtalk_max_conn_history_records"; + + /** + * This is gdata url to lookup album and picture info from picasa web. It also controls + * whether url scraping for picasa is enabled (NULL to disable). + */ + public static final String GTALK_PICASA_ALBUM_URL = + "gtalk_picasa_album_url"; + + /** + * This is the url to lookup picture info from flickr. It also controls + * whether url scraping for flickr is enabled (NULL to disable). + */ + public static final String GTALK_FLICKR_PHOTO_INFO_URL = + "gtalk_flickr_photo_info_url"; + + /** + * This is the url to lookup an actual picture from flickr. + */ + public static final String GTALK_FLICKR_PHOTO_URL = + "gtalk_flickr_photo_url"; + + /** + * This is the gdata url to lookup info on a youtube video. It also controls + * whether url scraping for youtube is enabled (NULL to disable). + */ + public static final String GTALK_YOUTUBE_VIDEO_URL = + "gtalk_youtube_video_url"; + + /** + * Enable/disable GTalk URL scraping for JPG images ("true" to enable). + */ + public static final String GTALK_URL_SCRAPING_FOR_JPG = + "gtalk_url_scraping_for_jpg"; + + /** + * Chat message lifetime (for pruning old chat messages). + */ + public static final String GTALK_CHAT_MESSAGE_LIFETIME = + "gtalk_chat_message_lifetime"; + + /** + * OTR message lifetime (for pruning old otr messages). + */ + public static final String GTALK_OTR_MESSAGE_LIFETIME = + "gtalk_otr_message_lifetime"; + + /** + * Chat expiration time, i.e., time since last message in the chat (for pruning old chats). + */ + public static final String GTALK_CHAT_EXPIRATION_TIME = + "gtalk_chat_expiration_time"; + + /** + * This is the url for getting the app token for server-to-device push messaging. + */ + public static final String PUSH_MESSAGING_REGISTRATION_URL = + "push_messaging_registration_url"; + + /** + * Use android://<it> routing infos for Google Sync Server subcriptions. + */ + public static final String GSYNC_USE_RMQ2_ROUTING_INFO = "gsync_use_rmq2_routing_info"; + /** * Enable use of ssl session caching. * 'db' - save each session in a (per process) database @@ -2906,6 +3130,12 @@ public final class Settings { public static final String VENDING_REQUIRE_SIM_FOR_PURCHASE = "vending_require_sim_for_purchase"; + /** + * Indicates the Vending Machine backup state. It is set if the + * Vending application has been backed up at least once. + */ + public static final String VENDING_BACKUP_STATE = "vending_backup_state"; + /** * The current version id of the Vending Machine terms of service. */ @@ -2972,6 +3202,13 @@ public final class Settings { public static final String VENDING_PENDING_DOWNLOAD_RESEND_FREQUENCY_MS = "vending_pd_resend_frequency_ms"; + /** + * Time before an asset in the 'DOWNLOADING' state is considered ready + * for an install kick on the client. + */ + public static final String VENDING_DOWNLOADING_KICK_TIMEOUT_MS = + "vending_downloading_kick_ms"; + /** * Size of buffer in bytes for Vending to use when reading cache files. */ @@ -2991,6 +3228,25 @@ public final class Settings { public static final String VENDING_PROMO_REFRESH_FREQUENCY_MS = "vending_promo_refresh_freq_ms"; + /** + * Frequency in milliseconds when we should refresh the provisioning information from + * the carrier backend. + */ + public static final String VENDING_CARRIER_PROVISIONING_REFRESH_FREQUENCY_MS = + "vending_carrier_ref_freq_ms"; + + /** + * Interval in milliseconds after which a failed provisioning request should be retried. + */ + public static final String VENDING_CARRIER_PROVISIONING_RETRY_MS = + "vending_carrier_prov_retry_ms"; + + /** + * Buffer in milliseconds for carrier credentials to be considered valid. + */ + public static final String VENDING_CARRIER_CREDENTIALS_BUFFER_MS = + "vending_carrier_cred_buf_ms"; + /** * URL that points to the legal terms of service to display in Settings. *

    @@ -3224,39 +3480,6 @@ public final class Settings { public static final String SHORT_KEYLIGHT_DELAY_MS = "short_keylight_delay_ms"; - /** - * URL that points to the voice search servers. To be factored out of this class. - */ - public static final String VOICE_SEARCH_URL = "voice_search_url"; - - /** - * Speech encoding used with voice search on 3G networks. To be factored out of this class. - */ - public static final String VOICE_SEARCH_ENCODING_THREE_G = "voice_search_encoding_three_g"; - - /** - * Speech encoding used with voice search on WIFI networks. To be factored out of this class. - */ - public static final String VOICE_SEARCH_ENCODING_WIFI = "voice_search_encoding_wifi"; - - /** - * Whether to use automatic gain control in voice search (0 = disable, 1 = enable). - * To be factored out of this class. - */ - public static final String VOICE_SEARCH_ENABLE_AGC = "voice_search_enable_agc"; - - /** - * Whether to use noise suppression in voice search (0 = disable, 1 = enable). - * To be factored out of this class. - */ - public static final String VOICE_SEARCH_ENABLE_NS = "voice_search_enable_ns"; - - /** - * Whether to use the IIR filter in voice search (0 = disable, 1 = enable). - * To be factored out of this class. - */ - public static final String VOICE_SEARCH_ENABLE_IIR = "voice_search_enable_iir"; - /** * List of test suites (local disk filename) for the automatic instrumentation test runner. * The file format is similar to automated_suites.xml, see AutoTesterService. @@ -3299,6 +3522,121 @@ public final class Settings { */ public static final String USE_LOCATION_FOR_SERVICES = "use_location"; + /** + * The length of the calendar sync window into the future. + * This specifies the number of days into the future for the sliding window sync. + * Setting this to zero will disable sliding sync. + */ + public static final String GOOGLE_CALENDAR_SYNC_WINDOW_DAYS = + "google_calendar_sync_window_days"; + + /** + * How often to update the calendar sync window. + * The window will be advanced every n days. + */ + public static final String GOOGLE_CALENDAR_SYNC_WINDOW_UPDATE_DAYS = + "google_calendar_sync_window_update_days"; + + /** + * The number of promoted sources in GlobalSearch. + */ + public static final String SEARCH_NUM_PROMOTED_SOURCES = "search_num_promoted_sources"; + /** + * The maximum number of suggestions returned by GlobalSearch. + */ + public static final String SEARCH_MAX_RESULTS_TO_DISPLAY = "search_max_results_to_display"; + /** + * The number of suggestions GlobalSearch will ask each non-web search source for. + */ + public static final String SEARCH_MAX_RESULTS_PER_SOURCE = "search_max_results_per_source"; + /** + * The number of suggestions the GlobalSearch will ask the web search source for. + */ + public static final String SEARCH_WEB_RESULTS_OVERRIDE_LIMIT = + "search_web_results_override_limit"; + /** + * The number of milliseconds that GlobalSearch will wait for suggestions from + * promoted sources before continuing with all other sources. + */ + public static final String SEARCH_PROMOTED_SOURCE_DEADLINE_MILLIS = + "search_promoted_source_deadline_millis"; + /** + * The number of milliseconds before GlobalSearch aborts search suggesiton queries. + */ + public static final String SEARCH_SOURCE_TIMEOUT_MILLIS = "search_source_timeout_millis"; + /** + * The maximum number of milliseconds that GlobalSearch shows the previous results + * after receiving a new query. + */ + public static final String SEARCH_PREFILL_MILLIS = "search_prefill_millis"; + /** + * The maximum age of log data used for shortcuts in GlobalSearch. + */ + public static final String SEARCH_MAX_STAT_AGE_MILLIS = "search_max_stat_age_millis"; + /** + * The maximum age of log data used for source ranking in GlobalSearch. + */ + public static final String SEARCH_MAX_SOURCE_EVENT_AGE_MILLIS = + "search_max_source_event_age_millis"; + /** + * The minimum number of impressions needed to rank a source in GlobalSearch. + */ + public static final String SEARCH_MIN_IMPRESSIONS_FOR_SOURCE_RANKING = + "search_min_impressions_for_source_ranking"; + /** + * The minimum number of clicks needed to rank a source in GlobalSearch. + */ + public static final String SEARCH_MIN_CLICKS_FOR_SOURCE_RANKING = + "search_min_clicks_for_source_ranking"; + /** + * The maximum number of shortcuts shown by GlobalSearch. + */ + public static final String SEARCH_MAX_SHORTCUTS_RETURNED = "search_max_shortcuts_returned"; + /** + * The size of the core thread pool for suggestion queries in GlobalSearch. + */ + public static final String SEARCH_QUERY_THREAD_CORE_POOL_SIZE = + "search_query_thread_core_pool_size"; + /** + * The maximum size of the thread pool for suggestion queries in GlobalSearch. + */ + public static final String SEARCH_QUERY_THREAD_MAX_POOL_SIZE = + "search_query_thread_max_pool_size"; + /** + * The size of the core thread pool for shortcut refreshing in GlobalSearch. + */ + public static final String SEARCH_SHORTCUT_REFRESH_CORE_POOL_SIZE = + "search_shortcut_refresh_core_pool_size"; + /** + * The maximum size of the thread pool for shortcut refreshing in GlobalSearch. + */ + public static final String SEARCH_SHORTCUT_REFRESH_MAX_POOL_SIZE = + "search_shortcut_refresh_max_pool_size"; + /** + * The maximun time that excess threads in the GlobalSeach thread pools will + * wait before terminating. + */ + public static final String SEARCH_THREAD_KEEPALIVE_SECONDS = + "search_thread_keepalive_seconds"; + /** + * The maximum number of concurrent suggestion queries to each source. + */ + public static final String SEARCH_PER_SOURCE_CONCURRENT_QUERY_LIMIT = + "search_per_source_concurrent_query_limit"; + + /** + * Flag for allowing ActivityManagerService to send ACTION_APP_ERROR intents + * on application crashes and ANRs. If this is disabled, the crash/ANR dialog + * will never display the "Report" button. + * Type: int ( 0 = disallow, 1 = allow ) + */ + public static final String SEND_ACTION_APP_ERROR = "send_action_app_error"; + + /** + * Maximum size of /proc/last_kmsg content to upload after reboot. + */ + public static final String LAST_KMSG_KB = "last_kmsg_kb"; + /** * @deprecated * @hide @@ -3425,7 +3763,7 @@ public final class Settings { // The stored URL is bad... ignore it. } catch (IllegalArgumentException e) { // Column not found - Log.e(TAG, "Intent column not found", e); + Log.w(TAG, "Intent column not found", e); } } } finally { @@ -3541,42 +3879,6 @@ public final class Settings { } } - /** - * Returns the GTalk JID resource associated with this device. - * - * @return String the JID resource of the device. It uses the device IMEI in the computation - * of the JID resource. If IMEI is not ready (i.e. telephony module not ready), we'll return - * an empty string. - * @hide - */ - // TODO: we shouldn't not have a permenant Jid resource, as that's an easy target for - // spams. We should change it once a while, like when we resubscribe to the subscription feeds - // server. - // (also, should this live in GTalkService?) - public static synchronized String getJidResource() { - if (sJidResource != null) { - return sJidResource; - } - - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("this should never happen"); - } - - String deviceId = TelephonyManager.getDefault().getDeviceId(); - if (TextUtils.isEmpty(deviceId)) { - return ""; - } - - byte[] hashedDeviceId = digest.digest(deviceId.getBytes()); - String id = new String(Base64.encodeBase64(hashedDeviceId), 0, 12); - id = id.replaceAll("/", "_"); - sJidResource = JID_RESOURCE_PREFIX + id; - return sJidResource; - } - /** * Returns the device ID that we should use when connecting to the mobile gtalk server. * This is a string like "android-0x1242", where the hex string is the Android ID obtained diff --git a/core/java/android/provider/SocialContract.java b/core/java/android/provider/SocialContract.java new file mode 100644 index 0000000000000000000000000000000000000000..ee271ba84481f8a55277037b88ea8b6207f3cfb2 --- /dev/null +++ b/core/java/android/provider/SocialContract.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.provider; + +import android.content.res.Resources; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; + +/** + * The contract between the social provider and applications. Contains + * definitions for the supported URIs and columns. + * + * @hide + */ +public class SocialContract { + /** The authority for the social provider */ + public static final String AUTHORITY = "com.android.social"; + + /** A content:// style uri to the authority for the contacts provider */ + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + private interface ActivitiesColumns { + /** + * The package name to use when creating {@link Resources} objects for + * this data row. This value is only designed for use when building user + * interfaces, and should not be used to infer the owner. + *

    + * Type: TEXT + */ + public static final String RES_PACKAGE = "res_package"; + + /** + * The mime-type of this social activity. + *

    + * Type: TEXT + */ + public static final String MIMETYPE = "mimetype"; + + /** + * Internal raw identifier for this social activity. This field is + * analogous to the atom:id element defined in RFC 4287. + *

    + * Type: TEXT + */ + public static final String RAW_ID = "raw_id"; + + /** + * Reference to another {@link Activities#RAW_ID} that this social activity + * is replying to. This field is analogous to the + * thr:in-reply-to element defined in RFC 4685. + *

    + * Type: TEXT + */ + public static final String IN_REPLY_TO = "in_reply_to"; + + /** + * Reference to the {@link android.provider.ContactsContract.Contacts#_ID} that authored + * this social activity. This field is analogous to the atom:author + * element defined in RFC 4287. + *

    + * Type: INTEGER + */ + public static final String AUTHOR_CONTACT_ID = "author_contact_id"; + + /** + * Optional reference to the {@link android.provider.ContactsContract.Contacts#_ID} this + * social activity is targeted towards. If more than one direct target, this field may + * be left undefined. This field is analogous to the + * activity:target element defined in the Atom Activity + * Extensions Internet-Draft. + *

    + * Type: INTEGER + */ + public static final String TARGET_CONTACT_ID = "target_contact_id"; + + /** + * Timestamp when this social activity was published, in a + * {@link System#currentTimeMillis()} time base. This field is analogous + * to the atom:published element defined in RFC 4287. + *

    + * Type: INTEGER + */ + public static final String PUBLISHED = "published"; + + /** + * Timestamp when the original social activity in a thread was + * published. For activities that have an in-reply-to field specified, the + * content provider will automatically populate this field with the + * timestamp of the original activity. + *

    + * This field is useful for sorting order of activities that keeps together all + * messages in each thread. + *

    + * Type: INTEGER + */ + public static final String THREAD_PUBLISHED = "thread_published"; + + /** + * Title of this social activity. This field is analogous to the + * atom:title element defined in RFC 4287. + *

    + * Type: TEXT + */ + public static final String TITLE = "title"; + + /** + * Summary of this social activity. This field is analogous to the + * atom:summary element defined in RFC 4287. + *

    + * Type: TEXT + */ + public static final String SUMMARY = "summary"; + + /** + * A URI associated this social activity. This field is analogous to the + * atom:link rel="alternate" element defined in RFC 4287. + *

    + * Type: TEXT + */ + public static final String LINK = "link"; + + /** + * Optional thumbnail specific to this social activity. This is the raw + * bytes of an image that could be inflated using {@link BitmapFactory}. + *

    + * Type: BLOB + */ + public static final String THUMBNAIL = "thumbnail"; + } + + public static final class Activities implements BaseColumns, ActivitiesColumns { + /** + * This utility class cannot be instantiated + */ + private Activities() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "activities"); + + /** + * The content:// URI for this table filtered to the set of social activities + * authored by a specific {@link android.provider.ContactsContract.Contacts#_ID}. + */ + public static final Uri CONTENT_AUTHORED_BY_URI = + Uri.withAppendedPath(CONTENT_URI, "authored_by"); + + /** + * The {@link Uri} for the latest social activity performed by any + * raw contact aggregated under the specified {@link Contacts#_ID}. Will + * also join with most-present {@link Presence} for this aggregate. + */ + public static final Uri CONTENT_CONTACT_STATUS_URI = + Uri.withAppendedPath(AUTHORITY_URI, "contact_status"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of social + * activities. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/activity"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * social activity. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/activity"; + } + +} diff --git a/core/java/android/provider/SubscribedFeeds.java b/core/java/android/provider/SubscribedFeeds.java index 4d430d5fd91a0e36e944d242155852d17c58b947..8e9f4021c934f5d40af7ba247c5a72468d9056dd 100644 --- a/core/java/android/provider/SubscribedFeeds.java +++ b/core/java/android/provider/SubscribedFeeds.java @@ -20,6 +20,7 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.accounts.Account; /** * The SubscribedFeeds provider stores all information about subscribed feeds. @@ -99,7 +100,7 @@ public class SubscribedFeeds { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; } /** @@ -114,38 +115,36 @@ public class SubscribedFeeds { * @return the Uri of the feed that was added */ public static Uri addFeed(ContentResolver resolver, - String feed, String account, + String feed, Account account, String authority, String service) { ContentValues values = new ContentValues(); values.put(SubscribedFeeds.Feeds.FEED, feed); - values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account); + values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account.name); + values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, account.type); values.put(SubscribedFeeds.Feeds.AUTHORITY, authority); values.put(SubscribedFeeds.Feeds.SERVICE, service); return resolver.insert(SubscribedFeeds.Feeds.CONTENT_URI, values); } public static int deleteFeed(ContentResolver resolver, - String feed, String account, String authority) { + String feed, Account account, String authority) { StringBuilder where = new StringBuilder(); where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.FEED + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account, feed, authority}); + where.toString(), new String[] {account.name, account.type, feed, authority}); } public static int deleteFeeds(ContentResolver resolver, - String account, String authority) { + Account account, String authority) { StringBuilder where = new StringBuilder(); where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account, authority}); - } - - public static String gtalkServiceRoutingInfoFromAccountAndResource( - String account, String res) { - return Uri.parse("gtalk://" + account + "/" + res).toString(); + where.toString(), new String[] {account.name, account.type, authority}); } /** @@ -157,6 +156,12 @@ public class SubscribedFeeds { *

    Type: TEXT

    */ public static final String _SYNC_ACCOUNT = SyncConstValue._SYNC_ACCOUNT; + + /** + * The account type. + *

    Type: TEXT

    + */ + public static final String _SYNC_ACCOUNT_TYPE = SyncConstValue._SYNC_ACCOUNT_TYPE; } /** @@ -199,6 +204,6 @@ public class SubscribedFeeds { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; } } diff --git a/core/java/android/provider/SyncConstValue.java b/core/java/android/provider/SyncConstValue.java index 6eb4398444de7701335759a94dea32c39c8a70e9..30966eb6f63dc820721c5839932aa0be36fe4c7c 100644 --- a/core/java/android/provider/SyncConstValue.java +++ b/core/java/android/provider/SyncConstValue.java @@ -28,6 +28,12 @@ public interface SyncConstValue */ public static final String _SYNC_ACCOUNT = "_sync_account"; + /** + * The type of the account that was used to sync the entry to the device. + *

    Type: TEXT

    + */ + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + /** * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. *

    Type: TEXT

    @@ -68,4 +74,9 @@ public interface SyncConstValue * Used to indicate that this account is not synced */ public static final String NON_SYNCABLE_ACCOUNT = "non_syncable"; + + /** + * Used to indicate that this account is not synced + */ + public static final String NON_SYNCABLE_ACCOUNT_TYPE = "android.local"; } diff --git a/core/java/android/provider/SyncStateContract.java b/core/java/android/provider/SyncStateContract.java new file mode 100644 index 0000000000000000000000000000000000000000..e8177ca51572f95ba700402b51c8b9706633837b --- /dev/null +++ b/core/java/android/provider/SyncStateContract.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.provider; + +import android.net.Uri; +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.accounts.Account; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Pair; + +/** + * The ContentProvider contract for associating data with ana data array account. + * This may be used by providers that want to store this data in a standard way. + */ +public class SyncStateContract { + public interface Columns extends BaseColumns { + /** + * A reference to the name of the account to which this data belongs + *

    Type: STRING

    + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * A reference to the type of the account to which this data belongs + *

    Type: STRING

    + */ + public static final String ACCOUNT_TYPE = "account_type"; + + /** + * The sync data associated with this account. + *

    Type: NONE

    + */ + public static final String DATA = "data"; + } + + public static class Constants implements Columns { + public static final String CONTENT_DIRECTORY = "syncstate"; + } + + public static final class Helpers { + private static final String[] DATA_PROJECTION = new String[]{Columns.DATA, Columns._ID}; + private static final String SELECT_BY_ACCOUNT = + Columns.ACCOUNT_NAME + "=? AND " + Columns.ACCOUNT_TYPE + "=?"; + + /** + * Get the sync state that is associated with the account or null. + * @param provider the {@link ContentProviderClient} that is to be used to communicate + * with the {@link android.content.ContentProvider} that contains the sync state. + * @param uri the uri of the sync state + * @param account the {@link Account} whose sync state should be returned + * @return the sync state or null if there is no sync state associated with the account + * @throws RemoteException if there is a failure communicating with the remote + * {@link android.content.ContentProvider} + */ + public static byte[] get(ContentProviderClient provider, Uri uri, + Account account) throws RemoteException { + Cursor c = provider.query(uri, DATA_PROJECTION, SELECT_BY_ACCOUNT, + new String[]{account.name, account.type}, null); + try { + if (c.moveToNext()) { + return c.getBlob(c.getColumnIndexOrThrow(Columns.DATA)); + } + } finally { + c.close(); + } + return null; + } + + /** + * Assigns the data array as the sync state for the given account. + * @param provider the {@link ContentProviderClient} that is to be used to communicate + * with the {@link android.content.ContentProvider} that contains the sync state. + * @param uri the uri of the sync state + * @param account the {@link Account} whose sync state should be set + * @param data the byte[] that contains the sync state + * @throws RemoteException if there is a failure communicating with the remote + * {@link android.content.ContentProvider} + */ + public static void set(ContentProviderClient provider, Uri uri, + Account account, byte[] data) throws RemoteException { + ContentValues values = new ContentValues(); + values.put(Columns.DATA, data); + values.put(Columns.ACCOUNT_NAME, account.name); + values.put(Columns.ACCOUNT_TYPE, account.type); + provider.insert(uri, values); + } + + public static Uri insert(ContentProviderClient provider, Uri uri, + Account account, byte[] data) throws RemoteException { + ContentValues values = new ContentValues(); + values.put(Columns.DATA, data); + values.put(Columns.ACCOUNT_NAME, account.name); + values.put(Columns.ACCOUNT_TYPE, account.type); + return provider.insert(uri, values); + } + + public static void update(ContentProviderClient provider, Uri uri, byte[] data) + throws RemoteException { + ContentValues values = new ContentValues(); + values.put(Columns.DATA, data); + provider.update(uri, values, null, null); + } + + public static Pair getWithUri(ContentProviderClient provider, Uri uri, + Account account) throws RemoteException { + Cursor c = provider.query(uri, DATA_PROJECTION, SELECT_BY_ACCOUNT, + new String[]{account.name, account.type}, null); + try { + if (c.moveToNext()) { + long rowId = c.getLong(1); + byte[] blob = c.getBlob(c.getColumnIndexOrThrow(Columns.DATA)); + return Pair.create(ContentUris.withAppendedId(uri, rowId), blob); + } + } finally { + c.close(); + } + return null; + } + + /** + * Creates and returns a ContentProviderOperation that assigns the data array as the + * sync state for the given account. + * @param uri the uri of the sync state + * @param account the {@link Account} whose sync state should be set + * @param data the byte[] that contains the sync state + * @return the new ContentProviderOperation that assigns the data array as the + * account's sync state + */ + public static ContentProviderOperation newSetOperation(Uri uri, + Account account, byte[] data) { + ContentValues values = new ContentValues(); + values.put(Columns.DATA, data); + return ContentProviderOperation + .newInsert(uri) + .withValue(Columns.ACCOUNT_NAME, account.name) + .withValue(Columns.ACCOUNT_TYPE, account.type) + .withValues(values) + .build(); + } + + /** + * Creates and returns a ContentProviderOperation that assigns the data array as the + * sync state for the given account. + * @param uri the uri of the specific sync state to set + * @param data the byte[] that contains the sync state + * @return the new ContentProviderOperation that assigns the data array as the + * account's sync state + */ + public static ContentProviderOperation newUpdateOperation(Uri uri, byte[] data) { + ContentValues values = new ContentValues(); + values.put(Columns.DATA, data); + return ContentProviderOperation + .newUpdate(uri) + .withValues(values) + .build(); + } + } +} diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index c292c537e696132b7e5bb2e3b51f7c9716af8b18..d8c5a53771a7af9f73273dd788db1fc3ed4f674e 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -146,7 +146,13 @@ public final class Telephony { *

    Type: TEXT

    */ public static final String SERVICE_CENTER = "service_center"; - } + + /** + * Has the message been locked? + *

    Type: INTEGER (boolean)

    + */ + public static final String LOCKED = "locked"; +} /** * Contains all text based SMS messages. @@ -483,6 +489,13 @@ public final class Telephony { */ public static final int RESULT_SMS_OUT_OF_MEMORY = 3; + /** + * Set by BroadcastReceiver. Indicates the message, while + * possibly valid, is of a format or encoding that is not + * supported. + */ + public static final int RESULT_SMS_UNSUPPORTED = 4; + /** * Broadcast Action: A new text based SMS message has been received * by the device. The intent will have the following extra @@ -551,6 +564,23 @@ public final class Telephony { public static final String SIM_FULL_ACTION = "android.provider.Telephony.SIM_FULL"; + /** + * Broadcast Action: An incoming SMS has been rejected by the + * telephony framework. This intent is sent in lieu of any + * of the RECEIVED_ACTION intents. The intent will have the + * following extra value:

    + * + *
      + *
    • result - An int result code, eg, + * {@link #RESULT_SMS_OUT_OF_MEMORY}, + * indicating the error returned to the network.
    • + *
    + + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_REJECTED_ACTION = + "android.provider.Telephony.SMS_REJECTED"; + /** * Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a * {@link #DATA_SMS_RECEIVED_ACTION} intent. @@ -1008,6 +1038,12 @@ public final class Telephony { *

    Type: INTEGER

    */ public static final String THREAD_ID = "thread_id"; + + /** + * Has the message been locked? + *

    Type: INTEGER (boolean)

    + */ + public static final String LOCKED = "locked"; } /** @@ -1251,6 +1287,21 @@ public final class Telephony { return match.matches(); } + /** + * Returns true if the number is a Phone number + * + * @param number the input number to be tested + * @return true if number is a Phone number + */ + public static boolean isPhoneNumber(String number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Matcher match = Regex.PHONE_PATTERN.matcher(number); + return match.matches(); + } + /** * Contains all MMS messages in the MMS app's inbox. */ @@ -1416,6 +1467,8 @@ public final class Telephony { */ public static final String _DATA = "_data"; + public static final String TEXT = "text"; + } public static final class Rate { @@ -1428,6 +1481,21 @@ public final class Telephony { public static final String SENT_TIME = "sent_time"; } + public static final class ScrapSpace { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://mms/scrapSpace"); + + /** + * This is the scrap file we use to store the media attachment when the user + * chooses to capture a photo to be attached . We pass {#link@Uri} to the Camera app, + * which streams the captured image to the uri. Internally we write the media content + * to this file. It's named '.temp.jpg' so Gallery won't pick it up. + */ + public static final String SCRAP_FILE_PATH = "/sdcard/mms/scrapSpace/.temp.jpg"; + } + public static final class Intents { private Intents() { // Non-instantiatable. @@ -1493,6 +1561,17 @@ public final class Telephony { public static final Uri CONTENT_DRAFT_URI = Uri.parse( "content://mms-sms/draft"); + public static final Uri CONTENT_LOCKED_URI = Uri.parse( + "content://mms-sms/locked"); + + /*** + * Pass in a query parameter called "pattern" which is the text + * to search for. + * The sort order is fixed to be thread_id ASC,date DESC. + */ + public static final Uri SEARCH_URI = Uri.parse( + "content://mms-sms/search"); + // Constants for message protocol types. public static final int SMS_PROTO = 0; public static final int MMS_PROTO = 1; @@ -1590,6 +1669,8 @@ public final class Telephony { public static final String NUMERIC = "numeric"; + public static final String AUTH_TYPE = "authtype"; + public static final String TYPE = "type"; public static final String CURRENT = "current"; @@ -1639,7 +1720,3 @@ public final class Telephony { public static final String EXTRA_SPN = "spn"; } } - - - - diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 5c4e56da165e5b248f92b1694c4ec17d7f1ae310..ec3b2ff7b2c8a3346cf4fb31c600aa3ffe86e055 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -23,17 +23,16 @@ package android.server; import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothIntent; +import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothA2dp; +import android.os.ParcelUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.media.AudioManager; -import android.os.Binder; import android.os.Handler; import android.os.Message; import android.provider.Settings; @@ -41,9 +40,9 @@ import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; +import java.util.Set; public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String TAG = "BluetoothA2dpService"; @@ -54,386 +53,423 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; - private static final String A2DP_SINK_ADDRESS = "a2dp_sink_address"; private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; private static final int MESSAGE_CONNECT_TO = 1; - private static final int MESSAGE_DISCONNECT = 2; - private final Context mContext; - private final IntentFilter mIntentFilter; - private HashMap mAudioDevices; - private final AudioManager mAudioManager; - private final BluetoothDevice mBluetooth; - - // list of disconnected sinks to process after a delay - private final ArrayList mPendingDisconnects = new ArrayList(); - // number of active sinks - private int mSinkCount = 0; - - private class SinkState { - public String address; - public int state; - public SinkState(String a, int s) {address = a; state = s;} - } - - public BluetoothA2dpService(Context context) { - mContext = context; - - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - - mBluetooth = (BluetoothDevice) mContext.getSystemService(Context.BLUETOOTH_SERVICE); - if (mBluetooth == null) { - throw new RuntimeException("Platform does not support Bluetooth"); - } - - if (!initNative()) { - throw new RuntimeException("Could not init BluetoothA2dpService"); - } + private static final String PROPERTY_STATE = "State"; - mIntentFilter = new IntentFilter(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); - mIntentFilter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION); - mIntentFilter.addAction(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); - mContext.registerReceiver(mReceiver, mIntentFilter); + private static final String SINK_STATE_DISCONNECTED = "disconnected"; + private static final String SINK_STATE_CONNECTING = "connecting"; + private static final String SINK_STATE_CONNECTED = "connected"; + private static final String SINK_STATE_PLAYING = "playing"; - if (mBluetooth.isEnabled()) { - onBluetoothEnable(); - } - } + private static int mSinkCount; - @Override - protected void finalize() throws Throwable { - try { - cleanupNative(); - } finally { - super.finalize(); - } - } + private final Context mContext; + private final IntentFilter mIntentFilter; + private HashMap mAudioDevices; + private final AudioManager mAudioManager; + private final BluetoothService mBluetoothService; + private final BluetoothAdapter mAdapter; + private int mTargetA2dpState; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - String address = intent.getStringExtra(BluetoothIntent.ADDRESS); - if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) { - int state = intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, - BluetoothError.ERROR); + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); switch (state) { - case BluetoothDevice.BLUETOOTH_STATE_ON: + case BluetoothAdapter.STATE_ON: onBluetoothEnable(); break; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: + case BluetoothAdapter.STATE_TURNING_OFF: onBluetoothDisable(); break; } - } else if (action.equals(BluetoothIntent.BOND_STATE_CHANGED_ACTION)) { - int bondState = intent.getIntExtra(BluetoothIntent.BOND_STATE, - BluetoothError.ERROR); + } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { + int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.ERROR); switch(bondState) { case BluetoothDevice.BOND_BONDED: - setSinkPriority(address, BluetoothA2dp.PRIORITY_AUTO); + setSinkPriority(device, BluetoothA2dp.PRIORITY_AUTO); break; case BluetoothDevice.BOND_BONDING: - case BluetoothDevice.BOND_NOT_BONDED: - setSinkPriority(address, BluetoothA2dp.PRIORITY_OFF); + case BluetoothDevice.BOND_NONE: + setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF); break; } - } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION)) { - if (getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF) { + } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { + if (getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && + isSinkDevice(device)) { // This device is a preferred sink. Make an A2DP connection // after a delay. We delay to avoid connection collisions, // and to give other profiles such as HFP a chance to // connect first. - Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, address); + Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, device); mHandler.sendMessageDelayed(msg, 6000); } + } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + synchronized (this) { + if (mAudioDevices.containsKey(device)) { + int state = mAudioDevices.get(device); + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); + } + } } } }; + public BluetoothA2dpService(Context context, BluetoothService bluetoothService) { + mContext = context; + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + mBluetoothService = bluetoothService; + if (mBluetoothService == null) { + throw new RuntimeException("Platform does not support Bluetooth"); + } + + if (!initNative()) { + throw new RuntimeException("Could not init BluetoothA2dpService"); + } + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mReceiver, mIntentFilter); + + mAudioDevices = new HashMap(); + + if (mBluetoothService.isEnabled()) + onBluetoothEnable(); + mTargetA2dpState = -1; + } + + @Override + protected void finalize() throws Throwable { + try { + cleanupNative(); + } finally { + super.finalize(); + } + } + private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_CONNECT_TO: - String address = (String)msg.obj; + BluetoothDevice device = (BluetoothDevice) msg.obj; // check bluetooth is still on, device is still preferred, and // nothing is currently connected - if (mBluetooth.isEnabled() && - getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF && + if (mBluetoothService.isEnabled() && + getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && lookupSinksMatchingStates(new int[] { BluetoothA2dp.STATE_CONNECTING, BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { - log("Auto-connecting A2DP to sink " + address); - connectSink(address); + log("Auto-connecting A2DP to sink " + device); + connectSink(device); } break; - case MESSAGE_DISCONNECT: - handleDeferredDisconnect((String)msg.obj); - break; } } }; - private synchronized void onBluetoothEnable() { - mAudioDevices = new HashMap(); - String[] paths = (String[])listHeadsetsNative(); - if (paths != null) { - for (String path : paths) { - mAudioDevices.put(path, new SinkState(getAddressNative(path), - isSinkConnectedNative(path) ? BluetoothA2dp.STATE_CONNECTED : - BluetoothA2dp.STATE_DISCONNECTED)); + private int convertBluezSinkStringtoState(String value) { + if (value.equalsIgnoreCase("disconnected")) + return BluetoothA2dp.STATE_DISCONNECTED; + if (value.equalsIgnoreCase("connecting")) + return BluetoothA2dp.STATE_CONNECTING; + if (value.equalsIgnoreCase("connected")) + return BluetoothA2dp.STATE_CONNECTED; + if (value.equalsIgnoreCase("playing")) + return BluetoothA2dp.STATE_PLAYING; + return -1; + } + + private boolean isSinkDevice(BluetoothDevice device) { + ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress()); + if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) { + return true; + } + return false; + } + + private synchronized boolean addAudioSink (BluetoothDevice device) { + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + String propValues[] = (String []) getSinkPropertiesNative(path); + if (propValues == null) { + Log.e(TAG, "Error while getting AudioSink properties for device: " + device); + return false; + } + Integer state = null; + // Properties are name-value pairs + for (int i = 0; i < propValues.length; i+=2) { + if (propValues[i].equals(PROPERTY_STATE)) { + state = new Integer(convertBluezSinkStringtoState(propValues[i+1])); + break; } } - mAudioManager.setParameter(BLUETOOTH_ENABLED, "true"); + mAudioDevices.put(device, state); + handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state); + return true; + } + + private synchronized void onBluetoothEnable() { + String devices = mBluetoothService.getProperty("Devices"); + mSinkCount = 0; + if (devices != null) { + String [] paths = devices.split(","); + for (String path: paths) { + String address = mBluetoothService.getAddressFromObjectPath(path); + BluetoothDevice device = mAdapter.getRemoteDevice(address); + ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address); + if (remoteUuids != null) + if (BluetoothUuid.containsAnyUuid(remoteUuids, + new ParcelUuid[] {BluetoothUuid.AudioSink, + BluetoothUuid.AdvAudioDist})) { + addAudioSink(device); + } + } + } + mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true"); + mAudioManager.setParameters("A2dpSuspended=false"); } private synchronized void onBluetoothDisable() { - if (mAudioDevices != null) { - // copy to allow modification during iteration - String[] paths = new String[mAudioDevices.size()]; - paths = mAudioDevices.keySet().toArray(paths); - for (String path : paths) { - switch (mAudioDevices.get(path).state) { + if (!mAudioDevices.isEmpty()) { + BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()]; + devices = mAudioDevices.keySet().toArray(devices); + for (BluetoothDevice device : devices) { + int state = getSinkState(device); + switch (state) { case BluetoothA2dp.STATE_CONNECTING: case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: - disconnectSinkNative(path); - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + disconnectSinkNative(mBluetoothService.getObjectPathFromAddress( + device.getAddress())); + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); break; case BluetoothA2dp.STATE_DISCONNECTING: - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING, + BluetoothA2dp.STATE_DISCONNECTED); break; } } - mAudioDevices = null; + mAudioDevices.clear(); } - mAudioManager.setBluetoothA2dpOn(false); - mAudioManager.setParameter(BLUETOOTH_ENABLED, "false"); + + mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false"); } - public synchronized int connectSink(String address) { + public synchronized boolean connectSink(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (DBG) log("connectSink(" + address + ")"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { - return BluetoothError.ERROR; - } + if (DBG) log("connectSink(" + device + ")"); + // ignore if there are any active sinks if (lookupSinksMatchingStates(new int[] { BluetoothA2dp.STATE_CONNECTING, BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) { - return BluetoothError.ERROR; + return false; } - String path = lookupPath(address); - if (path == null) { - path = createHeadsetNative(address); - if (DBG) log("new bluez sink: " + address + " (" + path + ")"); - } - if (path == null) { - return BluetoothError.ERROR; - } + if (mAudioDevices.get(device) == null && !addAudioSink(device)) + return false; + + int state = mAudioDevices.get(device); - SinkState sink = mAudioDevices.get(path); - int state = BluetoothA2dp.STATE_DISCONNECTED; - if (sink != null) { - state = sink.state; - } switch (state) { case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: case BluetoothA2dp.STATE_DISCONNECTING: - return BluetoothError.ERROR; + return false; case BluetoothA2dp.STATE_CONNECTING: - return BluetoothError.SUCCESS; + return true; } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + if (path == null) + return false; + // State is DISCONNECTED if (!connectSinkNative(path)) { - return BluetoothError.ERROR; + return false; } - updateState(path, BluetoothA2dp.STATE_CONNECTING); - return BluetoothError.SUCCESS; + return true; } - public synchronized int disconnectSink(String address) { + public synchronized boolean disconnectSink(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (DBG) log("disconnectSink(" + address + ")"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { - return BluetoothError.ERROR; - } - String path = lookupPath(address); + if (DBG) log("disconnectSink(" + device + ")"); + + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (path == null) { - return BluetoothError.ERROR; + return false; } - switch (mAudioDevices.get(path).state) { + + switch (getSinkState(device)) { case BluetoothA2dp.STATE_DISCONNECTED: - return BluetoothError.ERROR; + return false; case BluetoothA2dp.STATE_DISCONNECTING: - return BluetoothError.SUCCESS; + return true; } // State is CONNECTING or CONNECTED or PLAYING if (!disconnectSinkNative(path)) { - return BluetoothError.ERROR; + return false; } else { - updateState(path, BluetoothA2dp.STATE_DISCONNECTING); - return BluetoothError.SUCCESS; + return true; } } - public synchronized List listConnectedSinks() { + public synchronized boolean suspendSink(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); + if (device == null || mAudioDevices == null) { + return false; + } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + Integer state = mAudioDevices.get(device); + if (path == null || state == null) { + return false; + } + + mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED; + return checkSinkSuspendState(state.intValue()); + } + + public synchronized boolean resumeSink(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); + if (device == null || mAudioDevices == null) { + return false; + } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + Integer state = mAudioDevices.get(device); + if (path == null || state == null) { + return false; + } + mTargetA2dpState = BluetoothA2dp.STATE_PLAYING; + return checkSinkSuspendState(state.intValue()); + } + + public synchronized BluetoothDevice[] getConnectedSinks() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return lookupSinksMatchingStates(new int[] {BluetoothA2dp.STATE_CONNECTED, - BluetoothA2dp.STATE_PLAYING}); + Set sinks = lookupSinksMatchingStates( + new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING}); + return sinks.toArray(new BluetoothDevice[sinks.size()]); } - public synchronized int getSinkState(String address) { + public synchronized int getSinkState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { + Integer state = mAudioDevices.get(device); + if (state == null) return BluetoothA2dp.STATE_DISCONNECTED; - } - for (SinkState sink : mAudioDevices.values()) { - if (address.equals(sink.address)) { - return sink.state; - } - } - return BluetoothA2dp.STATE_DISCONNECTED; + return state; } - public synchronized int getSinkPriority(String address) { + public synchronized int getSinkPriority(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } return Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), + Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), BluetoothA2dp.PRIORITY_OFF); } - public synchronized int setSinkPriority(String address, int priority) { + public synchronized boolean setSinkPriority(BluetoothDevice device, int priority) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; + if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { + return false; } return Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), priority) ? - BluetoothError.SUCCESS : BluetoothError.ERROR; + Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); } - private synchronized void onHeadsetCreated(String path) { - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } - - private synchronized void onHeadsetRemoved(String path) { - if (mAudioDevices == null) return; - mAudioDevices.remove(path); - } - - private synchronized void onSinkConnected(String path) { - // if we are reconnected, do not process previous disconnect event. - mPendingDisconnects.remove(path); - - if (mAudioDevices == null) return; - // bluez 3.36 quietly disconnects the previous sink when a new sink - // is connected, so we need to mark all previously connected sinks as - // disconnected - - // copy to allow modification during iteration - String[] paths = new String[mAudioDevices.size()]; - paths = mAudioDevices.keySet().toArray(paths); - for (String oldPath : paths) { - if (path.equals(oldPath)) { - continue; - } - int state = mAudioDevices.get(oldPath).state; - if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) { - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } + private synchronized void onSinkPropertyChanged(String path, String []propValues) { + if (!mBluetoothService.isEnabled()) { + return; } - updateState(path, BluetoothA2dp.STATE_CONNECTING); - mAudioManager.setParameter(A2DP_SINK_ADDRESS, lookupAddress(path)); - mAudioManager.setBluetoothA2dpOn(true); - updateState(path, BluetoothA2dp.STATE_CONNECTED); - } + String name = propValues[0]; + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) { + Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null"); + return; + } - private synchronized void onSinkDisconnected(String path) { - // This is to work around a problem in bluez that results - // sink disconnect events being sent, immediately followed by a reconnect. - // To avoid unnecessary audio routing changes, we defer handling - // sink disconnects until after a short delay. - mPendingDisconnects.add(path); - Message msg = Message.obtain(mHandler, MESSAGE_DISCONNECT, path); - mHandler.sendMessageDelayed(msg, 2000); - } + BluetoothDevice device = mAdapter.getRemoteDevice(address); - private synchronized void handleDeferredDisconnect(String path) { - if (mPendingDisconnects.contains(path)) { - mPendingDisconnects.remove(path); - if (mSinkCount == 1) { - mAudioManager.setBluetoothA2dpOn(false); + if (name.equals(PROPERTY_STATE)) { + int state = convertBluezSinkStringtoState(propValues[1]); + if (mAudioDevices.get(device) == null) { + // This is for an incoming connection for a device not known to us. + // We have authorized it and bluez state has changed. + addAudioSink(device); + } else { + int prevState = mAudioDevices.get(device); + handleSinkStateChange(device, prevState, state); } - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); } } - private synchronized void onSinkPlaying(String path) { - updateState(path, BluetoothA2dp.STATE_PLAYING); - } - - private synchronized void onSinkStopped(String path) { - updateState(path, BluetoothA2dp.STATE_CONNECTED); - } - - private synchronized final String lookupAddress(String path) { - if (mAudioDevices == null) return null; - SinkState sink = mAudioDevices.get(path); - if (sink == null) { - Log.w(TAG, "lookupAddress() called for unknown device " + path); - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } - String address = mAudioDevices.get(path).address; - if (address == null) Log.e(TAG, "Can't find address for " + path); - return address; - } + private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) { + if (state != prevState) { + if (state == BluetoothA2dp.STATE_DISCONNECTED || + state == BluetoothA2dp.STATE_DISCONNECTING) { + if (prevState == BluetoothA2dp.STATE_CONNECTED || + prevState == BluetoothA2dp.STATE_PLAYING) { + // disconnecting or disconnected + Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + mContext.sendBroadcast(intent); + } + mSinkCount--; + } else if (state == BluetoothA2dp.STATE_CONNECTED) { + mSinkCount ++; + } + mAudioDevices.put(device, state); - private synchronized final String lookupPath(String address) { - if (mAudioDevices == null) return null; + checkSinkSuspendState(state); + mTargetA2dpState = -1; - for (String path : mAudioDevices.keySet()) { - if (address.equals(mAudioDevices.get(path).address)) { - return path; + if (state == BluetoothA2dp.STATE_CONNECTING) { + mAudioManager.setParameters("A2dpSuspended=false"); } + Intent intent = new Intent(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, prevState); + intent.putExtra(BluetoothA2dp.EXTRA_SINK_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state); } - return null; } - private synchronized List lookupSinksMatchingStates(int[] states) { - List sinks = new ArrayList(); - if (mAudioDevices == null) { + private synchronized Set lookupSinksMatchingStates(int[] states) { + Set sinks = new HashSet(); + if (mAudioDevices.isEmpty()) { return sinks; } - for (SinkState sink : mAudioDevices.values()) { + for (BluetoothDevice device: mAudioDevices.keySet()) { + int sinkState = getSinkState(device); for (int state : states) { - if (sink.state == state) { - sinks.add(sink.address); + if (state == sinkState) { + sinks.add(device); break; } } @@ -441,57 +477,30 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return sinks; } - private synchronized void updateState(String path, int state) { - if (mAudioDevices == null) return; - - SinkState s = mAudioDevices.get(path); - int prevState; - String address; - if (s == null) { - address = getAddressNative(path); - mAudioDevices.put(path, new SinkState(address, state)); - prevState = BluetoothA2dp.STATE_DISCONNECTED; - } else { - address = lookupAddress(path); - prevState = s.state; - s.state = state; - } - - if (state != prevState) { - if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state); - - // keep track of the number of active sinks - if (prevState == BluetoothA2dp.STATE_DISCONNECTED) { - mSinkCount++; - } else if (state == BluetoothA2dp.STATE_DISCONNECTED) { - mSinkCount--; - } - - Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState); - intent.putExtra(BluetoothA2dp.SINK_STATE, state); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - - if ((prevState == BluetoothA2dp.STATE_CONNECTED || - prevState == BluetoothA2dp.STATE_PLAYING) && - (state != BluetoothA2dp.STATE_CONNECTING && - state != BluetoothA2dp.STATE_CONNECTED && - state != BluetoothA2dp.STATE_PLAYING)) { - // disconnected - intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - mContext.sendBroadcast(intent); + private boolean checkSinkSuspendState(int state) { + boolean result = true; + + if (state != mTargetA2dpState) { + if (state == BluetoothA2dp.STATE_PLAYING && + mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) { + mAudioManager.setParameters("A2dpSuspended=true"); + } else if (state == BluetoothA2dp.STATE_CONNECTED && + mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) { + mAudioManager.setParameters("A2dpSuspended=false"); + } else { + result = false; } } + return result; } @Override protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mAudioDevices == null) return; + if (mAudioDevices.isEmpty()) return; pw.println("Cached audio devices:"); - for (String path : mAudioDevices.keySet()) { - SinkState sink = mAudioDevices.get(path); - pw.println(path + " " + sink.address + " " + BluetoothA2dp.stateToString(sink.state)); + for (BluetoothDevice device : mAudioDevices.keySet()) { + int state = mAudioDevices.get(device); + pw.println(device + " " + BluetoothA2dp.stateToString(state)); } } @@ -501,11 +510,9 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private native boolean initNative(); private native void cleanupNative(); - private synchronized native String[] listHeadsetsNative(); - private synchronized native String createHeadsetNative(String address); - private synchronized native boolean removeHeadsetNative(String path); - private synchronized native String getAddressNative(String path); private synchronized native boolean connectSinkNative(String path); private synchronized native boolean disconnectSinkNative(String path); - private synchronized native boolean isSinkConnectedNative(String path); + private synchronized native boolean suspendSinkNative(String path); + private synchronized native boolean resumeSinkNative(String path); + private synchronized native Object []getSinkPropertiesNative(String path); } diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java deleted file mode 100644 index 8c843efd7e4ebc5e7d923fdb054c99127e4a5a6a..0000000000000000000000000000000000000000 --- a/core/java/android/server/BluetoothDeviceService.java +++ /dev/null @@ -1,1263 +0,0 @@ -/* - * 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. - */ - -/** - * TODO: Move this to - * java/services/com/android/server/BluetoothDeviceService.java - * and make the contructor package private again. - * - * @hide - */ - -package android.server; - -import android.bluetooth.BluetoothClass; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothIntent; -import android.bluetooth.IBluetoothDevice; -import android.bluetooth.IBluetoothDeviceCallback; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemService; -import android.provider.Settings; -import android.util.Log; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import com.android.internal.app.IBatteryStats; - -public class BluetoothDeviceService extends IBluetoothDevice.Stub { - private static final String TAG = "BluetoothDeviceService"; - private static final boolean DBG = true; - - private int mNativeData; - private BluetoothEventLoop mEventLoop; - private IntentFilter mIntentFilter; - private boolean mIsAirplaneSensitive; - private int mBluetoothState; - private boolean mRestart = false; // need to call enable() after disable() - - private final BondState mBondState = new BondState(); // local cache of bondings - private boolean mIsDiscovering; - private final IBatteryStats mBatteryStats; - - private final Context mContext; - - 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 int MESSAGE_REGISTER_SDP_RECORDS = 1; - private static final int MESSAGE_FINISH_DISABLE = 2; - - static { - classInitNative(); - } - private native static void classInitNative(); - - public BluetoothDeviceService(Context context) { - mContext = context; - - // Need to do this in place of: - // mBatteryStats = BatteryStatsService.getService(); - // Since we can not import BatteryStatsService from here. This class really needs to be - // moved to java/services/com/android/server/ - mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); - } - - /** Must be called after construction, and before any other method. - */ - public synchronized void init() { - initializeNativeDataNative(); - - if (isEnabledNative() == 1) { - Log.w(TAG, "Bluetooth daemons already running - runtime restart? "); - disableNative(); - } - - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); - mIsDiscovering = false; - mEventLoop = new BluetoothEventLoop(mContext, this); - registerForAirplaneMode(); - } - private native void initializeNativeDataNative(); - - @Override - protected void finalize() throws Throwable { - if (mIsAirplaneSensitive) { - mContext.unregisterReceiver(mReceiver); - } - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - private native void cleanupNativeDataNative(); - - public boolean isEnabled() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothState == BluetoothDevice.BLUETOOTH_STATE_ON; - } - private native int isEnabledNative(); - - public int getBluetoothState() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothState; - } - - - /** - * Bring down bluetooth and disable BT in settings. Returns true on success. - */ - public boolean disable() { - return disable(true); - } - - /** - * Bring down bluetooth. Returns true on success. - * - * @param saveSetting If true, disable BT in settings - */ - public synchronized boolean disable(boolean saveSetting) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - - switch (mBluetoothState) { - case BluetoothDevice.BLUETOOTH_STATE_OFF: - return true; - case BluetoothDevice.BLUETOOTH_STATE_ON: - break; - default: - return false; - } - if (mEnableThread != null && mEnableThread.isAlive()) { - return false; - } - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF); - - // Allow 3 seconds for profiles to gracefully disconnect - // TODO: Introduce a callback mechanism so that each profile can notify - // BluetoothDeviceService when it is done shutting down - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MESSAGE_FINISH_DISABLE, saveSetting ? 1 : 0, 0), 3000); - return true; - } - - - private synchronized void finishDisable(boolean saveSetting) { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF) { - return; - } - mEventLoop.stop(); - disableNative(); - - // mark in progress bondings as cancelled - for (String address : mBondState.listInState(BluetoothDevice.BOND_BONDING)) { - mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); - } - - // Remove remoteServiceChannelCallbacks - HashMap callbacksMap = - mEventLoop.getRemoteServiceChannelCallbacks(); - - for (Iterator i = callbacksMap.keySet().iterator(); i.hasNext();) { - String address = i.next(); - IBluetoothDeviceCallback callback = callbacksMap.get(address); - try { - callback.onGetRemoteServiceChannelResult(address, BluetoothError.ERROR_DISABLED); - } catch (RemoteException e) {} - i.remove(); - } - - // update mode - Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.SCAN_MODE, BluetoothDevice.SCAN_MODE_NONE); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - - mIsDiscovering = false; - - if (saveSetting) { - persistBluetoothOnSetting(false); - } - - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); - - // Log bluetooth off to battery stats. - long ident = Binder.clearCallingIdentity(); - try { - mBatteryStats.noteBluetoothOff(); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - - if (mRestart) { - mRestart = false; - enable(); - } - } - - /** Bring up BT and persist BT on in settings */ - public boolean enable() { - return enable(true); - } - - /** - * Enable this Bluetooth device, asynchronously. - * This turns on/off the underlying hardware. - * - * @param saveSetting If true, persist the new state of BT in settings - * @return True on success (so far) - */ - public synchronized boolean enable(boolean saveSetting) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - - // Airplane mode can prevent Bluetooth radio from being turned on. - if (mIsAirplaneSensitive && isAirplaneModeOn()) { - return false; - } - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_OFF) { - return false; - } - if (mEnableThread != null && mEnableThread.isAlive()) { - return false; - } - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_ON); - mEnableThread = new EnableThread(saveSetting); - mEnableThread.start(); - return true; - } - - /** Forcibly restart Bluetooth if it is on */ - /* package */ synchronized void restart() { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_ON) { - return; - } - mRestart = true; - if (!disable(false)) { - mRestart = false; - } - } - - private synchronized void setBluetoothState(int state) { - if (state == mBluetoothState) { - return; - } - - if (DBG) log("Bluetooth state " + mBluetoothState + " -> " + state); - - Intent intent = new Intent(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.BLUETOOTH_PREVIOUS_STATE, mBluetoothState); - intent.putExtra(BluetoothIntent.BLUETOOTH_STATE, state); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - - mBluetoothState = state; - - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_REGISTER_SDP_RECORDS: - //TODO: Don't assume HSP/HFP is running, don't use sdptool, - if (isEnabled()) { - SystemService.start("hsag"); - SystemService.start("hfag"); - } - break; - case MESSAGE_FINISH_DISABLE: - finishDisable(msg.arg1 != 0); - break; - } - } - }; - - private EnableThread mEnableThread; - - private class EnableThread extends Thread { - private final boolean mSaveSetting; - public EnableThread(boolean saveSetting) { - mSaveSetting = saveSetting; - } - public void run() { - boolean res = (enableNative() == 0); - if (res) { - int retryCount = 2; - boolean running = false; - while ((retryCount-- > 0) && !running) { - mEventLoop.start(); - // it may take a momement for the other thread to do its - // thing. Check periodically for a while. - int pollCount = 5; - while ((pollCount-- > 0) && !running) { - if (mEventLoop.isEventLoopRunning()) { - running = true; - break; - } - try { - Thread.sleep(100); - } catch (InterruptedException e) {} - } - } - if (!running) { - log("bt EnableThread giving up"); - res = false; - disableNative(); - } - } - - - if (res) { - if (mSaveSetting) { - persistBluetoothOnSetting(true); - } - mIsDiscovering = false; - mBondState.loadBondState(); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS), - 3000); - - // Log bluetooth on to battery stats. - long ident = Binder.clearCallingIdentity(); - try { - mBatteryStats.noteBluetoothOn(); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - mEnableThread = null; - - setBluetoothState(res ? - BluetoothDevice.BLUETOOTH_STATE_ON : - BluetoothDevice.BLUETOOTH_STATE_OFF); - - if (res) { - // Update mode - mEventLoop.onModeChanged(getModeNative()); - } - - if (mIsAirplaneSensitive && isAirplaneModeOn()) { - disable(false); - } - - } - } - - private void persistBluetoothOnSetting(boolean bluetoothOn) { - long origCallerIdentityToken = Binder.clearCallingIdentity(); - Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, - bluetoothOn ? 1 : 0); - Binder.restoreCallingIdentity(origCallerIdentityToken); - } - - private native int enableNative(); - private native int disableNative(); - - /* package */ BondState getBondState() { - return mBondState; - } - - /** local cache of bonding state. - /* we keep our own state to track the intermediate state BONDING, which - /* bluez does not track. - * All addreses must be passed in upper case. - */ - public class BondState { - private final HashMap mState = new HashMap(); - private final HashMap mPinAttempt = new HashMap(); - private final ArrayList mAutoPairingFailures = new ArrayList(); - // List of all the vendor_id prefix of Bluetooth addresses for - // which auto pairing is not attempted. - // The following companies are included in the list below: - // ALPS (lexus), Murata (Prius 2007, Nokia 616), TEMIC SDS (Porsche, Audi), - // Parrot, Zhongshan General K-mate Electronics, Great Well - // Electronics, Flaircomm Electronics, Jatty Electronics, Delphi, - // Clarion, Novero, Denso (Lexus, Toyota), Johnson Controls (Acura), - // Continental Automotive, Harman/Becker - private final ArrayList mAutoPairingBlacklisted = - new ArrayList(Arrays.asList( - "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", "00:21:4F", - "00:23:06", "00:24:33", "00:A0:79", "00:0E:6D", "00:13:E0", "00:21:E8", - "00:60:57", "00:0E:9F", "00:12:1C", "00:18:91", "00:18:96", "00:13:04", - "00:16:FD", "00:22:A0", "00:0B:4C", "00:60:6F", "00:23:3D", "00:C0:59", - "00:0A:30", "00:1E:AE", "00:1C:D7" - )); - - public synchronized void loadBondState() { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_ON) { - return; - } - String[] bonds = listBondingsNative(); - if (bonds == null) { - return; - } - mState.clear(); - if (DBG) log("found " + bonds.length + " bonded devices"); - for (String address : bonds) { - mState.put(address.toUpperCase(), BluetoothDevice.BOND_BONDED); - } - } - - public synchronized void setBondState(String address, int state) { - setBondState(address, state, 0); - } - - /** reason is ignored unless state == BOND_NOT_BONDED */ - public synchronized void setBondState(String address, int state, int reason) { - int oldState = getBondState(address); - if (oldState == state) { - return; - } - if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + - reason + ")"); - Intent intent = new Intent(BluetoothIntent.BOND_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.BOND_STATE, state); - intent.putExtra(BluetoothIntent.BOND_PREVIOUS_STATE, oldState); - if (state == BluetoothDevice.BOND_NOT_BONDED) { - if (reason <= 0) { - Log.w(TAG, "setBondState() called to unbond device, but reason code is " + - "invalid. Overriding reason code with BOND_RESULT_REMOVED"); - reason = BluetoothDevice.UNBOND_REASON_REMOVED; - } - intent.putExtra(BluetoothIntent.REASON, reason); - mState.remove(address); - } else { - mState.put(address, state); - } - - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - - public boolean isAutoPairingBlacklisted(String address) { - for (String blacklistAddress : mAutoPairingBlacklisted) { - if (address.startsWith(blacklistAddress)) return true; - } - return false; - } - - public synchronized int getBondState(String address) { - Integer state = mState.get(address); - if (state == null) { - return BluetoothDevice.BOND_NOT_BONDED; - } - return state.intValue(); - } - - private synchronized String[] listInState(int state) { - ArrayList result = new ArrayList(mState.size()); - for (Map.Entry e : mState.entrySet()) { - if (e.getValue().intValue() == state) { - result.add(e.getKey()); - } - } - return result.toArray(new String[result.size()]); - } - - public synchronized void addAutoPairingFailure(String address) { - if (!mAutoPairingFailures.contains(address)) { - mAutoPairingFailures.add(address); - } - } - - public synchronized boolean isAutoPairingAttemptsInProgress(String address) { - return getAttempt(address) != 0; - } - - public synchronized void clearPinAttempts(String address) { - mPinAttempt.remove(address); - } - - public synchronized boolean hasAutoPairingFailed(String address) { - return mAutoPairingFailures.contains(address); - } - - public synchronized int getAttempt(String address) { - Integer attempt = mPinAttempt.get(address); - if (attempt == null) { - return 0; - } - return attempt.intValue(); - } - - public synchronized void attempt(String address) { - Integer attempt = mPinAttempt.get(address); - int newAttempt; - if (attempt == null) { - newAttempt = 1; - } else { - newAttempt = attempt.intValue() + 1; - } - mPinAttempt.put(address, new Integer(newAttempt)); - } - - } - private native String[] listBondingsNative(); - - private static String toBondStateString(int bondState) { - switch (bondState) { - case BluetoothDevice.BOND_NOT_BONDED: - return "not bonded"; - case BluetoothDevice.BOND_BONDING: - return "bonding"; - case BluetoothDevice.BOND_BONDED: - return "bonded"; - default: - return "??????"; - } - } - - public synchronized String getAddress() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getAddressNative(); - } - private native String getAddressNative(); - - public synchronized String getName() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getNameNative(); - } - private native String getNameNative(); - - public synchronized boolean setName(String name) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (name == null) { - return false; - } - // hcid handles persistance of the bluetooth name - return setNameNative(name); - } - private native boolean setNameNative(String name); - - /** - * Returns the user-friendly name of a remote device. This value is - * retrned from our local cache, which is updated during device discovery. - * Do not expect to retrieve the updated remote name immediately after - * changing the name on the remote device. - * - * @param address Bluetooth address of remote device. - * - * @return The user-friendly name of the specified remote device. - */ - public synchronized String getRemoteName(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteNameNative(address); - } - private native String getRemoteNameNative(String address); - - /* pacakge */ native String getAdapterPathNative(); - - public synchronized boolean startDiscovery(boolean resolveNames) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return startDiscoveryNative(resolveNames); - } - private native boolean startDiscoveryNative(boolean resolveNames); - - public synchronized boolean cancelDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return cancelDiscoveryNative(); - } - private native boolean cancelDiscoveryNative(); - - public synchronized boolean isDiscovering() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mIsDiscovering; - } - - /* package */ void setIsDiscovering(boolean isDiscovering) { - mIsDiscovering = isDiscovering; - } - - public synchronized boolean startPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return startPeriodicDiscoveryNative(); - } - private native boolean startPeriodicDiscoveryNative(); - - public synchronized boolean stopPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return stopPeriodicDiscoveryNative(); - } - private native boolean stopPeriodicDiscoveryNative(); - - public synchronized boolean isPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return isPeriodicDiscoveryNative(); - } - private native boolean isPeriodicDiscoveryNative(); - - /** - * Set the discoverability window for the device. A timeout of zero - * makes the device permanently discoverable (if the device is - * discoverable). Setting the timeout to a nonzero value does not make - * a device discoverable; you need to call setMode() to make the device - * explicitly discoverable. - * - * @param timeout_s The discoverable timeout in seconds. - */ - public synchronized boolean setDiscoverableTimeout(int timeout) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return setDiscoverableTimeoutNative(timeout); - } - private native boolean setDiscoverableTimeoutNative(int timeout_s); - - /** - * Get the discoverability window for the device. A timeout of zero - * means that the device is permanently discoverable (if the device is - * in the discoverable mode). - * - * @return The discoverability window of the device, in seconds. A negative - * value indicates an error. - */ - public synchronized int getDiscoverableTimeout() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getDiscoverableTimeoutNative(); - } - private native int getDiscoverableTimeoutNative(); - - public synchronized boolean isAclConnected(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return isConnectedNative(address); - } - private native boolean isConnectedNative(String address); - - public synchronized int getScanMode() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return bluezStringToScanMode(getModeNative()); - } - private native String getModeNative(); - - public synchronized boolean setScanMode(int mode) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - String bluezMode = scanModeToBluezString(mode); - if (bluezMode != null) { - return setModeNative(bluezMode); - } - return false; - } - private native boolean setModeNative(String mode); - - public synchronized boolean disconnectRemoteDeviceAcl(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return disconnectRemoteDeviceNative(address); - } - private native boolean disconnectRemoteDeviceNative(String address); - - public synchronized boolean createBond(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - - String[] bonding = mBondState.listInState(BluetoothDevice.BOND_BONDING); - if (bonding.length > 0 && !bonding[0].equals(address)) { - log("Ignoring createBond(): another device is bonding"); - // a different device is currently bonding, fail - return false; - } - - // Check for bond state only if we are not performing auto - // pairing exponential back-off attempts. - if (!mBondState.isAutoPairingAttemptsInProgress(address) && - mBondState.getBondState(address) != BluetoothDevice.BOND_NOT_BONDED) { - log("Ignoring createBond(): this device is already bonding or bonded"); - return false; - } - - if (!createBondingNative(address, 60000 /* 1 minute */)) { - return false; - } - - mBondState.setBondState(address, BluetoothDevice.BOND_BONDING); - return true; - } - private native boolean createBondingNative(String address, int timeout_ms); - - public synchronized boolean cancelBondProcess(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - if (mBondState.getBondState(address) != BluetoothDevice.BOND_BONDING) { - return false; - } - - mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); - cancelBondingProcessNative(address); - return true; - } - private native boolean cancelBondingProcessNative(String address); - - public synchronized boolean removeBond(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return removeBondingNative(address); - } - private native boolean removeBondingNative(String address); - - public synchronized String[] listBonds() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBondState.listInState(BluetoothDevice.BOND_BONDED); - } - - public synchronized int getBondState(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - return mBondState.getBondState(address.toUpperCase()); - } - - public synchronized String[] listAclConnections() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return listConnectionsNative(); - } - private native String[] listConnectionsNative(); - - /** - * This method lists all remote devices that this adapter is aware of. - * This is a list not only of all most-recently discovered devices, but of - * all devices discovered by this adapter up to some point in the past. - * Note that many of these devices may not be in the neighborhood anymore, - * and attempting to connect to them will result in an error. - * - * @return An array of strings representing the Bluetooth addresses of all - * remote devices that this adapter is aware of. - */ - public synchronized String[] listRemoteDevices() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return listRemoteDevicesNative(); - } - private native String[] listRemoteDevicesNative(); - - /** - * Returns the version of the Bluetooth chip. This version is compiled from - * the LMP version. In case of EDR the features attribute must be checked. - * Example: "Bluetooth 2.0 + EDR". - * - * @return a String representation of the this Adapter's underlying - * Bluetooth-chip version. - */ - public synchronized String getVersion() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getVersionNative(); - } - private native String getVersionNative(); - - /** - * Returns the revision of the Bluetooth chip. This is a vendor-specific - * value and in most cases it represents the firmware version. This might - * derive from the HCI revision and LMP subversion values or via extra - * vendord specific commands. - * In case the revision of a chip is not available. This method should - * return the LMP subversion value as a string. - * Example: "HCI 19.2" - * - * @return The HCI revision of this adapter. - */ - public synchronized String getRevision() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getRevisionNative(); - } - private native String getRevisionNative(); - - /** - * Returns the manufacturer of the Bluetooth chip. If the company id is not - * known the sting "Company ID %d" where %d should be replaced with the - * numeric value from the manufacturer field. - * Example: "Cambridge Silicon Radio" - * - * @return Manufacturer name. - */ - public synchronized String getManufacturer() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getManufacturerNative(); - } - private native String getManufacturerNative(); - - /** - * Returns the company name from the OUI database of the Bluetooth device - * address. This function will need a valid and up-to-date oui.txt from - * the IEEE. This value will be different from the manufacturer string in - * the most cases. - * If the oui.txt file is not present or the OUI part of the Bluetooth - * address is not listed, it should return the string "OUI %s" where %s is - * the actual OUI. - * - * Example: "Apple Computer" - * - * @return company name - */ - public synchronized String getCompany() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getCompanyNative(); - } - private native String getCompanyNative(); - - /** - * Like getVersion(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device Bluetooth version - * - * @see #getVersion - */ - public synchronized String getRemoteVersion(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteVersionNative(address); - } - private native String getRemoteVersionNative(String address); - - /** - * Like getRevision(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device HCI revision - * - * @see #getRevision - */ - public synchronized String getRemoteRevision(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteRevisionNative(address); - } - private native String getRemoteRevisionNative(String address); - - /** - * Like getManufacturer(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device Bluetooth chip manufacturer - * - * @see #getManufacturer - */ - public synchronized String getRemoteManufacturer(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteManufacturerNative(address); - } - private native String getRemoteManufacturerNative(String address); - - /** - * Like getCompany(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device company - * - * @see #getCompany - */ - public synchronized String getRemoteCompany(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteCompanyNative(address); - } - private native String getRemoteCompanyNative(String address); - - /** - * Returns the date and time when the specified remote device has been seen - * by a discover procedure. - * Example: "2006-02-08 12:00:00 GMT" - * - * @return a String with the timestamp. - */ - public synchronized String lastSeen(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return lastSeenNative(address); - } - private native String lastSeenNative(String address); - - /** - * Returns the date and time when the specified remote device has last been - * connected to - * Example: "2006-02-08 12:00:00 GMT" - * - * @return a String with the timestamp. - */ - public synchronized String lastUsed(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return lastUsedNative(address); - } - private native String lastUsedNative(String address); - - /** - * Gets the remote major, minor, and service classes encoded as a 32-bit - * integer. - * - * Note: this value is retrieved from cache, because we get it during - * remote-device discovery. - * - * @return 32-bit integer encoding the remote major, minor, and service - * classes. - * - * @see #getRemoteMajorClass - * @see #getRemoteMinorClass - * @see #getRemoteServiceClasses - */ - public synchronized int getRemoteClass(String address) { - if (!BluetoothDevice.checkBluetoothAddress(address)) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return BluetoothClass.ERROR; - } - return getRemoteClassNative(address); - } - private native int getRemoteClassNative(String address); - - /** - * Gets the remote features encoded as bit mask. - * - * Note: This method may be obsoleted soon. - * - * @return byte array of features. - */ - public synchronized byte[] getRemoteFeatures(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteFeaturesNative(address); - } - private native byte[] getRemoteFeaturesNative(String address); - - /** - * This method and {@link #getRemoteServiceRecord} query the SDP service - * on a remote device. They do not interpret the data, but simply return - * it raw to the user. To read more about SDP service handles and records, - * consult the Bluetooth core documentation (www.bluetooth.com). - * - * @param address Bluetooth address of remote device. - * @param match a String match to narrow down the service-handle search. - * The only supported value currently is "hsp" for the headset - * profile. To retrieve all service handles, simply pass an empty - * match string. - * - * @return all service handles corresponding to the string match. - * - * @see #getRemoteServiceRecord - */ - public synchronized int[] getRemoteServiceHandles(String address, String match) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - if (match == null) { - match = ""; - } - return getRemoteServiceHandlesNative(address, match); - } - private native int[] getRemoteServiceHandlesNative(String address, String match); - - /** - * This method retrieves the service records corresponding to a given - * service handle (method {@link #getRemoteServiceHandles} retrieves the - * service handles.) - * - * This method and {@link #getRemoteServiceHandles} do not interpret their - * data, but simply return it raw to the user. To read more about SDP - * service handles and records, consult the Bluetooth core documentation - * (www.bluetooth.com). - * - * @param address Bluetooth address of remote device. - * @param handle Service handle returned by {@link #getRemoteServiceHandles} - * - * @return a byte array of all service records corresponding to the - * specified service handle. - * - * @see #getRemoteServiceHandles - */ - public synchronized byte[] getRemoteServiceRecord(String address, int handle) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteServiceRecordNative(address, handle); - } - private native byte[] getRemoteServiceRecordNative(String address, int handle); - - private static final int MAX_OUTSTANDING_ASYNC = 32; - - // AIDL does not yet support short's - public synchronized boolean getRemoteServiceChannel(String address, int uuid16, - IBluetoothDeviceCallback callback) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - HashMap callbacks = - mEventLoop.getRemoteServiceChannelCallbacks(); - if (callbacks.containsKey(address)) { - Log.w(TAG, "SDP request already in progress for " + address); - return false; - } - // Protect from malicious clients - only allow 32 bonding requests per minute. - if (callbacks.size() > MAX_OUTSTANDING_ASYNC) { - Log.w(TAG, "Too many outstanding SDP requests, dropping request for " + address); - return false; - } - callbacks.put(address, callback); - - if (!getRemoteServiceChannelNative(address, (short)uuid16)) { - callbacks.remove(address); - return false; - } - return true; - } - private native boolean getRemoteServiceChannelNative(String address, short uuid16); - - public synchronized boolean setPin(String address, byte[] pin) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (pin == null || pin.length <= 0 || pin.length > 16 || - !BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); - if (data == null) { - Log.w(TAG, "setPin(" + address + ") called but no native data available, " + - "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + - " or by bluez.\n"); - return false; - } - // bluez API wants pin as a string - String pinString; - try { - pinString = new String(pin, "UTF8"); - } catch (UnsupportedEncodingException uee) { - Log.e(TAG, "UTF8 not supported?!?"); - return false; - } - return setPinNative(address, pinString, data.intValue()); - } - private native boolean setPinNative(String address, String pin, int nativeData); - - public synchronized boolean cancelPin(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); - if (data == null) { - Log.w(TAG, "cancelPin(" + address + ") called but no native data available, " + - "ignoring. Maybe the PasskeyAgent Request was already cancelled by the remote " + - "or by bluez.\n"); - return false; - } - return cancelPinNative(address, data.intValue()); - } - private native boolean cancelPinNative(String address, int natveiData); - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { - ContentResolver resolver = context.getContentResolver(); - // Query the airplane mode from Settings.System just to make sure that - // some random app is not sending this intent and disabling bluetooth - boolean enabled = !isAirplaneModeOn(); - // If bluetooth is currently expected to be on, then enable or disable bluetooth - if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { - if (enabled) { - enable(false); - } else { - disable(false); - } - } - } - } - }; - - private void registerForAirplaneMode() { - String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), - Settings.System.AIRPLANE_MODE_RADIOS); - mIsAirplaneSensitive = airplaneModeRadios == null - ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); - if (mIsAirplaneSensitive) { - mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mContext.registerReceiver(mReceiver, mIntentFilter); - } - } - - /* Returns true if airplane mode is currently on */ - private final boolean isAirplaneModeOn() { - return Settings.System.getInt(mContext.getContentResolver(), - Settings.System.AIRPLANE_MODE_ON, 0) == 1; - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive + "\n"); - - switch(mBluetoothState) { - case BluetoothDevice.BLUETOOTH_STATE_OFF: - pw.println("\nBluetooth OFF\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_ON: - pw.println("\nBluetooth TURNING ON\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: - pw.println("\nBluetooth TURNING OFF\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_ON: - pw.println("\nBluetooth ON\n"); - } - - pw.println("\nLocal address = " + getAddress()); - pw.println("\nLocal name = " + getName()); - pw.println("\nisDiscovering() = " + isDiscovering()); - - BluetoothHeadset headset = new BluetoothHeadset(mContext, null); - - String[] addresses = listRemoteDevices(); - - pw.println("\n--Known devices--"); - for (String address : addresses) { - pw.printf("%s %10s (%d) %s\n", address, - toBondStateString(mBondState.getBondState(address)), - mBondState.getAttempt(address), - getRemoteName(address)); - } - - addresses = listAclConnections(); - pw.println("\n--ACL connected devices--"); - for (String address : addresses) { - pw.println(address); - } - - // Rather not do this from here, but no-where else and I need this - // dump - pw.println("\n--Headset Service--"); - switch (headset.getState()) { - case BluetoothHeadset.STATE_DISCONNECTED: - pw.println("getState() = STATE_DISCONNECTED"); - break; - case BluetoothHeadset.STATE_CONNECTING: - pw.println("getState() = STATE_CONNECTING"); - break; - case BluetoothHeadset.STATE_CONNECTED: - pw.println("getState() = STATE_CONNECTED"); - break; - case BluetoothHeadset.STATE_ERROR: - pw.println("getState() = STATE_ERROR"); - break; - } - pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress()); - pw.println("getBatteryUsageHint() = " + headset.getBatteryUsageHint()); - - headset.close(); - } - - /* package */ static int bluezStringToScanMode(String mode) { - if (mode == null) { - return BluetoothError.ERROR; - } - mode = mode.toLowerCase(); - if (mode.equals("off")) { - return BluetoothDevice.SCAN_MODE_NONE; - } else if (mode.equals("connectable")) { - return BluetoothDevice.SCAN_MODE_CONNECTABLE; - } else if (mode.equals("discoverable")) { - return BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE; - } else { - return BluetoothError.ERROR; - } - } - - /* package */ static String scanModeToBluezString(int mode) { - switch (mode) { - case BluetoothDevice.SCAN_MODE_NONE: - return "off"; - case BluetoothDevice.SCAN_MODE_CONNECTABLE: - return "connectable"; - case BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE: - return "discoverable"; - } - return null; - } - - private static void log(String msg) { - Log.d(TAG, msg); - } -} diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index 8cc229bf7dfc09911acc069c913b5f39cfdbbbc1..c0b9a6843ea176beb0eb8624a911d1fad70df65f 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -17,16 +17,15 @@ package android.server; import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothIntent; -import android.bluetooth.IBluetoothDeviceCallback; +import android.bluetooth.BluetoothUuid; +import android.os.ParcelUuid; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; -import android.os.RemoteException; import android.util.Log; import java.util.HashMap; @@ -46,13 +45,20 @@ class BluetoothEventLoop { private Thread mThread; private boolean mStarted; private boolean mInterrupted; + private final HashMap mPasskeyAgentRequestData; - private final HashMap mGetRemoteServiceChannelCallbacks; - private final BluetoothDeviceService mBluetoothService; + private final BluetoothService mBluetoothService; + private final BluetoothAdapter mAdapter; private final Context mContext; private static final int EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 1; private static final int EVENT_RESTART_BLUETOOTH = 2; + private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 3; + private static final int EVENT_AGENT_CANCEL = 4; + + private static final int CREATE_DEVICE_ALREADY_EXISTS = 1; + private static final int CREATE_DEVICE_SUCCESS = 0; + private static final int CREATE_DEVICE_FAILED = -1; // The time (in millisecs) to delay the pairing attempt after the first // auto pairing attempt fails. We use an exponential delay with @@ -67,9 +73,10 @@ class BluetoothEventLoop { private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { + String address = null; switch (msg.what) { case EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY: - String address = (String)msg.obj; + address = (String)msg.obj; if (address != null) { mBluetoothService.createBond(address); return; @@ -78,6 +85,28 @@ class BluetoothEventLoop { case EVENT_RESTART_BLUETOOTH: mBluetoothService.restart(); break; + case EVENT_PAIRING_CONSENT_DELAYED_ACCEPT: + address = (String)msg.obj; + if (address != null) { + mBluetoothService.setPairingConfirmation(address, true); + } + break; + case EVENT_AGENT_CANCEL: + // Set the Bond State to BOND_NONE. + // We always have only 1 device in BONDING state. + String[] devices = + mBluetoothService.getBondState().listInState(BluetoothDevice.BOND_BONDING); + if (devices.length == 0) { + break; + } else if (devices.length > 1) { + Log.e(TAG, " There is more than one device in the Bonding State"); + break; + } + address = devices[0]; + mBluetoothService.getBondState().setBondState(address, + BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED); + break; } } }; @@ -85,14 +114,14 @@ class BluetoothEventLoop { static { classInitNative(); } private static native void classInitNative(); - /* pacakge */ BluetoothEventLoop(Context context, BluetoothDeviceService bluetoothService) { + /* pacakge */ BluetoothEventLoop(Context context, BluetoothAdapter adapter, + BluetoothService bluetoothService) { mBluetoothService = bluetoothService; mContext = context; mPasskeyAgentRequestData = new HashMap(); - mGetRemoteServiceChannelCallbacks = new HashMap(); + mAdapter = adapter; initializeNativeDataNative(); } - private native void initializeNativeDataNative(); protected void finalize() throws Throwable { try { @@ -101,20 +130,11 @@ class BluetoothEventLoop { super.finalize(); } } - private native void cleanupNativeDataNative(); - - /* pacakge */ HashMap getRemoteServiceChannelCallbacks() { - return mGetRemoteServiceChannelCallbacks; - } - /* pacakge */ HashMap getPasskeyAgentRequestData() { + /* package */ HashMap getPasskeyAgentRequestData() { return mPasskeyAgentRequestData; } - private native void startEventLoopNative(); - private native void stopEventLoopNative(); - private native boolean isEventLoopRunningNative(); - /* package */ void start() { if (!isEventLoopRunningNative()) { @@ -134,81 +154,61 @@ class BluetoothEventLoop { return isEventLoopRunningNative(); } - /*package*/ void onModeChanged(String bluezMode) { - int mode = BluetoothDeviceService.bluezStringToScanMode(bluezMode); - if (mode >= 0) { - Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.SCAN_MODE, mode); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + private void addDevice(String address, String[] properties) { + mBluetoothService.addRemoteDeviceProperties(address, properties); + String rssi = mBluetoothService.getRemoteDeviceProperty(address, "RSSI"); + String classValue = mBluetoothService.getRemoteDeviceProperty(address, "Class"); + String name = mBluetoothService.getRemoteDeviceProperty(address, "Name"); + short rssiValue; + // For incoming connections, we don't get the RSSI value. Use a default of MIN_VALUE. + // If we accept the pairing, we will automatically show it at the top of the list. + if (rssi != null) { + rssiValue = (short)Integer.valueOf(rssi).intValue(); + } else { + rssiValue = Short.MIN_VALUE; + } + if (classValue != null) { + Intent intent = new Intent(BluetoothDevice.ACTION_FOUND); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_CLASS, + new BluetoothClass(Integer.valueOf(classValue))); + intent.putExtra(BluetoothDevice.EXTRA_RSSI, rssiValue); + intent.putExtra(BluetoothDevice.EXTRA_NAME, name); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } else { + log ("ClassValue: " + classValue + " for remote device: " + address + " is null"); } } - private void onDiscoveryStarted() { - mBluetoothService.setIsDiscovering(true); - Intent intent = new Intent(BluetoothIntent.DISCOVERY_STARTED_ACTION); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onDiscoveryCompleted() { - mBluetoothService.setIsDiscovering(false); - Intent intent = new Intent(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); + private void onDeviceFound(String address, String[] properties) { + if (properties == null) { + Log.e(TAG, "ERROR: Remote device properties are null"); + return; + } + addDevice(address, properties); } - private void onRemoteDeviceFound(String address, int deviceClass, short rssi) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.CLASS, deviceClass); - intent.putExtra(BluetoothIntent.RSSI, rssi); + private void onDeviceDisappeared(String address) { + Intent intent = new Intent(BluetoothDevice.ACTION_DISAPPEARED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } - private void onRemoteDeviceDisappeared(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteClassUpdated(String address, int deviceClass) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.CLASS, deviceClass); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceConnected(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceDisconnectRequested(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceDisconnected(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameUpdated(String address, String name) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameFailed(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_FAILED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameChanged(String address, String name) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.NAME, name); + + private void onDeviceDisconnectRequested(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address == null) { + Log.e(TAG, "onDeviceDisconnectRequested: Address of the remote device in null"); + return; + } + Intent intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } - private void onCreateBondingResult(String address, int result) { + private void onCreatePairedDeviceResult(String address, int result) { address = address.toUpperCase(); - if (result == BluetoothError.SUCCESS) { + if (result == BluetoothDevice.BOND_SUCCESS) { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); @@ -222,7 +222,7 @@ class BluetoothEventLoop { pairingAttempt(address, result); } else { mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); } @@ -242,7 +242,7 @@ class BluetoothEventLoop { MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); return; } @@ -253,45 +253,249 @@ class BluetoothEventLoop { if (!postResult) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); return; } mBluetoothService.getBondState().attempt(address); } - private void onBondingCreated(String address) { - mBluetoothService.getBondState().setBondState(address.toUpperCase(), - BluetoothDevice.BOND_BONDED); + private void onDeviceCreated(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (!mBluetoothService.isRemoteDeviceInCache(address)) { + // Incoming connection, we haven't seen this device, add to cache. + String[] properties = mBluetoothService.getRemoteDeviceProperties(address); + if (properties != null) { + addDevice(address, properties); + } + } + return; } - private void onBondingRemoved(String address) { - mBluetoothService.getBondState().setBondState(address.toUpperCase(), - BluetoothDevice.BOND_NOT_BONDED, BluetoothDevice.UNBOND_REASON_REMOVED); + private void onDeviceRemoved(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address != null) + mBluetoothService.getBondState().setBondState(address.toUpperCase(), + BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED); } - private void onNameChanged(String name) { - Intent intent = new Intent(BluetoothIntent.NAME_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); + /*package*/ void onPropertyChanged(String[] propValues) { + if (mBluetoothService.isAdapterPropertiesEmpty()) { + // We have got a property change before + // we filled up our cache. + mBluetoothService.getAllProperties(); + } + String name = propValues[0]; + if (name.equals("Name")) { + Intent intent = new Intent(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_LOCAL_NAME, propValues[1]); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Pairable") || name.equals("Discoverable")) { + String pairable = name.equals("Pairable") ? propValues[1] : + mBluetoothService.getProperty("Pairable"); + String discoverable = name.equals("Discoverable") ? propValues[1] : + mBluetoothService.getProperty("Discoverable"); + + // This shouldn't happen, unless Adapter Properties are null. + if (pairable == null || discoverable == null) + return; + + int mode = BluetoothService.bluezStringToScanMode( + pairable.equals("true"), + discoverable.equals("true")); + if (mode >= 0) { + Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, mode); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Discovering")) { + Intent intent; + if (propValues[1].equals("true")) { + mBluetoothService.setIsDiscovering(true); + intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED); + } else { + // Stop the discovery. + mBluetoothService.cancelDiscovery(); + mBluetoothService.setIsDiscovering(false); + intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + } + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Devices")) { + String value = null; + int len = Integer.valueOf(propValues[1]); + if (len > 0) { + StringBuilder str = new StringBuilder(); + for (int i = 2; i < propValues.length; i++) { + str.append(propValues[i]); + str.append(","); + } + value = str.toString(); + } + mBluetoothService.setProperty(name, value); + } else if (name.equals("Powered")) { + // bluetoothd has restarted, re-read all our properties. + // Note: bluez only sends this property change when it restarts. + if (propValues[1].equals("true")) + onRestartRequired(); + } } - private void onPasskeyAgentRequest(String address, int nativeData) { + private void onDevicePropertyChanged(String deviceObjectPath, String[] propValues) { + String name = propValues[0]; + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address == null) { + Log.e(TAG, "onDevicePropertyChanged: Address of the remote device in null"); + return; + } + if (DBG) { + log("Device property changed:" + address + "property:" + name); + } + BluetoothDevice device = mAdapter.getRemoteDevice(address); + if (name.equals("Name")) { + Intent intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothDevice.EXTRA_NAME, propValues[1]); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("Class")) { + Intent intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothDevice.EXTRA_CLASS, + new BluetoothClass(Integer.valueOf(propValues[1]))); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("Connected")) { + Intent intent = null; + if (propValues[1].equals("true")) { + intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED); + } else { + intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED); + } + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("UUIDs")) { + String uuid = null; + int len = Integer.valueOf(propValues[1]); + if (len > 0) { + StringBuilder str = new StringBuilder(); + for (int i = 2; i < propValues.length; i++) { + str.append(propValues[i]); + str.append(","); + } + uuid = str.toString(); + } + mBluetoothService.setRemoteDeviceProperty(address, name, uuid); + + // UUIDs have changed, query remote service channel and update cache. + mBluetoothService.updateDeviceServiceChannelCache(address); + + mBluetoothService.sendUuidIntent(address); + } else if (name.equals("Paired")) { + if (propValues[1].equals("true")) { + mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); + } else { + mBluetoothService.getBondState().setBondState(address, + BluetoothDevice.BOND_NONE); + mBluetoothService.setRemoteDeviceProperty(address, "Trusted", "false"); + } + } else if (name.equals("Trusted")) { + if (DBG) + log("set trust state succeded, value is " + propValues[1]); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } + } + + private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) { + String address = mBluetoothService.getAddressFromObjectPath(objectPath); + if (address == null) { + Log.e(TAG, "Unable to get device address in checkPairingRequestAndGetAddress, " + + "returning null"); + return null; + } address = address.toUpperCase(); mPasskeyAgentRequestData.put(address, new Integer(nativeData)); - if (mBluetoothService.getBluetoothState() == BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF) { + if (mBluetoothService.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) { // shutdown path - mBluetoothService.cancelPin(address); + mBluetoothService.cancelPairingUserInput(address); + return null; + } + // Set state to BONDING. For incoming connections it will be set here. + // For outgoing connections, it gets set when we call createBond. + // Also set it only when the state is not already Bonded, we can sometimes + // get an authorization request from the remote end if it doesn't have the link key + // while we still have it. + if (mBluetoothService.getBondState().getBondState(address) != BluetoothDevice.BOND_BONDED) + mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDING); + return address; + } + + private void onRequestPairingConsent(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + /* The link key will not be stored if the incoming request has MITM + * protection switched on. Unfortunately, some devices have MITM + * switched on even though their capabilities are NoInputNoOutput, + * so we may get this request many times. Also if we respond immediately, + * the other end is unable to handle it. Delay sending the message. + */ + if (mBluetoothService.getBondState().getBondState(address) == BluetoothDevice.BOND_BONDED) { + Message message = mHandler.obtainMessage(EVENT_PAIRING_CONSENT_DELAYED_ACCEPT); + message.obj = address; + mHandler.sendMessageDelayed(message, 1500); return; } - if (mBluetoothService.getBondState().getBondState(address) == - BluetoothDevice.BOND_BONDING) { + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_CONSENT); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPasskeyConfirmation(String objectPath, int passkey, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPasskey(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_PASSKEY); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPinCode(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + String pendingOutgoingAddress = + mBluetoothService.getBondState().getPendingOutgoingBonding(); + if (address.equals(pendingOutgoingAddress)) { // we initiated the bonding - int btClass = mBluetoothService.getRemoteClass(address); + BluetoothClass btClass = new BluetoothClass(mBluetoothService.getRemoteClass(address)); // try 0000 once if the device looks dumb - switch (BluetoothClass.Device.getDevice(btClass)) { + switch (btClass.getDeviceClass()) { case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: @@ -306,56 +510,99 @@ class BluetoothEventLoop { } } } - Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PIN); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; } - private void onPasskeyAgentCancel(String address) { - address = address.toUpperCase(); - mBluetoothService.cancelPin(address); - Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); + private void onDisplayPasskey(String objectPath, int passkey, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); - mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); } - private boolean onAuthAgentAuthorize(String address, String service, String uuid) { + private boolean onAgentAuthorize(String objectPath, String deviceUuid) { + String address = mBluetoothService.getAddressFromObjectPath(objectPath); + if (address == null) { + Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); + return false; + } + boolean authorized = false; - if (mBluetoothService.isEnabled() && service.endsWith("service_audio")) { + ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); + // Bluez sends the UUID of the local service being accessed, _not_ the + // remote service + if (mBluetoothService.isEnabled() && + (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) + || BluetoothUuid.isAdvAudioDist(uuid))) { BluetoothA2dp a2dp = new BluetoothA2dp(mContext); - authorized = a2dp.getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; if (authorized) { - Log.i(TAG, "Allowing incoming A2DP connection from " + address); + Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); } else { - Log.i(TAG, "Rejecting incoming A2DP connection from " + address); + Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); } } else { - Log.i(TAG, "Rejecting incoming " + service + " connection from " + address); + Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } + log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized); return authorized; } - private void onAuthAgentCancel(String address, String service, String uuid) { - // We immediately response to DBUS Authorize() so this should not - // usually happen - log("onAuthAgentCancel(" + address + ", " + service + ", " + uuid + ")"); + private void onAgentCancel() { + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + + mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_AGENT_CANCEL), + 1500); + + return; + } + + private void onDiscoverServicesResult(String deviceObjectPath, boolean result) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + // We don't parse the xml here, instead just query Bluez for the properties. + if (result) { + mBluetoothService.updateRemoteDevicePropertiesCache(address); + } + mBluetoothService.sendUuidIntent(address); + mBluetoothService.makeServiceChannelCallbacks(address); } - private void onGetRemoteServiceChannelResult(String address, int channel) { - IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address); - if (callback != null) { - mGetRemoteServiceChannelCallbacks.remove(address); - try { - callback.onGetRemoteServiceChannelResult(address, channel); - } catch (RemoteException e) {} + private void onCreateDeviceResult(String address, int result) { + if (DBG) log("Result of onCreateDeviceResult:" + result); + + switch (result) { + case CREATE_DEVICE_ALREADY_EXISTS: + String path = mBluetoothService.getObjectPathFromAddress(address); + if (path != null) { + mBluetoothService.discoverServicesNative(path, ""); + break; + } + Log.w(TAG, "Device exists, but we dont have the bluez path, failing"); + // fall-through + case CREATE_DEVICE_FAILED: + mBluetoothService.sendUuidIntent(address); + mBluetoothService.makeServiceChannelCallbacks(address); + break; + case CREATE_DEVICE_SUCCESS: + // nothing to do, UUID intent's will be sent via property changed } } private void onRestartRequired() { if (mBluetoothService.isEnabled()) { - Log.e(TAG, "*** A serious error occured (did hcid crash?) - restarting Bluetooth ***"); + Log.e(TAG, "*** A serious error occured (did bluetoothd crash?) - " + + "restarting Bluetooth ***"); mHandler.sendEmptyMessage(EVENT_RESTART_BLUETOOTH); } } @@ -363,4 +610,10 @@ class BluetoothEventLoop { private static void log(String msg) { Log.d(TAG, msg); } + + private native void initializeNativeDataNative(); + private native void startEventLoopNative(); + private native void stopEventLoopNative(); + private native boolean isEventLoopRunningNative(); + private native void cleanupNativeDataNative(); } diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java new file mode 100644 index 0000000000000000000000000000000000000000..d1dd3110286b5883b675b9359d128886cbdb760a --- /dev/null +++ b/core/java/android/server/BluetoothService.java @@ -0,0 +1,1673 @@ +/* + * 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. + */ + +/** + * TODO: Move this to + * java/services/com/android/server/BluetoothService.java + * and make the contructor package private again. + * + * @hide + */ + +package android.server; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothCallback; +import android.os.ParcelUuid; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Binder; +import android.os.IBinder; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemService; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.app.IBatteryStats; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class BluetoothService extends IBluetooth.Stub { + private static final String TAG = "BluetoothService"; + private static final boolean DBG = false; + + private int mNativeData; + private BluetoothEventLoop mEventLoop; + private IntentFilter mIntentFilter; + private boolean mIsAirplaneSensitive; + private int mBluetoothState; + private boolean mRestart = false; // need to call enable() after disable() + private boolean mIsDiscovering; + + private BluetoothAdapter mAdapter; // constant after init() + private final BondState mBondState = new BondState(); // local cache of bondings + private final IBatteryStats mBatteryStats; + private final Context mContext; + + 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 int MESSAGE_REGISTER_SDP_RECORDS = 1; + private static final int MESSAGE_FINISH_DISABLE = 2; + private static final int MESSAGE_UUID_INTENT = 3; + private static final int MESSAGE_DISCOVERABLE_TIMEOUT = 4; + + // The timeout used to sent the UUIDs Intent + // This timeout should be greater than the page timeout + private static final int UUID_INTENT_DELAY = 6000; + + /** Always retrieve RFCOMM channel for these SDP UUIDs */ + private static final ParcelUuid[] RFCOMM_UUIDS = { + BluetoothUuid.Handsfree, + BluetoothUuid.HSP, + BluetoothUuid.ObexObjectPush }; + + + private final Map mAdapterProperties; + private final HashMap> mDeviceProperties; + + private final HashMap> mDeviceServiceChannelCache; + private final ArrayList mUuidIntentTracker; + private final HashMap mUuidCallbackTracker; + + private final HashMap mServiceRecordToPid; + + private static class RemoteService { + public String address; + public ParcelUuid uuid; + public RemoteService(String address, ParcelUuid uuid) { + this.address = address; + this.uuid = uuid; + } + @Override + public boolean equals(Object o) { + if (o instanceof RemoteService) { + RemoteService service = (RemoteService)o; + return address.equals(service.address) && uuid.equals(service.uuid); + } + return false; + } + } + + static { + classInitNative(); + } + + public BluetoothService(Context context) { + mContext = context; + + // Need to do this in place of: + // mBatteryStats = BatteryStatsService.getService(); + // Since we can not import BatteryStatsService from here. This class really needs to be + // moved to java/services/com/android/server/ + mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); + + initializeNativeDataNative(); + + if (isEnabledNative() == 1) { + Log.w(TAG, "Bluetooth daemons already running - runtime restart? "); + disableNative(); + } + + mBluetoothState = BluetoothAdapter.STATE_OFF; + mIsDiscovering = false; + mAdapterProperties = new HashMap(); + mDeviceProperties = new HashMap>(); + + mDeviceServiceChannelCache = new HashMap>(); + mUuidIntentTracker = new ArrayList(); + mUuidCallbackTracker = new HashMap(); + mServiceRecordToPid = new HashMap(); + registerForAirplaneMode(); + } + + public synchronized void initAfterRegistration() { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mEventLoop = new BluetoothEventLoop(mContext, mAdapter, this); + } + + @Override + protected void finalize() throws Throwable { + if (mIsAirplaneSensitive) { + mContext.unregisterReceiver(mReceiver); + } + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + + public boolean isEnabled() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBluetoothState == BluetoothAdapter.STATE_ON; + } + + public int getBluetoothState() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBluetoothState; + } + + + /** + * Bring down bluetooth and disable BT in settings. Returns true on success. + */ + public boolean disable() { + return disable(true); + } + + /** + * Bring down bluetooth. Returns true on success. + * + * @param saveSetting If true, persist the new setting + */ + public synchronized boolean disable(boolean saveSetting) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); + + switch (mBluetoothState) { + case BluetoothAdapter.STATE_OFF: + return true; + case BluetoothAdapter.STATE_ON: + break; + default: + return false; + } + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + setBluetoothState(BluetoothAdapter.STATE_TURNING_OFF); + mHandler.removeMessages(MESSAGE_REGISTER_SDP_RECORDS); + + // Allow 3 seconds for profiles to gracefully disconnect + // TODO: Introduce a callback mechanism so that each profile can notify + // BluetoothService when it is done shutting down + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_FINISH_DISABLE, saveSetting ? 1 : 0, 0), 3000); + return true; + } + + + private synchronized void finishDisable(boolean saveSetting) { + if (mBluetoothState != BluetoothAdapter.STATE_TURNING_OFF) { + return; + } + mEventLoop.stop(); + tearDownNativeDataNative(); + disableNative(); + + // mark in progress bondings as cancelled + for (String address : mBondState.listInState(BluetoothDevice.BOND_BONDING)) { + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + } + + // update mode + Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.SCAN_MODE_NONE); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + mIsDiscovering = false; + mAdapterProperties.clear(); + mServiceRecordToPid.clear(); + + if (saveSetting) { + persistBluetoothOnSetting(false); + } + + setBluetoothState(BluetoothAdapter.STATE_OFF); + + // Log bluetooth off to battery stats. + long ident = Binder.clearCallingIdentity(); + try { + mBatteryStats.noteBluetoothOff(); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + + if (mRestart) { + mRestart = false; + enable(); + } + } + + /** Bring up BT and persist BT on in settings */ + public boolean enable() { + return enable(true); + } + + /** + * Enable this Bluetooth device, asynchronously. + * This turns on/off the underlying hardware. + * + * @param saveSetting If true, persist the new state of BT in settings + * @return True on success (so far) + */ + public synchronized boolean enable(boolean saveSetting) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + // Airplane mode can prevent Bluetooth radio from being turned on. + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + return false; + } + if (mBluetoothState != BluetoothAdapter.STATE_OFF) { + return false; + } + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + setBluetoothState(BluetoothAdapter.STATE_TURNING_ON); + mEnableThread = new EnableThread(saveSetting); + mEnableThread.start(); + return true; + } + + /** Forcibly restart Bluetooth if it is on */ + /* package */ synchronized void restart() { + if (mBluetoothState != BluetoothAdapter.STATE_ON) { + return; + } + mRestart = true; + if (!disable(false)) { + mRestart = false; + } + } + + private synchronized void setBluetoothState(int state) { + if (state == mBluetoothState) { + return; + } + + if (DBG) log("Bluetooth state " + mBluetoothState + " -> " + state); + + Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, mBluetoothState); + intent.putExtra(BluetoothAdapter.EXTRA_STATE, state); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + + mBluetoothState = state; + + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_REGISTER_SDP_RECORDS: + if (!isEnabled()) { + return; + } + // SystemService.start() forks sdptool to register service + // records. It can fail to register some records if it is + // forked multiple times in a row, probably because there is + // some race in sdptool or bluez when operated in parallel. + // As a workaround, delay 500ms between each fork of sdptool. + // TODO: Don't fork sdptool in order to regsiter service + // records, use a DBUS call instead. + switch (msg.arg1) { + case 1: + Log.d(TAG, "Registering hsag record"); + SystemService.start("hsag"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 2, -1), 500); + break; + case 2: + Log.d(TAG, "Registering hfag record"); + SystemService.start("hfag"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 3, -1), 500); + break; + case 3: + Log.d(TAG, "Registering opush record"); + SystemService.start("opush"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 4, -1), 500); + break; + case 4: + Log.d(TAG, "Registering pbap record"); + SystemService.start("pbap"); + break; + } + break; + case MESSAGE_FINISH_DISABLE: + finishDisable(msg.arg1 != 0); + break; + case MESSAGE_UUID_INTENT: + String address = (String)msg.obj; + if (address != null) { + sendUuidIntent(address); + makeServiceChannelCallbacks(address); + } + break; + case MESSAGE_DISCOVERABLE_TIMEOUT: + int mode = msg.arg1; + if (isEnabled()) { + // TODO: Switch back to the previous scan mode + // This is ok for now, because we only use + // CONNECTABLE and CONNECTABLE_DISCOVERABLE + setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE, -1); + } + break; + } + } + }; + + private EnableThread mEnableThread; + + private class EnableThread extends Thread { + private final boolean mSaveSetting; + public EnableThread(boolean saveSetting) { + mSaveSetting = saveSetting; + } + public void run() { + boolean res = (enableNative() == 0); + if (res) { + int retryCount = 2; + boolean running = false; + while ((retryCount-- > 0) && !running) { + mEventLoop.start(); + // it may take a momement for the other thread to do its + // thing. Check periodically for a while. + int pollCount = 5; + while ((pollCount-- > 0) && !running) { + if (mEventLoop.isEventLoopRunning()) { + running = true; + break; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + } + } + if (!running) { + log("bt EnableThread giving up"); + res = false; + disableNative(); + } + } + + + if (res) { + if (!setupNativeDataNative()) { + return; + } + if (mSaveSetting) { + persistBluetoothOnSetting(true); + } + mIsDiscovering = false; + mBondState.loadBondState(); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 1, -1), 3000); + + // Log bluetooth on to battery stats. + long ident = Binder.clearCallingIdentity(); + try { + mBatteryStats.noteBluetoothOn(); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + mEnableThread = null; + + setBluetoothState(res ? + BluetoothAdapter.STATE_ON : + BluetoothAdapter.STATE_OFF); + + if (res) { + // Update mode + String[] propVal = {"Pairable", getProperty("Pairable")}; + mEventLoop.onPropertyChanged(propVal); + } + + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + disable(false); + } + + } + } + + private void persistBluetoothOnSetting(boolean bluetoothOn) { + long origCallerIdentityToken = Binder.clearCallingIdentity(); + Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, + bluetoothOn ? 1 : 0); + Binder.restoreCallingIdentity(origCallerIdentityToken); + } + + /* package */ BondState getBondState() { + return mBondState; + } + + /** local cache of bonding state. + /* we keep our own state to track the intermediate state BONDING, which + /* bluez does not track. + * All addreses must be passed in upper case. + */ + public class BondState { + private final HashMap mState = new HashMap(); + private final HashMap mPinAttempt = new HashMap(); + private final ArrayList mAutoPairingFailures = new ArrayList(); + // List of all the vendor_id prefix of Bluetooth addresses for + // which auto pairing is not attempted. + // The following companies are included in the list below: + // ALPS (lexus), Murata (Prius 2007, Nokia 616), TEMIC SDS (Porsche, Audi), + // Parrot, Zhongshan General K-mate Electronics, Great Well + // Electronics, Flaircomm Electronics, Jatty Electronics, Delphi, + // Clarion, Novero, Denso (Lexus, Toyota), Johnson Controls (Acura), + // Continental Automotive, Harman/Becker, Panasonic/Kyushu Ten, + // BMW (Motorola PCS) + private final ArrayList mAutoPairingAddressBlacklist = + new ArrayList(Arrays.asList( + "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", "00:21:4F", + "00:23:06", "00:24:33", "00:A0:79", "00:0E:6D", "00:13:E0", "00:21:E8", + "00:60:57", "00:0E:9F", "00:12:1C", "00:18:91", "00:18:96", "00:13:04", + "00:16:FD", "00:22:A0", "00:0B:4C", "00:60:6F", "00:23:3D", "00:C0:59", + "00:0A:30", "00:1E:AE", "00:1C:D7", "00:80:F0", "00:12:8A" + )); + + // List of names of Bluetooth devices for which auto pairing should be + // disabled. + private final ArrayList mAutoPairingNameBlacklist = + new ArrayList(Arrays.asList( + "Motorola IHF1000", "i.TechBlueBAND", "X5 Stereo v1.3")); + + // If this is an outgoing connection, store the address. + // There can be only 1 pending outgoing connection at a time, + private String mPendingOutgoingBonding; + + private synchronized void setPendingOutgoingBonding(String address) { + mPendingOutgoingBonding = address; + } + + public synchronized String getPendingOutgoingBonding() { + return mPendingOutgoingBonding; + } + + public synchronized void loadBondState() { + if (mBluetoothState != BluetoothAdapter.STATE_TURNING_ON) { + return; + } + String []bonds = null; + String val = getProperty("Devices"); + if (val != null) { + bonds = val.split(","); + } + if (bonds == null) { + return; + } + mState.clear(); + if (DBG) log("found " + bonds.length + " bonded devices"); + for (String device : bonds) { + mState.put(getAddressFromObjectPath(device).toUpperCase(), + BluetoothDevice.BOND_BONDED); + } + } + + public synchronized void setBondState(String address, int state) { + setBondState(address, state, 0); + } + + /** reason is ignored unless state == BOND_NOT_BONDED */ + public synchronized void setBondState(String address, int state, int reason) { + int oldState = getBondState(address); + if (oldState == state) { + return; + } + + // Check if this was an pending outgoing bonding. + // If yes, reset the state. + if (oldState == BluetoothDevice.BOND_BONDING) { + if (address.equals(mPendingOutgoingBonding)) { + mPendingOutgoingBonding = null; + } + } + + if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + + reason + ")"); + Intent intent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, state); + intent.putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, oldState); + if (state == BluetoothDevice.BOND_NONE) { + if (reason <= 0) { + Log.w(TAG, "setBondState() called to unbond device, but reason code is " + + "invalid. Overriding reason code with BOND_RESULT_REMOVED"); + reason = BluetoothDevice.UNBOND_REASON_REMOVED; + } + intent.putExtra(BluetoothDevice.EXTRA_REASON, reason); + mState.remove(address); + } else { + mState.put(address, state); + } + + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + + public boolean isAutoPairingBlacklisted(String address) { + for (String blacklistAddress : mAutoPairingAddressBlacklist) { + if (address.startsWith(blacklistAddress)) return true; + } + + String name = getRemoteName(address); + if (name != null) { + for (String blacklistName : mAutoPairingNameBlacklist) { + if (name.equals(blacklistName)) return true; + } + } + return false; + } + + public synchronized int getBondState(String address) { + Integer state = mState.get(address); + if (state == null) { + return BluetoothDevice.BOND_NONE; + } + return state.intValue(); + } + + /*package*/ synchronized String[] listInState(int state) { + ArrayList result = new ArrayList(mState.size()); + for (Map.Entry e : mState.entrySet()) { + if (e.getValue().intValue() == state) { + result.add(e.getKey()); + } + } + return result.toArray(new String[result.size()]); + } + + public synchronized void addAutoPairingFailure(String address) { + if (!mAutoPairingFailures.contains(address)) { + mAutoPairingFailures.add(address); + } + } + + public synchronized boolean isAutoPairingAttemptsInProgress(String address) { + return getAttempt(address) != 0; + } + + public synchronized void clearPinAttempts(String address) { + mPinAttempt.remove(address); + } + + public synchronized boolean hasAutoPairingFailed(String address) { + return mAutoPairingFailures.contains(address); + } + + public synchronized int getAttempt(String address) { + Integer attempt = mPinAttempt.get(address); + if (attempt == null) { + return 0; + } + return attempt.intValue(); + } + + public synchronized void attempt(String address) { + Integer attempt = mPinAttempt.get(address); + int newAttempt; + if (attempt == null) { + newAttempt = 1; + } else { + newAttempt = attempt.intValue() + 1; + } + mPinAttempt.put(address, new Integer(newAttempt)); + } + + } + + private static String toBondStateString(int bondState) { + switch (bondState) { + case BluetoothDevice.BOND_NONE: + return "not bonded"; + case BluetoothDevice.BOND_BONDING: + return "bonding"; + case BluetoothDevice.BOND_BONDED: + return "bonded"; + default: + return "??????"; + } + } + + /*package*/ synchronized boolean isAdapterPropertiesEmpty() { + return mAdapterProperties.isEmpty(); + } + + /*package*/synchronized void getAllProperties() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + mAdapterProperties.clear(); + + String properties[] = (String [])getAdapterPropertiesNative(); + // The String Array consists of key-value pairs. + if (properties == null) { + Log.e(TAG, "*Error*: GetAdapterProperties returned NULL"); + return; + } + + for (int i = 0; i < properties.length; i++) { + String name = properties[i]; + String newValue = null; + int len; + if (name == null) { + Log.e(TAG, "Error:Adapter Property at index" + i + "is null"); + continue; + } + if (name.equals("Devices")) { + StringBuilder str = new StringBuilder(); + len = Integer.valueOf(properties[++i]); + for (int j = 0; j < len; j++) { + str.append(properties[++i]); + str.append(","); + } + if (len > 0) { + newValue = str.toString(); + } + } else { + newValue = properties[++i]; + } + mAdapterProperties.put(name, newValue); + } + + // Add adapter object path property. + String adapterPath = getAdapterPathNative(); + if (adapterPath != null) + mAdapterProperties.put("ObjectPath", adapterPath + "/dev_"); + } + + /* package */ synchronized void setProperty(String name, String value) { + mAdapterProperties.put(name, value); + } + + public synchronized boolean setName(String name) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (name == null) { + return false; + } + return setPropertyString("Name", name); + } + + //TODO(): setPropertyString, setPropertyInteger, setPropertyBoolean + // Either have a single property function with Object as the parameter + // or have a function for each property and then obfuscate in the JNI layer. + // The following looks dirty. + private boolean setPropertyString(String key, String value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyStringNative(key, value); + } + + private boolean setPropertyInteger(String key, int value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyIntegerNative(key, value); + } + + private boolean setPropertyBoolean(String key, boolean value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyBooleanNative(key, value ? 1 : 0); + } + + /** + * Set the discoverability window for the device. A timeout of zero + * makes the device permanently discoverable (if the device is + * discoverable). Setting the timeout to a nonzero value does not make + * a device discoverable; you need to call setMode() to make the device + * explicitly discoverable. + * + * @param timeout_s The discoverable timeout in seconds. + */ + public synchronized boolean setDiscoverableTimeout(int timeout) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return setPropertyInteger("DiscoverableTimeout", timeout); + } + + public synchronized boolean setScanMode(int mode, int duration) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS, + "Need WRITE_SECURE_SETTINGS permission"); + boolean pairable = false; + boolean discoverable = false; + + switch (mode) { + case BluetoothAdapter.SCAN_MODE_NONE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = false; + discoverable = false; + break; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = true; + discoverable = false; + break; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = true; + discoverable = true; + Message msg = mHandler.obtainMessage(MESSAGE_DISCOVERABLE_TIMEOUT); + mHandler.sendMessageDelayed(msg, duration * 1000); + if (DBG) Log.d(TAG, "BT Discoverable for " + duration + " seconds"); + break; + default: + Log.w(TAG, "Requested invalid scan mode " + mode); + return false; + } + setPropertyBoolean("Pairable", pairable); + setPropertyBoolean("Discoverable", discoverable); + + return true; + } + + /*package*/ synchronized String getProperty (String name) { + if (!mAdapterProperties.isEmpty()) + return mAdapterProperties.get(name); + getAllProperties(); + return mAdapterProperties.get(name); + } + + public synchronized String getAddress() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return getProperty("Address"); + } + + public synchronized String getName() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return getProperty("Name"); + } + + /** + * Returns the user-friendly name of a remote device. This value is + * returned from our local cache, which is updated when onPropertyChange + * event is received. + * Do not expect to retrieve the updated remote name immediately after + * changing the name on the remote device. + * + * @param address Bluetooth address of remote device. + * + * @return The user-friendly name of the specified remote device. + */ + public synchronized String getRemoteName(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return null; + } + return getRemoteDeviceProperty(address, "Name"); + } + + /** + * Get the discoverability window for the device. A timeout of zero + * means that the device is permanently discoverable (if the device is + * in the discoverable mode). + * + * @return The discoverability window of the device, in seconds. A negative + * value indicates an error. + */ + public synchronized int getDiscoverableTimeout() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + String timeout = getProperty("DiscoverableTimeout"); + if (timeout != null) + return Integer.valueOf(timeout); + else + return -1; + } + + public synchronized int getScanMode() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!isEnabled()) + return BluetoothAdapter.SCAN_MODE_NONE; + + boolean pairable = getProperty("Pairable").equals("true"); + boolean discoverable = getProperty("Discoverable").equals("true"); + return bluezStringToScanMode (pairable, discoverable); + } + + public synchronized boolean startDiscovery() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!isEnabled()) { + return false; + } + return startDiscoveryNative(); + } + + public synchronized boolean cancelDiscovery() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return stopDiscoveryNative(); + } + + public synchronized boolean isDiscovering() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mIsDiscovering; + } + + /* package */ void setIsDiscovering(boolean isDiscovering) { + mIsDiscovering = isDiscovering; + } + + public synchronized boolean createBond(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + + if (mBondState.getPendingOutgoingBonding() != null) { + log("Ignoring createBond(): another device is bonding"); + // a different device is currently bonding, fail + return false; + } + + // Check for bond state only if we are not performing auto + // pairing exponential back-off attempts. + if (!mBondState.isAutoPairingAttemptsInProgress(address) && + mBondState.getBondState(address) != BluetoothDevice.BOND_NONE) { + log("Ignoring createBond(): this device is already bonding or bonded"); + return false; + } + + if (!createPairedDeviceNative(address, 60000 /* 1 minute */)) { + return false; + } + + mBondState.setPendingOutgoingBonding(address); + mBondState.setBondState(address, BluetoothDevice.BOND_BONDING); + + return true; + } + + public synchronized boolean cancelBondProcess(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + if (mBondState.getBondState(address) != BluetoothDevice.BOND_BONDING) { + return false; + } + + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + cancelDeviceCreationNative(address); + return true; + } + + public synchronized boolean removeBond(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + return removeDeviceNative(getObjectPathFromAddress(address)); + } + + public synchronized String[] listBonds() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBondState.listInState(BluetoothDevice.BOND_BONDED); + } + + public synchronized int getBondState(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return BluetoothDevice.ERROR; + } + return mBondState.getBondState(address.toUpperCase()); + } + + /*package*/ boolean isRemoteDeviceInCache(String address) { + return (mDeviceProperties.get(address) != null); + } + + /*package*/ String[] getRemoteDeviceProperties(String address) { + String objectPath = getObjectPathFromAddress(address); + return (String [])getDevicePropertiesNative(objectPath); + } + + /*package*/ synchronized String getRemoteDeviceProperty(String address, String property) { + Map properties = mDeviceProperties.get(address); + if (properties != null) { + return properties.get(property); + } else { + // Query for remote device properties, again. + // We will need to reload the cache when we switch Bluetooth on / off + // or if we crash. + if (updateRemoteDevicePropertiesCache(address)) + return getRemoteDeviceProperty(address, property); + } + Log.e(TAG, "getRemoteDeviceProperty: " + property + "not present:" + address); + return null; + } + + /* package */ synchronized boolean updateRemoteDevicePropertiesCache(String address) { + String[] propValues = getRemoteDeviceProperties(address); + if (propValues != null) { + addRemoteDeviceProperties(address, propValues); + return true; + } + return false; + } + + /* package */ synchronized void addRemoteDeviceProperties(String address, String[] properties) { + /* + * We get a DeviceFound signal every time RSSI changes or name changes. + * Don't create a new Map object every time */ + Map propertyValues = mDeviceProperties.get(address); + if (propertyValues == null) { + propertyValues = new HashMap(); + } + + for (int i = 0; i < properties.length; i++) { + String name = properties[i]; + String newValue = null; + int len; + if (name == null) { + Log.e(TAG, "Error: Remote Device Property at index" + i + "is null"); + continue; + } + if (name.equals("UUIDs") || name.equals("Nodes")) { + StringBuilder str = new StringBuilder(); + len = Integer.valueOf(properties[++i]); + for (int j = 0; j < len; j++) { + str.append(properties[++i]); + str.append(","); + } + if (len > 0) { + newValue = str.toString(); + } + } else { + newValue = properties[++i]; + } + + propertyValues.put(name, newValue); + } + mDeviceProperties.put(address, propertyValues); + + // We have added a new remote device or updated its properties. + // Also update the serviceChannel cache. + updateDeviceServiceChannelCache(address); + } + + /* package */ void removeRemoteDeviceProperties(String address) { + mDeviceProperties.remove(address); + } + + /* package */ synchronized void setRemoteDeviceProperty(String address, String name, + String value) { + Map propVal = mDeviceProperties.get(address); + if (propVal != null) { + propVal.put(name, value); + mDeviceProperties.put(address, propVal); + } else { + Log.e(TAG, "setRemoteDeviceProperty for a device not in cache:" + address); + } + } + + /** + * Sets the remote device trust state. + * + * @return boolean to indicate operation success or fail + */ + public synchronized boolean setTrust(String address, boolean value) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return false; + } + + return setDevicePropertyBooleanNative(getObjectPathFromAddress(address), "Trusted", + value ? 1 : 0); + } + + /** + * Gets the remote device trust state as boolean. + * Note: this value may be + * retrieved from cache if we retrieved the data before * + * + * @return boolean to indicate trust or untrust state + */ + public synchronized boolean getTrustState(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return false; + } + + String val = getRemoteDeviceProperty(address, "Trusted"); + if (val == null) { + return false; + } else { + return val.equals("true") ? true : false; + } + } + + /** + * Gets the remote major, minor classes encoded as a 32-bit + * integer. + * + * Note: this value is retrieved from cache, because we get it during + * remote-device discovery. + * + * @return 32-bit integer encoding the remote major, minor, and service + * classes. + */ + public synchronized int getRemoteClass(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return BluetoothClass.ERROR; + } + String val = getRemoteDeviceProperty(address, "Class"); + if (val == null) + return BluetoothClass.ERROR; + else { + return Integer.valueOf(val); + } + } + + + /** + * Gets the UUIDs supported by the remote device + * + * @return array of 128bit ParcelUuids + */ + public synchronized ParcelUuid[] getRemoteUuids(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return null; + } + return getUuidFromCache(address); + } + + private ParcelUuid[] getUuidFromCache(String address) { + String value = getRemoteDeviceProperty(address, "UUIDs"); + if (value == null) return null; + + String[] uuidStrings = null; + // The UUIDs are stored as a "," separated string. + uuidStrings = value.split(","); + ParcelUuid[] uuids = new ParcelUuid[uuidStrings.length]; + + for (int i = 0; i < uuidStrings.length; i++) { + uuids[i] = ParcelUuid.fromString(uuidStrings[i]); + } + return uuids; + } + + /** + * Connect and fetch new UUID's using SDP. + * The UUID's found are broadcast as intents. + * Optionally takes a uuid and callback to fetch the RFCOMM channel for the + * a given uuid. + * TODO: Don't wait UUID_INTENT_DELAY to broadcast UUID intents on success + * TODO: Don't wait UUID_INTENT_DELAY to handle the failure case for + * callback and broadcast intents. + */ + public synchronized boolean fetchRemoteUuids(String address, ParcelUuid uuid, + IBluetoothCallback callback) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + + RemoteService service = new RemoteService(address, uuid); + if (uuid != null && mUuidCallbackTracker.get(service) != null) { + // An SDP query for this address & uuid is already in progress + // Do not add this callback for the uuid + return false; + } + + if (mUuidIntentTracker.contains(address)) { + // An SDP query for this address is already in progress + // Add this uuid onto the in-progress SDP query + if (uuid != null) { + mUuidCallbackTracker.put(new RemoteService(address, uuid), callback); + } + return true; + } + + boolean ret; + if (getBondState(address) == BluetoothDevice.BOND_BONDED) { + String path = getObjectPathFromAddress(address); + if (path == null) return false; + + // Use an empty string for the UUID pattern + ret = discoverServicesNative(path, ""); + } else { + ret = createDeviceNative(address); + } + + mUuidIntentTracker.add(address); + if (uuid != null) { + mUuidCallbackTracker.put(new RemoteService(address, uuid), callback); + } + + Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT); + message.obj = address; + mHandler.sendMessageDelayed(message, UUID_INTENT_DELAY); + return ret; + } + + /** + * Gets the rfcomm channel associated with the UUID. + * Pulls records from the cache only. + * + * @param address Address of the remote device + * @param uuid ParcelUuid of the service attribute + * + * @return rfcomm channel associated with the service attribute + * -1 on error + */ + public int getRemoteServiceChannel(String address, ParcelUuid uuid) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return BluetoothDevice.ERROR; + } + // Check if we are recovering from a crash. + if (mDeviceProperties.isEmpty()) { + if (!updateRemoteDevicePropertiesCache(address)) + return -1; + } + + Map value = mDeviceServiceChannelCache.get(address); + if (value != null && value.containsKey(uuid)) + return value.get(uuid); + return -1; + } + + public synchronized boolean setPin(String address, byte[] pin) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (pin == null || pin.length <= 0 || pin.length > 16 || + !BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPin(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + // bluez API wants pin as a string + String pinString; + try { + pinString = new String(pin, "UTF8"); + } catch (UnsupportedEncodingException uee) { + Log.e(TAG, "UTF8 not supported?!?"); + return false; + } + return setPinNative(address, pinString, data.intValue()); + } + + public synchronized boolean setPasskey(String address, int passkey) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (passkey < 0 || passkey > 999999 || !BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPasskey(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + return setPasskeyNative(address, passkey, data.intValue()); + } + + public synchronized boolean setPairingConfirmation(String address, boolean confirm) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPasskey(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + return setPairingConfirmationNative(address, confirm, data.intValue()); + } + + public synchronized boolean cancelPairingUserInput(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "cancelUserInputNative(" + address + ") called but no native data " + + "available, ignoring. Maybe the PasskeyAgent Request was already cancelled " + + "by the remote or by bluez.\n"); + return false; + } + return cancelPairingUserInputNative(address, data.intValue()); + } + + public void updateDeviceServiceChannelCache(String address) { + ParcelUuid[] deviceUuids = getRemoteUuids(address); + // We are storing the rfcomm channel numbers only for the uuids + // we are interested in. + int channel; + if (DBG) log("updateDeviceServiceChannelCache(" + address + ")"); + + ArrayList applicationUuids = new ArrayList(); + + synchronized (this) { + for (RemoteService service : mUuidCallbackTracker.keySet()) { + if (service.address.equals(address)) { + applicationUuids.add(service.uuid); + } + } + } + + Map value = new HashMap(); + + // Retrieve RFCOMM channel for default uuids + for (ParcelUuid uuid : RFCOMM_UUIDS) { + if (BluetoothUuid.isUuidPresent(deviceUuids, uuid)) { + channel = getDeviceServiceChannelNative(getObjectPathFromAddress(address), + uuid.toString(), 0x0004); + if (DBG) log("\tuuid(system): " + uuid + " " + channel); + value.put(uuid, channel); + } + } + // Retrieve RFCOMM channel for application requested uuids + for (ParcelUuid uuid : applicationUuids) { + if (BluetoothUuid.isUuidPresent(deviceUuids, uuid)) { + channel = getDeviceServiceChannelNative(getObjectPathFromAddress(address), + uuid.toString(), 0x0004); + if (DBG) log("\tuuid(application): " + uuid + " " + channel); + value.put(uuid, channel); + } + } + + synchronized (this) { + // Make application callbacks + for (Iterator iter = mUuidCallbackTracker.keySet().iterator(); + iter.hasNext();) { + RemoteService service = iter.next(); + if (service.address.equals(address)) { + channel = -1; + if (value.get(service.uuid) != null) { + channel = value.get(service.uuid); + } + if (channel != -1) { + if (DBG) log("Making callback for " + service.uuid + " with result " + + channel); + IBluetoothCallback callback = mUuidCallbackTracker.get(service); + if (callback != null) { + try { + callback.onRfcommChannelFound(channel); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + iter.remove(); + } + } + } + + // Update cache + mDeviceServiceChannelCache.put(address, value); + } + } + + /** + * b is a handle to a Binder instance, so that this service can be notified + * for Applications that terminate unexpectedly, to clean there service + * records + */ + public synchronized int addRfcommServiceRecord(String serviceName, ParcelUuid uuid, + int channel, IBinder b) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + if (serviceName == null || uuid == null || channel < 1 || + channel > BluetoothSocket.MAX_RFCOMM_CHANNEL) { + return -1; + } + if (BluetoothUuid.isUuidPresent(BluetoothUuid.RESERVED_UUIDS, uuid)) { + Log.w(TAG, "Attempted to register a reserved UUID: " + uuid); + return -1; + } + int handle = addRfcommServiceRecordNative(serviceName, + uuid.getUuid().getMostSignificantBits(), uuid.getUuid().getLeastSignificantBits(), + (short)channel); + if (DBG) log("new handle " + Integer.toHexString(handle)); + if (handle == -1) { + return -1; + } + + int pid = Binder.getCallingPid(); + mServiceRecordToPid.put(new Integer(handle), new Integer(pid)); + try { + b.linkToDeath(new Reaper(handle, pid), 0); + } catch (RemoteException e) {} + return handle; + } + + public void removeServiceRecord(int handle) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + checkAndRemoveRecord(handle, Binder.getCallingPid()); + } + + private synchronized void checkAndRemoveRecord(int handle, int pid) { + Integer handleInt = new Integer(handle); + Integer owner = mServiceRecordToPid.get(handleInt); + if (owner != null && pid == owner.intValue()) { + if (DBG) log("Removing service record " + Integer.toHexString(handle) + " for pid " + + pid); + mServiceRecordToPid.remove(handleInt); + removeServiceRecordNative(handle); + } + } + + private class Reaper implements IBinder.DeathRecipient { + int pid; + int handle; + Reaper(int handle, int pid) { + this.pid = pid; + this.handle = handle; + } + public void binderDied() { + synchronized (BluetoothService.this) { + if (DBG) log("Tracked app " + pid + " died"); + checkAndRemoveRecord(handle, pid); + } + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { + ContentResolver resolver = context.getContentResolver(); + // Query the airplane mode from Settings.System just to make sure that + // some random app is not sending this intent and disabling bluetooth + boolean enabled = !isAirplaneModeOn(); + // If bluetooth is currently expected to be on, then enable or disable bluetooth + if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { + if (enabled) { + enable(false); + } else { + disable(false); + } + } + } + } + }; + + private void registerForAirplaneMode() { + String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_RADIOS); + mIsAirplaneSensitive = airplaneModeRadios == null + ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); + if (mIsAirplaneSensitive) { + mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); + mContext.registerReceiver(mReceiver, mIntentFilter); + } + } + + /* Returns true if airplane mode is currently on */ + private final boolean isAirplaneModeOn() { + return Settings.System.getInt(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_ON, 0) == 1; + } + + /* Broadcast the Uuid intent */ + /*package*/ synchronized void sendUuidIntent(String address) { + ParcelUuid[] uuid = getUuidFromCache(address); + Intent intent = new Intent(BluetoothDevice.ACTION_UUID); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_UUID, uuid); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + + if (mUuidIntentTracker.contains(address)) + mUuidIntentTracker.remove(address); + + } + + /*package*/ synchronized void makeServiceChannelCallbacks(String address) { + for (Iterator iter = mUuidCallbackTracker.keySet().iterator(); + iter.hasNext();) { + RemoteService service = iter.next(); + if (service.address.equals(address)) { + if (DBG) log("Cleaning up failed UUID channel lookup: " + service.address + + " " + service.uuid); + IBluetoothCallback callback = mUuidCallbackTracker.get(service); + if (callback != null) { + try { + callback.onRfcommChannelFound(-1); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + iter.remove(); + } + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + switch(mBluetoothState) { + case BluetoothAdapter.STATE_OFF: + pw.println("Bluetooth OFF\n"); + return; + case BluetoothAdapter.STATE_TURNING_ON: + pw.println("Bluetooth TURNING ON\n"); + return; + case BluetoothAdapter.STATE_TURNING_OFF: + pw.println("Bluetooth TURNING OFF\n"); + return; + case BluetoothAdapter.STATE_ON: + pw.println("Bluetooth ON\n"); + } + + pw.println("mIsAirplaneSensitive = " + mIsAirplaneSensitive); + + pw.println("Local address = " + getAddress()); + pw.println("Local name = " + getName()); + pw.println("isDiscovering() = " + isDiscovering()); + + BluetoothHeadset headset = new BluetoothHeadset(mContext, null); + + pw.println("\n--Known devices--"); + for (String address : mDeviceProperties.keySet()) { + int bondState = mBondState.getBondState(address); + pw.printf("%s %10s (%d) %s\n", address, + toBondStateString(bondState), + mBondState.getAttempt(address), + getRemoteName(address)); + + Map uuidChannels = mDeviceServiceChannelCache.get(address); + if (uuidChannels == null) { + pw.println("\tuuids = null"); + } else { + for (ParcelUuid uuid : uuidChannels.keySet()) { + Integer channel = uuidChannels.get(uuid); + if (channel == null) { + pw.println("\t" + uuid); + } else { + pw.println("\t" + uuid + " RFCOMM channel = " + channel); + } + } + } + for (RemoteService service : mUuidCallbackTracker.keySet()) { + if (service.address.equals(address)) { + pw.println("\tPENDING CALLBACK: " + service.uuid); + } + } + } + + String value = getProperty("Devices"); + String[] devicesObjectPath = null; + if (value != null) { + devicesObjectPath = value.split(","); + } + pw.println("\n--ACL connected devices--"); + if (devicesObjectPath != null) { + for (String device : devicesObjectPath) { + pw.println(getAddressFromObjectPath(device)); + } + } + + // Rather not do this from here, but no-where else and I need this + // dump + pw.println("\n--Headset Service--"); + switch (headset.getState()) { + case BluetoothHeadset.STATE_DISCONNECTED: + pw.println("getState() = STATE_DISCONNECTED"); + break; + case BluetoothHeadset.STATE_CONNECTING: + pw.println("getState() = STATE_CONNECTING"); + break; + case BluetoothHeadset.STATE_CONNECTED: + pw.println("getState() = STATE_CONNECTED"); + break; + case BluetoothHeadset.STATE_ERROR: + pw.println("getState() = STATE_ERROR"); + break; + } + + pw.println("\ngetCurrentHeadset() = " + headset.getCurrentHeadset()); + pw.println("getBatteryUsageHint() = " + headset.getBatteryUsageHint()); + headset.close(); + pw.println("\n--Application Service Records--"); + for (Integer handle : mServiceRecordToPid.keySet()) { + Integer pid = mServiceRecordToPid.get(handle); + pw.println("\tpid " + pid + " handle " + Integer.toHexString(handle)); + } + } + + /* package */ static int bluezStringToScanMode(boolean pairable, boolean discoverable) { + if (pairable && discoverable) + return BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; + else if (pairable && !discoverable) + return BluetoothAdapter.SCAN_MODE_CONNECTABLE; + else + return BluetoothAdapter.SCAN_MODE_NONE; + } + + /* package */ static String scanModeToBluezString(int mode) { + switch (mode) { + case BluetoothAdapter.SCAN_MODE_NONE: + return "off"; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + return "connectable"; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + return "discoverable"; + } + return null; + } + + /*package*/ String getAddressFromObjectPath(String objectPath) { + String adapterObjectPath = getProperty("ObjectPath"); + if (adapterObjectPath == null || objectPath == null) { + Log.e(TAG, "getAddressFromObjectPath: AdpaterObjectPath:" + adapterObjectPath + + " or deviceObjectPath:" + objectPath + " is null"); + return null; + } + if (!objectPath.startsWith(adapterObjectPath)) { + Log.e(TAG, "getAddressFromObjectPath: AdpaterObjectPath:" + adapterObjectPath + + " is not a prefix of deviceObjectPath:" + objectPath + + "bluetoothd crashed ?"); + return null; + } + String address = objectPath.substring(adapterObjectPath.length()); + if (address != null) return address.replace('_', ':'); + + Log.e(TAG, "getAddressFromObjectPath: Address being returned is null"); + return null; + } + + /*package*/ String getObjectPathFromAddress(String address) { + String path = getProperty("ObjectPath"); + if (path == null) { + Log.e(TAG, "Error: Object Path is null"); + return null; + } + path = path + address.replace(":", "_"); + return path; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + private native static void classInitNative(); + private native void initializeNativeDataNative(); + private native boolean setupNativeDataNative(); + private native boolean tearDownNativeDataNative(); + private native void cleanupNativeDataNative(); + private native String getAdapterPathNative(); + + private native int isEnabledNative(); + private native int enableNative(); + private native int disableNative(); + + private native Object[] getAdapterPropertiesNative(); + private native Object[] getDevicePropertiesNative(String objectPath); + private native boolean setAdapterPropertyStringNative(String key, String value); + private native boolean setAdapterPropertyIntegerNative(String key, int value); + private native boolean setAdapterPropertyBooleanNative(String key, int value); + + private native boolean startDiscoveryNative(); + private native boolean stopDiscoveryNative(); + + private native boolean createPairedDeviceNative(String address, int timeout_ms); + private native boolean cancelDeviceCreationNative(String address); + private native boolean removeDeviceNative(String objectPath); + private native int getDeviceServiceChannelNative(String objectPath, String uuid, + int attributeId); + + private native boolean cancelPairingUserInputNative(String address, int nativeData); + private native boolean setPinNative(String address, String pin, int nativeData); + private native boolean setPasskeyNative(String address, int passkey, int nativeData); + private native boolean setPairingConfirmationNative(String address, boolean confirm, + int nativeData); + private native boolean setDevicePropertyBooleanNative(String objectPath, String key, + int value); + private native boolean createDeviceNative(String address); + /*package*/ native boolean discoverServicesNative(String objectPath, String pattern); + + private native int addRfcommServiceRecordNative(String name, long uuidMsb, long uuidLsb, + short channel); + private native boolean removeServiceRecordNative(int handle); +} diff --git a/core/java/android/server/search/SearchDialogWrapper.java b/core/java/android/server/search/SearchDialogWrapper.java index 49718cb0acd0f9b15c6b57b6b5b5cc689f0b1c3c..9ee64af367d5541c3338ea8c60f36f59d322e60f 100644 --- a/core/java/android/server/search/SearchDialogWrapper.java +++ b/core/java/android/server/search/SearchDialogWrapper.java @@ -58,6 +58,7 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { // data[KEY_INITIAL_QUERY]: initial query // data[KEY_LAUNCH_ACTIVITY]: launch activity // data[KEY_APP_SEARCH_DATA]: app search data + // data[KEY_TRIGGER]: 0 = false, 1 = true private static final int MSG_START_SEARCH = 1; // Takes no arguments private static final int MSG_STOP_SEARCH = 2; @@ -69,7 +70,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { private static final String KEY_INITIAL_QUERY = "q"; private static final String KEY_LAUNCH_ACTIVITY = "a"; private static final String KEY_APP_SEARCH_DATA = "d"; - private static final String KEY_IDENT= "i"; + private static final String KEY_IDENT = "i"; + private static final String KEY_TRIGGER = "t"; // Context used for getting search UI resources private final Context mContext; @@ -173,7 +175,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { final Bundle appSearchData, final boolean globalSearch, final ISearchManagerCallback searchManagerCallback, - int ident) { + int ident, + boolean trigger) { if (DBG) debug("startSearch()"); Message msg = Message.obtain(); msg.what = MSG_START_SEARCH; @@ -185,6 +188,7 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { msgData.putParcelable(KEY_LAUNCH_ACTIVITY, launchActivity); msgData.putBundle(KEY_APP_SEARCH_DATA, appSearchData); msgData.putInt(KEY_IDENT, ident); + msgData.putInt(KEY_TRIGGER, trigger ? 1 : 0); mSearchUiThread.sendMessage(msg); // be a little more eager in setting this so isVisible will return the correct value if // called immediately after startSearch @@ -268,8 +272,9 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { boolean globalSearch = msg.arg2 != 0; ISearchManagerCallback searchManagerCallback = (ISearchManagerCallback) msg.obj; int ident = msgData.getInt(KEY_IDENT); + boolean trigger = msgData.getInt(KEY_TRIGGER) != 0; performStartSearch(initialQuery, selectInitialQuery, launchActivity, - appSearchData, globalSearch, searchManagerCallback, ident); + appSearchData, globalSearch, searchManagerCallback, ident, trigger); } } @@ -284,7 +289,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { Bundle appSearchData, boolean globalSearch, ISearchManagerCallback searchManagerCallback, - int ident) { + int ident, + boolean trigger) { if (DBG) debug("performStartSearch()"); registerBroadcastReceiver(); @@ -301,6 +307,9 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, globalSearch); mVisible = true; + if (trigger) { + mSearchDialog.launchQuerySearch(); + } } /** diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java index afed4a4968ce280b456643913e45c0082fe99579..78ea2e3fcfc3406b4301d4f9bffad1cc1c3b553c 100644 --- a/core/java/android/server/search/SearchManagerService.java +++ b/core/java/android/server/search/SearchManagerService.java @@ -233,7 +233,30 @@ public class SearchManagerService extends ISearchManager.Stub { appSearchData, globalSearch, searchManagerCallback, - ident); + ident, + false); // don't trigger + } + + /** + * Launches the search UI and triggers the search, as if the user had clicked on the + * search button within the dialog. + * + * @see SearchManager#triggerSearch(String, android.content.ComponentName, android.os.Bundle) + */ + public void triggerSearch(String query, + ComponentName launchActivity, + Bundle appSearchData, + ISearchManagerCallback searchManagerCallback, + int ident) { + getSearchDialog().startSearch( + query, + false, + launchActivity, + appSearchData, + false, + searchManagerCallback, + ident, + true); // triger search after launching } /** diff --git a/core/java/android/service/wallpaper/IWallpaperConnection.aidl b/core/java/android/service/wallpaper/IWallpaperConnection.aidl new file mode 100644 index 0000000000000000000000000000000000000000..b09ccabe26b9ce600a870e533b4863facc153cdf --- /dev/null +++ b/core/java/android/service/wallpaper/IWallpaperConnection.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.wallpaper; + +import android.os.ParcelFileDescriptor; +import android.service.wallpaper.IWallpaperEngine; + +/** + * @hide + */ +interface IWallpaperConnection { + void attachEngine(IWallpaperEngine engine); + ParcelFileDescriptor setWallpaper(String name); +} diff --git a/core/java/android/service/wallpaper/IWallpaperEngine.aidl b/core/java/android/service/wallpaper/IWallpaperEngine.aidl new file mode 100644 index 0000000000000000000000000000000000000000..37e6133bd134e1a932316a111797d7ee6065b987 --- /dev/null +++ b/core/java/android/service/wallpaper/IWallpaperEngine.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.wallpaper; + +import android.view.MotionEvent; + +/** + * @hide + */ +oneway interface IWallpaperEngine { + void setDesiredSize(int width, int height); + void setVisibility(boolean visible); + void dispatchPointer(in MotionEvent event); + void destroy(); +} diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl new file mode 100644 index 0000000000000000000000000000000000000000..bc7a1d7aff9ce4392f8796627893cc05f959b3b6 --- /dev/null +++ b/core/java/android/service/wallpaper/IWallpaperService.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.wallpaper; + +import android.service.wallpaper.IWallpaperConnection; + +/** + * @hide + */ +oneway interface IWallpaperService { + void attach(IWallpaperConnection connection, + IBinder windowToken, int windowType, boolean isPreview, + int reqWidth, int reqHeight); +} diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java new file mode 100644 index 0000000000000000000000000000000000000000..b29d8374d7a56dfa4d54c281c21512db40cdd050 --- /dev/null +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -0,0 +1,865 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.wallpaper; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.BaseIWindow; +import com.android.internal.view.BaseSurfaceHolder; + +import android.app.Service; +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.util.LogPrinter; +import android.view.Gravity; +import android.view.IWindowSession; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRoot; +import android.view.WindowManager; +import android.view.WindowManagerImpl; + +/** + * A wallpaper service is responsible for showing a live wallpaper behind + * applications that would like to sit on top of it. + */ +public abstract class WallpaperService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + public static final String SERVICE_INTERFACE = + "android.service.wallpaper.WallpaperService"; + + /** + * Name under which a WallpaperService component publishes information + * about itself. This meta-data must reference an XML resource containing + * a <{@link android.R.styleable#Wallpaper wallpaper}> + * tag. + */ + public static final String SERVICE_META_DATA = "android.service.wallpaper"; + + static final String TAG = "WallpaperService"; + static final boolean DEBUG = false; + + private static final int DO_ATTACH = 10; + private static final int DO_DETACH = 20; + private static final int DO_SET_DESIRED_SIZE = 30; + + private static final int MSG_UPDATE_SURFACE = 10000; + private static final int MSG_VISIBILITY_CHANGED = 10010; + private static final int MSG_WALLPAPER_OFFSETS = 10020; + private static final int MSG_WALLPAPER_COMMAND = 10025; + private static final int MSG_WINDOW_RESIZED = 10030; + private static final int MSG_TOUCH_EVENT = 10040; + + private Looper mCallbackLooper; + + static final class WallpaperCommand { + String action; + int x; + int y; + int z; + Bundle extras; + boolean sync; + } + + /** + * The actual implementation of a wallpaper. A wallpaper service may + * have multiple instances running (for example as a real wallpaper + * and as a preview), each of which is represented by its own Engine + * instance. You must implement {@link WallpaperService#onCreateEngine()} + * to return your concrete Engine implementation. + */ + public class Engine { + IWallpaperEngineWrapper mIWallpaperEngine; + + // Copies from mIWallpaperEngine. + HandlerCaller mCaller; + IWallpaperConnection mConnection; + IBinder mWindowToken; + + boolean mInitializing = true; + boolean mVisible; + boolean mScreenOn = true; + boolean mReportedVisible; + boolean mDestroyed; + + // Current window state. + boolean mCreated; + boolean mIsCreating; + boolean mDrawingAllowed; + int mWidth; + int mHeight; + int mFormat; + int mType; + int mCurWidth; + int mCurHeight; + int mWindowFlags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + int mCurWindowFlags = mWindowFlags; + boolean mDestroyReportNeeded; + final Rect mVisibleInsets = new Rect(); + final Rect mWinFrame = new Rect(); + final Rect mContentInsets = new Rect(); + + final WindowManager.LayoutParams mLayout + = new WindowManager.LayoutParams(); + IWindowSession mSession; + + final Object mLock = new Object(); + boolean mOffsetMessageEnqueued; + float mPendingXOffset; + float mPendingYOffset; + float mPendingXOffsetStep; + float mPendingYOffsetStep; + boolean mPendingSync; + MotionEvent mPendingMove; + + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) { + mScreenOn = true; + reportVisibility(); + } else if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + mScreenOn = false; + reportVisibility(); + } + } + }; + + final BaseSurfaceHolder mSurfaceHolder = new BaseSurfaceHolder() { + + @Override + public boolean onAllowLockCanvas() { + return mDrawingAllowed; + } + + @Override + public void onRelayoutContainer() { + Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE); + mCaller.sendMessage(msg); + } + + @Override + public void onUpdateSurface() { + Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE); + mCaller.sendMessage(msg); + } + + public boolean isCreating() { + return mIsCreating; + } + + @Override + public void setFixedSize(int width, int height) { + throw new UnsupportedOperationException( + "Wallpapers currently only support sizing from layout"); + } + + public void setKeepScreenOn(boolean screenOn) { + throw new UnsupportedOperationException( + "Wallpapers do not support keep screen on"); + } + + }; + + final BaseIWindow mWindow = new BaseIWindow() { + @Override + public boolean onDispatchPointer(MotionEvent event, long eventTime, + boolean callWhenDone) { + synchronized (mLock) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + if (mPendingMove != null) { + mCaller.removeMessages(MSG_TOUCH_EVENT, mPendingMove); + mPendingMove.recycle(); + } + mPendingMove = event; + } else { + mPendingMove = null; + } + Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, + event); + mCaller.sendMessage(msg); + } + return false; + } + + @Override + public void resized(int w, int h, Rect coveredInsets, + Rect visibleInsets, boolean reportDraw) { + Message msg = mCaller.obtainMessageI(MSG_WINDOW_RESIZED, + reportDraw ? 1 : 0); + mCaller.sendMessage(msg); + } + + @Override + public void dispatchAppVisibility(boolean visible) { + // We don't do this in preview mode; we'll let the preview + // activity tell us when to run. + if (!mIWallpaperEngine.mIsPreview) { + Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED, + visible ? 1 : 0); + mCaller.sendMessage(msg); + } + } + + @Override + public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, + boolean sync) { + synchronized (mLock) { + if (DEBUG) Log.v(TAG, "Dispatch wallpaper offsets: " + x + ", " + y); + mPendingXOffset = x; + mPendingYOffset = y; + mPendingXOffsetStep = xStep; + mPendingYOffsetStep = yStep; + if (sync) { + mPendingSync = true; + } + if (!mOffsetMessageEnqueued) { + mOffsetMessageEnqueued = true; + Message msg = mCaller.obtainMessage(MSG_WALLPAPER_OFFSETS); + mCaller.sendMessage(msg); + } + } + } + + public void dispatchWallpaperCommand(String action, int x, int y, + int z, Bundle extras, boolean sync) { + synchronized (mLock) { + if (DEBUG) Log.v(TAG, "Dispatch wallpaper command: " + x + ", " + y); + WallpaperCommand cmd = new WallpaperCommand(); + cmd.action = action; + cmd.x = x; + cmd.y = y; + cmd.z = z; + cmd.extras = extras; + cmd.sync = sync; + Message msg = mCaller.obtainMessage(MSG_WALLPAPER_COMMAND); + msg.obj = cmd; + mCaller.sendMessage(msg); + } + } + }; + + /** + * Provides access to the surface in which this wallpaper is drawn. + */ + public SurfaceHolder getSurfaceHolder() { + return mSurfaceHolder; + } + + /** + * Convenience for {@link WallpaperManager#getDesiredMinimumWidth() + * WallpaperManager.getDesiredMinimumWidth()}, returning the width + * that the system would like this wallpaper to run in. + */ + public int getDesiredMinimumWidth() { + return mIWallpaperEngine.mReqWidth; + } + + /** + * Convenience for {@link WallpaperManager#getDesiredMinimumHeight() + * WallpaperManager.getDesiredMinimumHeight()}, returning the height + * that the system would like this wallpaper to run in. + */ + public int getDesiredMinimumHeight() { + return mIWallpaperEngine.mReqHeight; + } + + /** + * Return whether the wallpaper is currently visible to the user, + * this is the last value supplied to + * {@link #onVisibilityChanged(boolean)}. + */ + public boolean isVisible() { + return mReportedVisible; + } + + /** + * Returns true if this engine is running in preview mode -- that is, + * it is being shown to the user before they select it as the actual + * wallpaper. + */ + public boolean isPreview() { + return mIWallpaperEngine.mIsPreview; + } + + /** + * Control whether this wallpaper will receive raw touch events + * from the window manager as the user interacts with the window + * that is currently displaying the wallpaper. By default they + * are turned off. If enabled, the events will be received in + * {@link #onTouchEvent(MotionEvent)}. + */ + public void setTouchEventsEnabled(boolean enabled) { + mWindowFlags = enabled + ? (mWindowFlags&~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + : (mWindowFlags|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + if (mCreated) { + updateSurface(false, false); + } + } + + /** + * Called once to initialize the engine. After returning, the + * engine's surface will be created by the framework. + */ + public void onCreate(SurfaceHolder surfaceHolder) { + } + + /** + * Called right before the engine is going away. After this the + * surface will be destroyed and this Engine object is no longer + * valid. + */ + public void onDestroy() { + } + + /** + * Called to inform you of the wallpaper becoming visible or + * hidden. It is very important that a wallpaper only use + * CPU while it is visible.. + */ + public void onVisibilityChanged(boolean visible) { + } + + /** + * Called as the user performs touch-screen interaction with the + * window that is currently showing this wallpaper. Note that the + * events you receive here are driven by the actual application the + * user is interacting with, so if it is slow you will get fewer + * move events. + */ + public void onTouchEvent(MotionEvent event) { + } + + /** + * Called to inform you of the wallpaper's offsets changing + * within its contain, corresponding to the container's + * call to {@link WallpaperManager#setWallpaperOffsets(IBinder, float, float) + * WallpaperManager.setWallpaperOffsets()}. + */ + public void onOffsetsChanged(float xOffset, float yOffset, + float xOffsetStep, float yOffsetStep, + int xPixelOffset, int yPixelOffset) { + } + + /** + * Process a command that was sent to the wallpaper with + * {@link WallpaperManager#sendWallpaperCommand}. + * The default implementation does nothing, and always returns null + * as the result. + * + * @param action The name of the command to perform. This tells you + * what to do and how to interpret the rest of the arguments. + * @param x Generic integer parameter. + * @param y Generic integer parameter. + * @param z Generic integer parameter. + * @param extras Any additional parameters. + * @param resultRequested If true, the caller is requesting that + * a result, appropriate for the command, be returned back. + * @return If returning a result, create a Bundle and place the + * result data in to it. Otherwise return null. + */ + public Bundle onCommand(String action, int x, int y, int z, + Bundle extras, boolean resultRequested) { + return null; + } + + /** + * Called when an application has changed the desired virtual size of + * the wallpaper. + */ + public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) { + } + + /** + * Convenience for {@link SurfaceHolder.Callback#surfaceChanged + * SurfaceHolder.Callback.surfaceChanged()}. + */ + public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + /** + * Convenience for {@link SurfaceHolder.Callback#surfaceCreated + * SurfaceHolder.Callback.surfaceCreated()}. + */ + public void onSurfaceCreated(SurfaceHolder holder) { + } + + /** + * Convenience for {@link SurfaceHolder.Callback#surfaceDestroyed + * SurfaceHolder.Callback.surfaceDestroyed()}. + */ + public void onSurfaceDestroyed(SurfaceHolder holder) { + } + + void updateSurface(boolean forceRelayout, boolean forceReport) { + if (mDestroyed) { + Log.w(TAG, "Ignoring updateSurface: destroyed"); + } + + int myWidth = mSurfaceHolder.getRequestedWidth(); + if (myWidth <= 0) myWidth = ViewGroup.LayoutParams.FILL_PARENT; + int myHeight = mSurfaceHolder.getRequestedHeight(); + if (myHeight <= 0) myHeight = ViewGroup.LayoutParams.FILL_PARENT; + + final boolean creating = !mCreated; + final boolean formatChanged = mFormat != mSurfaceHolder.getRequestedFormat(); + boolean sizeChanged = mWidth != myWidth || mHeight != myHeight; + final boolean typeChanged = mType != mSurfaceHolder.getRequestedType(); + final boolean flagsChanged = mCurWindowFlags != mWindowFlags; + if (forceRelayout || creating || formatChanged || sizeChanged + || typeChanged || flagsChanged) { + + if (DEBUG) Log.v(TAG, "Changes: creating=" + creating + + " format=" + formatChanged + " size=" + sizeChanged); + + try { + mWidth = myWidth; + mHeight = myHeight; + mFormat = mSurfaceHolder.getRequestedFormat(); + mType = mSurfaceHolder.getRequestedType(); + + mLayout.x = 0; + mLayout.y = 0; + mLayout.width = myWidth; + mLayout.height = myHeight; + + mLayout.format = mFormat; + + mCurWindowFlags = mWindowFlags; + mLayout.flags = mWindowFlags + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + ; + + mLayout.memoryType = mType; + mLayout.token = mWindowToken; + + if (!mCreated) { + mLayout.type = mIWallpaperEngine.mWindowType; + mLayout.gravity = Gravity.LEFT|Gravity.TOP; + mLayout.setTitle(WallpaperService.this.getClass().getName()); + mLayout.windowAnimations = + com.android.internal.R.style.Animation_Wallpaper; + mSession.add(mWindow, mLayout, View.VISIBLE, mContentInsets); + } + + mSurfaceHolder.mSurfaceLock.lock(); + mDrawingAllowed = true; + + final int relayoutResult = mSession.relayout( + mWindow, mLayout, mWidth, mHeight, + View.VISIBLE, false, mWinFrame, mContentInsets, + mVisibleInsets, mSurfaceHolder.mSurface); + + if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface + + ", frame=" + mWinFrame); + + int w = mWinFrame.width(); + if (mCurWidth != w) { + sizeChanged = true; + mCurWidth = w; + } + int h = mWinFrame.height(); + if (mCurHeight != h) { + sizeChanged = true; + mCurHeight = h; + } + + mSurfaceHolder.mSurfaceLock.unlock(); + + try { + mDestroyReportNeeded = true; + + SurfaceHolder.Callback callbacks[] = null; + synchronized (mSurfaceHolder.mCallbacks) { + final int N = mSurfaceHolder.mCallbacks.size(); + if (N > 0) { + callbacks = new SurfaceHolder.Callback[N]; + mSurfaceHolder.mCallbacks.toArray(callbacks); + } + } + + if (!mCreated) { + mIsCreating = true; + if (DEBUG) Log.v(TAG, "onSurfaceCreated(" + + mSurfaceHolder + "): " + this); + onSurfaceCreated(mSurfaceHolder); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceCreated(mSurfaceHolder); + } + } + } + if (forceReport || creating || formatChanged || sizeChanged) { + if (DEBUG) { + RuntimeException e = new RuntimeException(); + e.fillInStackTrace(); + Log.w(TAG, "forceReport=" + forceReport + " creating=" + creating + + " formatChanged=" + formatChanged + + " sizeChanged=" + sizeChanged, e); + } + if (DEBUG) Log.v(TAG, "onSurfaceChanged(" + + mSurfaceHolder + ", " + mFormat + + ", " + mCurWidth + ", " + mCurHeight + + "): " + this); + onSurfaceChanged(mSurfaceHolder, mFormat, + mCurWidth, mCurHeight); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceChanged(mSurfaceHolder, mFormat, + mCurWidth, mCurHeight); + } + } + } + } finally { + mIsCreating = false; + mCreated = true; + if (creating || (relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) { + mSession.finishDrawing(mWindow); + } + } + } catch (RemoteException ex) { + } + if (DEBUG) Log.v( + TAG, "Layout: x=" + mLayout.x + " y=" + mLayout.y + + " w=" + mLayout.width + " h=" + mLayout.height); + } + } + + void attach(IWallpaperEngineWrapper wrapper) { + if (DEBUG) Log.v(TAG, "attach: " + this + " wrapper=" + wrapper); + if (mDestroyed) { + return; + } + + mIWallpaperEngine = wrapper; + mCaller = wrapper.mCaller; + mConnection = wrapper.mConnection; + mWindowToken = wrapper.mWindowToken; + mSurfaceHolder.setSizeFromLayout(); + mInitializing = true; + mSession = ViewRoot.getWindowSession(getMainLooper()); + mWindow.setSession(mSession); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(mReceiver, filter); + + if (DEBUG) Log.v(TAG, "onCreate(): " + this); + onCreate(mSurfaceHolder); + + mInitializing = false; + updateSurface(false, false); + } + + void doDesiredSizeChanged(int desiredWidth, int desiredHeight) { + if (!mDestroyed) { + if (DEBUG) Log.v(TAG, "onDesiredSizeChanged(" + + desiredWidth + "," + desiredHeight + "): " + this); + onDesiredSizeChanged(desiredWidth, desiredHeight); + } + } + + void doVisibilityChanged(boolean visible) { + mVisible = visible; + reportVisibility(); + } + + void reportVisibility() { + if (!mDestroyed) { + boolean visible = mVisible && mScreenOn; + if (mReportedVisible != visible) { + mReportedVisible = visible; + if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + visible + + "): " + this); + onVisibilityChanged(visible); + } + } + } + + void doOffsetsChanged() { + if (mDestroyed) { + return; + } + + float xOffset; + float yOffset; + float xOffsetStep; + float yOffsetStep; + boolean sync; + synchronized (mLock) { + xOffset = mPendingXOffset; + yOffset = mPendingYOffset; + xOffsetStep = mPendingXOffsetStep; + yOffsetStep = mPendingYOffsetStep; + sync = mPendingSync; + mPendingSync = false; + mOffsetMessageEnqueued = false; + } + if (DEBUG) Log.v(TAG, "Offsets change in " + this + + ": " + xOffset + "," + yOffset); + final int availw = mIWallpaperEngine.mReqWidth-mCurWidth; + final int xPixels = availw > 0 ? -(int)(availw*xOffset+.5f) : 0; + final int availh = mIWallpaperEngine.mReqHeight-mCurHeight; + final int yPixels = availh > 0 ? -(int)(availh*yOffset+.5f) : 0; + onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixels, yPixels); + + if (sync) { + try { + if (DEBUG) Log.v(TAG, "Reporting offsets change complete"); + mSession.wallpaperOffsetsComplete(mWindow.asBinder()); + } catch (RemoteException e) { + } + } + } + + void doCommand(WallpaperCommand cmd) { + Bundle result; + if (!mDestroyed) { + result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z, + cmd.extras, cmd.sync); + } else { + result = null; + } + if (cmd.sync) { + try { + if (DEBUG) Log.v(TAG, "Reporting command complete"); + mSession.wallpaperCommandComplete(mWindow.asBinder(), result); + } catch (RemoteException e) { + } + } + } + + void detach() { + mDestroyed = true; + + if (mVisible) { + mVisible = false; + if (DEBUG) Log.v(TAG, "onVisibilityChanged(false): " + this); + onVisibilityChanged(false); + } + + if (mDestroyReportNeeded) { + mDestroyReportNeeded = false; + SurfaceHolder.Callback callbacks[]; + synchronized (mSurfaceHolder.mCallbacks) { + callbacks = new SurfaceHolder.Callback[ + mSurfaceHolder.mCallbacks.size()]; + mSurfaceHolder.mCallbacks.toArray(callbacks); + } + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceDestroyed(mSurfaceHolder); + } + if (DEBUG) Log.v(TAG, "onSurfaceDestroyed(" + + mSurfaceHolder + "): " + this); + onSurfaceDestroyed(mSurfaceHolder); + } + + if (DEBUG) Log.v(TAG, "onDestroy(): " + this); + onDestroy(); + + unregisterReceiver(mReceiver); + + if (mCreated) { + try { + mSession.remove(mWindow); + } catch (RemoteException e) { + } + mSurfaceHolder.mSurface.release(); + mCreated = false; + } + } + } + + class IWallpaperEngineWrapper extends IWallpaperEngine.Stub + implements HandlerCaller.Callback { + private final HandlerCaller mCaller; + + final IWallpaperConnection mConnection; + final IBinder mWindowToken; + final int mWindowType; + final boolean mIsPreview; + int mReqWidth; + int mReqHeight; + + Engine mEngine; + + IWallpaperEngineWrapper(WallpaperService context, + IWallpaperConnection conn, IBinder windowToken, + int windowType, boolean isPreview, int reqWidth, int reqHeight) { + if (DEBUG && mCallbackLooper != null) { + mCallbackLooper.setMessageLogging(new LogPrinter(Log.VERBOSE, TAG)); + } + mCaller = new HandlerCaller(context, + mCallbackLooper != null + ? mCallbackLooper : context.getMainLooper(), + this); + mConnection = conn; + mWindowToken = windowToken; + mWindowType = windowType; + mIsPreview = isPreview; + mReqWidth = reqWidth; + mReqHeight = reqHeight; + + Message msg = mCaller.obtainMessage(DO_ATTACH); + mCaller.sendMessage(msg); + } + + public void setDesiredSize(int width, int height) { + Message msg = mCaller.obtainMessageII(DO_SET_DESIRED_SIZE, width, height); + mCaller.sendMessage(msg); + } + + public void setVisibility(boolean visible) { + Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED, + visible ? 1 : 0); + mCaller.sendMessage(msg); + } + + public void dispatchPointer(MotionEvent event) { + if (mEngine != null) { + mEngine.mWindow.onDispatchPointer(event, event.getEventTime(), false); + } + } + + public void destroy() { + Message msg = mCaller.obtainMessage(DO_DETACH); + mCaller.sendMessage(msg); + } + + public void executeMessage(Message message) { + switch (message.what) { + case DO_ATTACH: { + try { + mConnection.attachEngine(this); + } catch (RemoteException e) { + Log.w(TAG, "Wallpaper host disappeared", e); + return; + } + Engine engine = onCreateEngine(); + mEngine = engine; + engine.attach(this); + return; + } + case DO_DETACH: { + mEngine.detach(); + return; + } + case DO_SET_DESIRED_SIZE: { + mEngine.doDesiredSizeChanged(message.arg1, message.arg2); + return; + } + case MSG_UPDATE_SURFACE: + mEngine.updateSurface(true, false); + break; + case MSG_VISIBILITY_CHANGED: + if (DEBUG) Log.v(TAG, "Visibility change in " + mEngine + + ": " + message.arg1); + mEngine.doVisibilityChanged(message.arg1 != 0); + break; + case MSG_WALLPAPER_OFFSETS: { + mEngine.doOffsetsChanged(); + } break; + case MSG_WALLPAPER_COMMAND: { + WallpaperCommand cmd = (WallpaperCommand)message.obj; + mEngine.doCommand(cmd); + } break; + case MSG_WINDOW_RESIZED: { + final boolean reportDraw = message.arg1 != 0; + mEngine.updateSurface(true, false); + if (reportDraw) { + try { + mEngine.mSession.finishDrawing(mEngine.mWindow); + } catch (RemoteException e) { + } + } + } break; + case MSG_TOUCH_EVENT: { + MotionEvent ev = (MotionEvent)message.obj; + synchronized (mEngine.mLock) { + if (mEngine.mPendingMove == ev) { + mEngine.mPendingMove = null; + } + } + if (DEBUG) Log.v(TAG, "Delivering touch event: " + ev); + mEngine.onTouchEvent(ev); + ev.recycle(); + } break; + default : + Log.w(TAG, "Unknown message type " + message.what); + } + } + } + + /** + * Implements the internal {@link IWallpaperService} interface to convert + * incoming calls to it back to calls on an {@link WallpaperService}. + */ + class IWallpaperServiceWrapper extends IWallpaperService.Stub { + private final WallpaperService mTarget; + + public IWallpaperServiceWrapper(WallpaperService context) { + mTarget = context; + } + + public void attach(IWallpaperConnection conn, IBinder windowToken, + int windowType, boolean isPreview, int reqWidth, int reqHeight) { + new IWallpaperEngineWrapper(mTarget, conn, windowToken, + windowType, isPreview, reqWidth, reqHeight); + } + } + + /** + * Implement to return the implementation of the internal accessibility + * service interface. Subclasses should not override. + */ + @Override + public final IBinder onBind(Intent intent) { + return new IWallpaperServiceWrapper(this); + } + + /** + * This allows subclasses to change the thread that most callbacks + * occur on. Currently hidden because it is mostly needed for the + * image wallpaper (which runs in the system process and doesn't want + * to get stuck running on that seriously in use main thread). Not + * exposed right now because the semantics of this are not totally + * well defined and some callbacks can still happen on the main thread). + * @hide + */ + public void setCallbackLooper(Looper looper) { + mCallbackLooper = looper; + } + + public abstract Engine onCreateEngine(); +} diff --git a/core/java/android/service/wallpaper/WallpaperSettingsActivity.java b/core/java/android/service/wallpaper/WallpaperSettingsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..501947dac46094b09bedb40985124b4ea77b8031 --- /dev/null +++ b/core/java/android/service/wallpaper/WallpaperSettingsActivity.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.service.wallpaper; + +import android.content.res.Resources; +import android.os.Bundle; +import android.preference.PreferenceActivity; + +/** + * Base class for activities that will be used to configure the settings of + * a wallpaper. You should derive from this class to allow it to select the + * proper theme of the activity depending on how it is being used. + */ +public class WallpaperSettingsActivity extends PreferenceActivity { + /** + * This boolean extra in the launch intent indicates that the settings + * are being used while the wallpaper is in preview mode. + */ + final public static String EXTRA_PREVIEW_MODE + = "android.service.wallpaper.PREVIEW_MODE"; + + @Override + protected void onCreate(Bundle icicle) { + if (false) { + Resources.Theme theme = getTheme(); + if (getIntent().getBooleanExtra(EXTRA_PREVIEW_MODE, false)) { + theme.applyStyle(com.android.internal.R.style.PreviewWallpaperSettings, true); + } else { + theme.applyStyle(com.android.internal.R.style.ActiveWallpaperSettings, true); + } + } + super.onCreate(icicle); + } +} diff --git a/core/java/android/speech/RecognitionResult.java b/core/java/android/speech/RecognitionResult.java index 978106b1ea74c66248006ca125b1ac10e72c110d..95715eed410746d63b7827734a423a0ff5963ff1 100644 --- a/core/java/android/speech/RecognitionResult.java +++ b/core/java/android/speech/RecognitionResult.java @@ -1,50 +1,63 @@ /* - * Copyright (C) 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * 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. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + package android.speech; import android.os.Parcel; import android.os.Parcelable; /** - * RecognitionResult is a passive object that stores a single recognized - * query and its search result. - * TODO: revisit and improve. May be we should have a separate result - * object for each type, and put them (type/value) in bundle? + * RecognitionResult is a passive object that stores a single recognized query + * and its search result. * + * TODO: Revisit and improve this class, reconciling the different types of actions and + * the different ways they are represented. Maybe we should have a separate result object + * for each type, and put them (type/value) in bundle? * {@hide} */ public class RecognitionResult implements Parcelable { /** * Status of the recognize request. */ - public static final int NETWORK_TIMEOUT = 1; // Network operation timed out. - public static final int NETWORK_ERROR = 2; // Other networkrelated errors. - public static final int AUDIO_ERROR = 3; // Audio recording error. - public static final int SERVER_ERROR = 4; // Server sends error status. - public static final int CLIENT_ERROR = 5; // Other client side errors. - public static final int SPEECH_TIMEOUT = 6; // No speech input - public static final int NO_MATCH = 7; // No recognition result matched. - public static final int SERVICE_BUSY = 8; // RecognitionService busy. + public static final int NETWORK_TIMEOUT = 1; // Network operation timed out. + + public static final int NETWORK_ERROR = 2; // Other network related errors. + + public static final int AUDIO_ERROR = 3; // Audio recording error. + + public static final int SERVER_ERROR = 4; // Server sends error status. + + public static final int CLIENT_ERROR = 5; // Other client side errors. + + public static final int SPEECH_TIMEOUT = 6; // No speech input + + public static final int NO_MATCH = 7; // No recognition result matched. + + public static final int SERVICE_BUSY = 8; // RecognitionService busy. /** - * Type of the recognition results. + * Type of the recognition results. */ - public static final int RAW_RECOGNITION_RESULT = 0; - public static final int WEB_SEARCH_RESULT = 1; + public static final int RAW_RECOGNITION_RESULT = 0; + + public static final int WEB_SEARCH_RESULT = 1; + public static final int CONTACT_RESULT = 2; + + public static final int ACTION_RESULT = 3; /** * A factory method to create a raw RecognitionResult @@ -56,11 +69,12 @@ public class RecognitionResult implements Parcelable { } /** - * A factory method to create RecognitionResult for contacts. + * A factory method to create a RecognitionResult for contacts. * * @param contact the contact name. * @param phoneType the phone type. - * @param callAction whether this result included a command to "call", or just the contact name. + * @param callAction whether this result included a command to "call", or + * just the contact name. */ public static RecognitionResult newContactResult(String contact, int phoneType, boolean callAction) { @@ -68,49 +82,59 @@ public class RecognitionResult implements Parcelable { } /** - * A factory method to create a RecognitionResult for Web Search Query. + * A factory method to create a RecognitionResult for a web search query. * - * @param query the query string. + * @param query the query string. * @param html the html page of the search result. - * @param url the url that performs the search with the query. + * @param url the url that performs the search with the query. */ public static RecognitionResult newWebResult(String query, String html, String url) { return new RecognitionResult(WEB_SEARCH_RESULT, query, html, url); } - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { + /** + * A factory method to create a RecognitionResult for an action. + * + * @param action the action type + * @param query the query string associated with that action. + */ + public static RecognitionResult newActionResult(int action, String query) { + return new RecognitionResult(ACTION_RESULT, action, query); + } - public RecognitionResult createFromParcel(Parcel in) { - return new RecognitionResult(in); - } + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + public RecognitionResult createFromParcel(Parcel in) { + return new RecognitionResult(in); + } - public RecognitionResult[] newArray(int size) { - return new RecognitionResult[size]; - } - }; + public RecognitionResult[] newArray(int size) { + return new RecognitionResult[size]; + } + }; /** * Result type. */ public final int mResultType; - /** - * The recognized string when mResultType is WEB_SEARCH_RESULT. - * The name of the contact when mResultType is CONTACT_RESULT. + /** + * The recognized string when mResultType is WEB_SEARCH_RESULT. The name of + * the contact when mResultType is CONTACT_RESULT. The relevant query when + * mResultType is ACTION_RESULT. */ public final String mText; /** - * The HTML result page for the query. If this is null, then the - * application must use the url field to get the HTML result page. + * The HTML result page for the query. If this is null, then the application + * must use the url field to get the HTML result page. */ public final String mHtml; /** - * The url to get the result page for the query string. The - * application must use this url instead of performing the search - * with the query. + * The url to get the result page for the query string. The application must + * use this url instead of performing the search with the query. */ public final String mUrl; @@ -120,17 +144,33 @@ public class RecognitionResult implements Parcelable { public final int mPhoneType; /** - * Whether a contact recognition result included a command to "call". This is valid only - * when mResultType == CONTACT_RESULT. + * Action type. This is valid only when mResultType == ACTION_RESULT. + */ + public final int mAction; + + /** + * Whether a contact recognition result included a command to "call". This + * is valid only when mResultType == CONTACT_RESULT. */ public final boolean mCallAction; + private RecognitionResult(int type, int action, String query) { + mResultType = type; + mAction = action; + mText = query; + mHtml = null; + mUrl = null; + mPhoneType = -1; + mCallAction = false; + } + private RecognitionResult(int type, String query, String html, String url) { mResultType = type; mText = query; mHtml = html; mUrl = url; mPhoneType = -1; + mAction = -1; mCallAction = false; } @@ -140,15 +180,17 @@ public class RecognitionResult implements Parcelable { mPhoneType = phoneType; mHtml = null; mUrl = null; + mAction = -1; mCallAction = callAction; } - + private RecognitionResult(Parcel in) { mResultType = in.readInt(); mText = in.readString(); - mHtml= in.readString(); - mUrl= in.readString(); + mHtml = in.readString(); + mUrl = in.readString(); mPhoneType = in.readInt(); + mAction = in.readInt(); mCallAction = (in.readInt() == 1); } @@ -158,15 +200,17 @@ public class RecognitionResult implements Parcelable { out.writeString(mHtml); out.writeString(mUrl); out.writeInt(mPhoneType); + out.writeInt(mAction); out.writeInt(mCallAction ? 1 : 0); } - - + @Override public String toString() { - String resultType[] = { "RAW", "WEB", "CONTACT" }; - return "[type=" + resultType[mResultType] + - ", text=" + mText+ ", mUrl=" + mUrl + ", html=" + mHtml + "]"; + String resultType[] = { + "RAW", "WEB", "CONTACT", "ACTION" + }; + return "[type=" + resultType[mResultType] + ", text=" + mText + ", mUrl=" + mUrl + + ", html=" + mHtml + ", mAction=" + mAction + ", mCallAction=" + mCallAction + "]"; } public int describeContents() { diff --git a/core/java/android/speech/RecognitionServiceUtil.java b/core/java/android/speech/RecognitionServiceUtil.java index a8c78684f6cdc99f2fa9ca5e8eb1d6ce6459225e..4207543256a3ef07fbc5c1d35c4fed786d85a2aa 100644 --- a/core/java/android/speech/RecognitionServiceUtil.java +++ b/core/java/android/speech/RecognitionServiceUtil.java @@ -1,17 +1,17 @@ /* - * Copyright (C) 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * 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. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package android.speech; diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index a6d76d684fe4e9e55d4be447c8ea7617643233d4..3f369ddd580a107fb2b2bf62a30bd055fd7f2896 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -35,9 +35,13 @@ import java.util.Locale; /** * * Synthesizes speech from text for immediate playback or to create a sound file. + *

    A TextToSpeech instance can only be used to synthesize text once it has completed its + * initialization. Implement the {@link TextToSpeech.OnInitListener} to be + * notified of the completion of the initialization.
    + * When you are done using the TextToSpeech instance, call the {@link #shutdown()} method + * to release the native resources used by the TextToSpeech engine. * */ -//TODO complete javadoc + add links to constants public class TextToSpeech { /** @@ -85,7 +89,7 @@ public class TextToSpeech { public static final int LANG_MISSING_DATA = -1; /** - * Denotes the language is not supported by the current TTS engine. + * Denotes the language is not supported. */ public static final int LANG_NOT_SUPPORTED = -2; @@ -100,29 +104,38 @@ public class TextToSpeech { /** - * Called when the TTS has initialized. - * - * The InitListener must implement the onInit function. onInit is passed a - * status code indicating the result of the TTS initialization. + * Interface definition of a callback to be invoked indicating the completion of the + * TextToSpeech engine initialization. */ public interface OnInitListener { + /** + * Called to signal the completion of the TextToSpeech engine initialization. + * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ public void onInit(int status); } /** - * Called when the TTS has completed saying something that has an utterance ID set. + * Interface definition of a callback to be invoked indicating the TextToSpeech engine has + * completed synthesizing an utterance with an utterance ID set. * - * The OnUtteranceCompletedListener must implement the onUtteranceCompleted function. - * onUtteranceCompleted is passed a String that is the utteranceId given in - * the original speak call. */ - public interface OnUtteranceCompletedListener { + public interface OnUtteranceCompletedListener { + /** + * Called to signal the completion of the synthesis of the utterance that was identified + * with the string parameter. This identifier is the one originally passed in the + * parameter hashmap of the synthesis request in + * {@link TextToSpeech#speak(String, int, HashMap)} or + * {@link TextToSpeech#synthesizeToFile(String, HashMap, String)} with the + * {@link TextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID} key. + * @param utteranceId the identifier of the utterance. + */ public void onUtteranceCompleted(String utteranceId); } /** - * Internal constants for the TTS functionality + * Internal constants for the TextToSpeech functionality * */ public class Engine { @@ -145,38 +158,41 @@ public class TextToSpeech { public static final String DEFAULT_SYNTH = "com.svox.pico"; // default values for rendering + /** + * Default audio stream used when playing synthesized speech. + */ public static final int DEFAULT_STREAM = AudioManager.STREAM_MUSIC; // return codes for a TTS engine's check data activity /** * Indicates success when checking the installation status of the resources used by the - * text-to-speech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_PASS = 1; /** * Indicates failure when checking the installation status of the resources used by the - * text-to-speech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_FAIL = 0; /** * Indicates erroneous data when checking the installation status of the resources used by - * the text-to-speech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_BAD_DATA = -1; /** * Indicates missing resources when checking the installation status of the resources used - * by the text-to-speech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_DATA = -2; /** * Indicates missing storage volume when checking the installation status of the resources - * used by the text-to-speech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. + * used by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_VOLUME = -3; // intents to ask engine to install data or check its data /** - * Activity Action: Triggers the platform Text-To-Speech engine to + * Activity Action: Triggers the platform TextToSpeech engine to * start the activity that installs the resource files on the device * that are required for TTS to be operational. Since the installation * of the data can be interrupted or declined by the user, the application @@ -197,7 +213,7 @@ public class TextToSpeech { public static final String ACTION_TTS_DATA_INSTALLED = "android.speech.tts.engine.TTS_DATA_INSTALLED"; /** - * Activity Action: Starts the activity from the platform Text-To-Speech + * Activity Action: Starts the activity from the platform TextToSpeech * engine to verify the proper installation and availability of the * resource files on the system. Upon completion, the activity will * return one of the following codes: @@ -210,9 +226,9 @@ public class TextToSpeech { * fields: *

      *
    • {@link #EXTRA_VOICE_DATA_ROOT_DIRECTORY} which - * indicates the path to the location of the resource files
    • , + * indicates the path to the location of the resource files, *
    • {@link #EXTRA_VOICE_DATA_FILES} which contains - * the list of all the resource files
    • , + * the list of all the resource files, *
    • and {@link #EXTRA_VOICE_DATA_FILES_INFO} which * contains, for each resource file, the description of the language covered by * the file in the xxx-YYY format, where xxx is the 3-letter ISO language code, @@ -226,18 +242,18 @@ public class TextToSpeech { // extras for a TTS engine's check data activity /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the text-to-speech engine specifies the path to its resources. + * the TextToSpeech engine specifies the path to its resources. */ public static final String EXTRA_VOICE_DATA_ROOT_DIRECTORY = "dataRoot"; /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the text-to-speech engine specifies the file names of its resources under the + * the TextToSpeech engine specifies the file names of its resources under the * resource path. */ public static final String EXTRA_VOICE_DATA_FILES = "dataFiles"; /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where - * the text-to-speech engine specifies the locale associated with each resource file. + * the TextToSpeech engine specifies the locale associated with each resource file. */ public static final String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo"; @@ -272,11 +288,17 @@ public class TextToSpeech { /** * Parameter key to specify the audio stream type to be used when speaking text * or playing back a file. + * @see TextToSpeech#speak(String, int, HashMap) + * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_STREAM = "streamType"; /** - * Parameter key to identify an utterance in the completion listener after text has been + * Parameter key to identify an utterance in the + * {@link TextToSpeech.OnUtteranceCompletedListener} after text has been * spoken, a file has been played back or a silence duration has elapsed. + * @see TextToSpeech#speak(String, int, HashMap) + * @see TextToSpeech#playEarcon(String, int, HashMap) + * @see TextToSpeech#synthesizeToFile(String, HashMap, String) */ public static final String KEY_PARAM_UTTERANCE_ID = "utteranceId"; @@ -330,13 +352,14 @@ public class TextToSpeech { private String[] mCachedParams; /** - * The constructor for the TTS. + * The constructor for the TextToSpeech class. + * This will also initialize the associated TextToSpeech engine if it isn't already running. * * @param context - * The context + * The context this instance is running in. * @param listener - * The InitListener that will be called when the TTS has - * initialized successfully. + * The {@link TextToSpeech.OnInitListener} that will be called when the + * TextToSpeech engine has initialized. */ public TextToSpeech(Context context, OnInitListener listener) { mContext = context; @@ -402,9 +425,9 @@ public class TextToSpeech { /** - * Shuts down the TTS. It is good practice to call this in the onDestroy - * method of the Activity that is using the TTS so that the TTS is stopped - * cleanly. + * Releases the resources used by the TextToSpeech engine. + * It is good practice for instance to call this method in the onDestroy() method of an Activity + * so the TextToSpeech engine can be cleanly stopped. */ public void shutdown() { try { @@ -418,11 +441,12 @@ public class TextToSpeech { /** * Adds a mapping between a string of text and a sound resource in a - * package. - * @see #speak(String, int, HashMap) + * package. After a call to this method, subsequent calls to + * {@link #speak(String, int, HashMap)} will play the specified sound resource + * if it is available, or synthesize the text it is missing. * * @param text - * Example: "south_south_east"
      + * The string of text. Example: "south_south_east" * * @param packagename * Pass the packagename of the application that contains the @@ -438,7 +462,7 @@ public class TextToSpeech { *

      * * @param resourceId - * Example: R.raw.south_south_east + * Example: R.raw.south_south_east * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -476,10 +500,13 @@ public class TextToSpeech { /** * Adds a mapping between a string of text and a sound file. Using this, it - * is possible to add custom pronounciations for text. + * is possible to add custom pronounciations for a string of text. + * After a call to this method, subsequent calls to {@link #speak(String, int, HashMap)} + * will play the specified sound resource if it is available, or synthesize the text it is + * missing. * * @param text - * The string of text + * The string of text. Example: "south_south_east" * @param filename * The full path to the sound file (for example: * "/sdcard/mysounds/hello.wav") @@ -520,28 +547,26 @@ public class TextToSpeech { /** * Adds a mapping between a string of text and a sound resource in a - * package. + * package. Use this to add custom earcons. * * @see #playEarcon(String, int, HashMap) * - * @param earcon The name of the earcon - * Example: "[tick]"
      + * @param earcon The name of the earcon. + * Example: "[tick]"
      * * @param packagename - * Pass the packagename of the application that contains the - * resource. If the resource is in your own application (this is - * the most common case), then put the packagename of your - * application here.
      + * the package name of the application that contains the + * resource. This can for instance be the package name of your own application. * Example: "com.google.marvin.compass"
      - * The packagename can be found in the AndroidManifest.xml of - * your application. + * The package name can be found in the AndroidManifest.xml of + * the application containing the resource. *

      * <manifest xmlns:android="..." * package="com.google.marvin.compass"> *

      * * @param resourceId - * Example: R.raw.tick_snd + * Example: R.raw.tick_snd * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -578,11 +603,14 @@ public class TextToSpeech { /** - * Adds a mapping between a string of text and a sound file. Using this, it - * is possible to add custom earcons. + * Adds a mapping between a string of text and a sound file. + * Use this to add custom earcons. + * + * @see #playEarcon(String, int, HashMap) * * @param earcon - * The name of the earcon + * The name of the earcon. + * Example: "[tick]" * @param filename * The full path to the sound file (for example: * "/sdcard/mysounds/tick.wav") @@ -623,18 +651,18 @@ public class TextToSpeech { /** * Speaks the string using the specified queuing strategy and speech - * parameters. Note that the speech parameters are not universally supported - * by all engines and will be treated as a hint. The TTS library will try to - * fulfill these parameters as much as possible, but there is no guarantee - * that the voice used will have the properties specified. + * parameters. * * @param text * The string of text to be spoken. * @param queueMode * The queuing strategy to use. - * See QUEUE_ADD and QUEUE_FLUSH. + * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. * @param params - * The hashmap of speech parameters to be used. + * The list of parameters to be used. Can be null if no parameters are given. + * They are specified using a (key, value) pair, where the key can be + * {@link Engine#KEY_PARAM_STREAM} or + * {@link Engine#KEY_PARAM_UTTERANCE_ID}. * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -690,9 +718,12 @@ public class TextToSpeech { * @param earcon * The earcon that should be played * @param queueMode - * See QUEUE_ADD and QUEUE_FLUSH. + * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. * @param params - * The hashmap of parameters to be used. + * The list of parameters to be used. Can be null if no parameters are given. + * They are specified using a (key, value) pair, where the key can be + * {@link Engine#KEY_PARAM_STREAM} or + * {@link Engine#KEY_PARAM_UTTERANCE_ID}. * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -747,7 +778,11 @@ public class TextToSpeech { * @param durationInMs * A long that indicates how long the silence should last. * @param queueMode - * See QUEUE_ADD and QUEUE_FLUSH. + * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params + * The list of parameters to be used. Can be null if no parameters are given. + * They are specified using a (key, value) pair, where the key can be + * {@link Engine#KEY_PARAM_UTTERANCE_ID}. * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -791,9 +826,9 @@ public class TextToSpeech { /** - * Returns whether or not the TTS is busy speaking. + * Returns whether or not the TextToSpeech engine is busy speaking. * - * @return Whether or not the TTS is busy speaking. + * @return Whether or not the TextToSpeech engine is busy speaking. */ public boolean isSpeaking() { synchronized (mStartLock) { @@ -827,7 +862,8 @@ public class TextToSpeech { /** - * Stops speech from the TTS. + * Interrupts the current utterance (whether played or rendered to file) and discards other + * utterances in the queue. * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ @@ -865,15 +901,12 @@ public class TextToSpeech { /** - * Sets the speech rate for the TTS engine. + * Sets the speech rate for the TextToSpeech engine. * - * Note that the speech rate is not universally supported by all engines and - * will be treated as a hint. The TTS library will try to use the specified - * speech rate, but there is no guarantee. * This has no effect on any pre-recorded speech. * * @param speechRate - * The speech rate for the TTS engine. 1 is the normal speed, + * The speech rate for the TextToSpeech engine. 1 is the normal speed, * lower values slow down the speech (0.5 is half the normal speech rate), * greater values accelerate it (2 is twice the normal speech rate). * @@ -917,15 +950,12 @@ public class TextToSpeech { /** - * Sets the speech pitch for the TTS engine. + * Sets the speech pitch for the TextToSpeech engine. * - * Note that the pitch is not universally supported by all engines and - * will be treated as a hint. The TTS library will try to use the specified - * pitch, but there is no guarantee. * This has no effect on any pre-recorded speech. * * @param pitch - * The pitch for the TTS engine. 1 is the normal pitch, + * The pitch for the TextToSpeech engine. 1 is the normal pitch, * lower values lower the tone of the synthesized voice, * greater values increase it. * @@ -967,11 +997,11 @@ public class TextToSpeech { /** - * Sets the language for the TTS engine. - * - * Note that the language is not universally supported by all engines and - * will be treated as a hint. The TTS library will try to use the specified - * language as represented by the Locale, but there is no guarantee. + * Sets the language for the TextToSpeech engine. + * The TextToSpeech engine will try to use the closest match to the specified + * language as represented by the Locale, but there is no guarantee that the exact same Locale + * will be used. Use {@link #isLanguageAvailable(Locale)} to check the level of support + * before choosing the language to use for the next utterances. * * @param loc * The locale describing the language to be used. @@ -1023,9 +1053,10 @@ public class TextToSpeech { /** - * Returns a Locale instance describing the language currently being used by the TTS engine. + * Returns a Locale instance describing the language currently being used by the TextToSpeech + * engine. * @return language, country (if any) and variant (if any) used by the engine stored in a Locale - * instance, or null is the TTS engine has failed. + * instance, or null is the TextToSpeech engine has failed. */ public Locale getLanguage() { synchronized (mStartLock) { @@ -1063,7 +1094,7 @@ public class TextToSpeech { } /** - * Checks if the specified language as represented by the Locale is available. + * Checks if the specified language as represented by the Locale is available and supported. * * @param loc * The Locale describing the language to be used. @@ -1112,7 +1143,9 @@ public class TextToSpeech { * @param text * The String of text that should be synthesized * @param params - * A hashmap of parameters. + * The list of parameters to be used. Can be null if no parameters are given. + * They are specified using a (key, value) pair, where the key can be + * {@link Engine#KEY_PARAM_UTTERANCE_ID}. * @param filename * The string that gives the full output filename; it should be * something like "/sdcard/myappsounds/mysound.wav". diff --git a/core/java/android/syncml/pim/VParser.java b/core/java/android/syncml/pim/VParser.java deleted file mode 100644 index 57c5f7a5cfd32c4aaa95fff21bd3a7c4d011feb4..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/VParser.java +++ /dev/null @@ -1,740 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; - -/** - * This interface is used to parse the V format files, such as VCard & VCal - * - */ -abstract public class VParser { - // Assume that "iso-8859-1" is able to map "all" 8bit characters to some unicode and - // decode the unicode to the original charset. If not, this setting will cause some bug. - public static String DEFAULT_CHARSET = "iso-8859-1"; - - /** - * The buffer used to store input stream - */ - protected String mBuffer = null; - - /** The builder to build parsed data */ - protected VBuilder mBuilder = null; - - /** The encoding type */ - protected String mEncoding = null; - - protected final int PARSE_ERROR = -1; - - protected final String mDefaultEncoding = "8BIT"; - - /** - * If offset reach '\r\n' return 2. Else return PARSE_ERROR. - */ - protected int parseCrlf(int offset) { - if (offset >= mBuffer.length()) - return PARSE_ERROR; - char ch = mBuffer.charAt(offset); - if (ch == '\r') { - offset++; - ch = mBuffer.charAt(offset); - if (ch == '\n') { - return 2; - } - } - return PARSE_ERROR; - } - - /** - * Parse the given stream - * - * @param is - * The source to parse. - * @param encoding - * The encoding type. - * @param builder - * The v builder which used to construct data. - * @return Return true for success, otherwise false. - * @throws IOException - */ - public boolean parse(InputStream is, String encoding, VBuilder builder) - throws IOException { - setInputStream(is, encoding); - mBuilder = builder; - int ret = 0, offset = 0, sum = 0; - - if (mBuilder != null) { - mBuilder.start(); - } - for (;;) { - ret = parseVFile(offset); // for next property length - if (PARSE_ERROR == ret) { - break; - } else { - offset += ret; - sum += ret; - } - } - if (mBuilder != null) { - mBuilder.end(); - } - return (mBuffer.length() == sum); - } - - /** - * Parse the given stream with the default encoding. - * - * @param is - * The source to parse. - * @param builder - * The v builder which used to construct data. - * @return Return true for success, otherwise false. - * @throws IOException - */ - public boolean parse(InputStream is, VBuilder builder) throws IOException { - return parse(is, DEFAULT_CHARSET, builder); - } - - /** - * Copy the content of input stream and filter the "folding" - */ - protected void setInputStream(InputStream is, String encoding) - throws UnsupportedEncodingException { - InputStreamReader reader = new InputStreamReader(is, encoding); - StringBuilder b = new StringBuilder(); - - int ch; - try { - while ((ch = reader.read()) != -1) { - if (ch == '\r') { - ch = reader.read(); - if (ch == '\n') { - ch = reader.read(); - if (ch == ' ' || ch == '\t') { - b.append((char) ch); - continue; - } - b.append("\r\n"); - if (ch == -1) { - break; - } - } else { - b.append("\r"); - } - } - b.append((char) ch); - } - mBuffer = b.toString(); - } catch (Exception e) { - return; - } - return; - } - - /** - * abstract function, waiting implement.
      - * analyse from offset, return the length of consumed property. - */ - abstract protected int parseVFile(int offset); - - /** - * From offset, jump ' ', '\t', '\r\n' sequence, return the length of jump.
      - * 1 * (SPACE / HTAB / CRLF) - */ - protected int parseWsls(int offset) { - int ret = 0, sum = 0; - - try { - char ch = mBuffer.charAt(offset); - if (ch == ' ' || ch == '\t') { - sum++; - offset++; - } else if ((ret = parseCrlf(offset)) != PARSE_ERROR) { - offset += ret; - sum += ret; - } else { - return PARSE_ERROR; - } - for (;;) { - ch = mBuffer.charAt(offset); - if (ch == ' ' || ch == '\t') { - sum++; - offset++; - } else if ((ret = parseCrlf(offset)) != PARSE_ERROR) { - offset += ret; - sum += ret; - } else { - break; - } - } - } catch (IndexOutOfBoundsException e) { - ; - } - if (sum > 0) - return sum; - return PARSE_ERROR; - } - - /** - * To determine if the given string equals to the start of the current - * string. - * - * @param offset - * The offset in buffer of current string - * @param tar - * The given string. - * @param ignoreCase - * To determine case sensitive or not. - * @return The consumed characters, otherwise return PARSE_ERROR. - */ - protected int parseString(int offset, final String tar, boolean ignoreCase) { - int sum = 0; - if (tar == null) { - return PARSE_ERROR; - } - - if (ignoreCase) { - int len = tar.length(); - try { - if (mBuffer.substring(offset, offset + len).equalsIgnoreCase( - tar)) { - sum = len; - } else { - return PARSE_ERROR; - } - } catch (IndexOutOfBoundsException e) { - return PARSE_ERROR; - } - - } else { /* case sensitive */ - if (mBuffer.startsWith(tar, offset)) { - sum = tar.length(); - } else { - return PARSE_ERROR; - } - } - return sum; - } - - /** - * Skip the white space in string. - */ - protected int removeWs(int offset) { - if (offset >= mBuffer.length()) - return PARSE_ERROR; - int sum = 0; - char ch; - while ((ch = mBuffer.charAt(offset)) == ' ' || ch == '\t') { - offset++; - sum++; - } - return sum; - } - - /** - * "X-" word, and its value. Return consumed length. - */ - protected int parseXWord(int offset) { - int ret = 0, sum = 0; - ret = parseString(offset, "X-", true); - if (PARSE_ERROR == ret) - return PARSE_ERROR; - offset += ret; - sum += ret; - - ret = parseWord(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - return sum; - } - - /** - * From offset, parse as :mEncoding ?= 7bit / 8bit / quoted-printable / - * base64 - */ - protected int parseValue(int offset) { - int ret = 0; - - if (mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") - || mEncoding.equalsIgnoreCase("8BIT") - || mEncoding.toUpperCase().startsWith("X-")) { - ret = parse8bit(offset); - if (ret != PARSE_ERROR) { - return ret; - } - return PARSE_ERROR; - } - - if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - ret = parseQuotedPrintable(offset); - if (ret != PARSE_ERROR) { - return ret; - } - return PARSE_ERROR; - } - - if (mEncoding.equalsIgnoreCase("BASE64")) { - ret = parseBase64(offset); - if (ret != PARSE_ERROR) { - return ret; - } - return PARSE_ERROR; - } - return PARSE_ERROR; - } - - /** - * Refer to RFC 1521, 8bit text - */ - protected int parse8bit(int offset) { - int index = 0; - - index = mBuffer.substring(offset).indexOf("\r\n"); - - if (index == -1) - return PARSE_ERROR; - else - return index; - - } - - /** - * Refer to RFC 1521, quoted printable text ([*(ptext / SPACE / TAB) ptext] - * ["="] CRLF) - */ - protected int parseQuotedPrintable(int offset) { - int ret = 0, sum = 0; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - for (;;) { - ret = parsePtext(offset); - if (PARSE_ERROR == ret) - break; - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - } - - ret = parseString(offset, "=", false); - if (ret != PARSE_ERROR) { - // offset += ret; - sum += ret; - } - - return sum; - } - - /** - * return 1 or 3 - * So maybe return 3. - */ - protected int parseOctet(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "=", false); - if (PARSE_ERROR == ret) - return PARSE_ERROR; - offset += ret; - sum += ret; - - try { - int ch = mBuffer.charAt(offset); - if (ch == ' ' || ch == '\t') - return ++sum; - if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) { - offset++; - sum++; - ch = mBuffer.charAt(offset); - if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) { - sum++; - return sum; - } - } - } catch (IndexOutOfBoundsException e) { - ; - } - return PARSE_ERROR; - } - - /** - * Refer to RFC 1521, base64 text The end of the text is marked with two - * CRLF sequences - */ - protected int parseBase64(int offset) { - int sum = 0; - try { - for (;;) { - char ch; - ch = mBuffer.charAt(offset); - - if (ch == '\r') { - int ret = parseString(offset, "\r\n\r\n", false); - sum += ret; - break; - } else { - /* ignore none base64 character */ - sum++; - offset++; - } - } - } catch (IndexOutOfBoundsException e) { - return PARSE_ERROR; - } - sum -= 2;/* leave one CRLF to parse the end of this property */ - return sum; - } - - /** - * Any printable ASCII sequence except [ ]=:.,; - */ - protected int parseWord(int offset) { - int sum = 0; - try { - for (;;) { - char ch = mBuffer.charAt(offset); - if (!isPrintable(ch)) - break; - if (ch == ' ' || ch == '=' || ch == ':' || ch == '.' - || ch == ',' || ch == ';') - break; - if (ch == '\\') { - ch = mBuffer.charAt(offset + 1); - if (ch == ';') { - offset++; - sum++; - } - } - offset++; - sum++; - } - } catch (IndexOutOfBoundsException e) { - ; - } - if (sum == 0) - return PARSE_ERROR; - return sum; - } - - /** - * If it is a letter or digit. - */ - protected boolean isLetterOrDigit(char ch) { - if (ch >= '0' && ch <= '9') - return true; - if (ch >= 'a' && ch <= 'z') - return true; - if (ch >= 'A' && ch <= 'Z') - return true; - return false; - } - - /** - * If it is printable in ASCII - */ - protected boolean isPrintable(char ch) { - if (ch >= ' ' && ch <= '~') - return true; - return false; - } - - /** - * If it is a letter. - */ - protected boolean isLetter(char ch) { - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - return true; - } - return false; - } - - /** - * Get a word from current position. - */ - protected String getWord(int offset) { - StringBuilder word = new StringBuilder(); - try { - for (;;) { - char ch = mBuffer.charAt(offset); - if (isLetterOrDigit(ch) || ch == '-') { - word.append(ch); - offset++; - } else { - break; - } - } - } catch (IndexOutOfBoundsException e) { - ; - } - return word.toString(); - } - - /** - * If is: "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word - */ - protected int parsePValueVal(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "INLINE", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "URL", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "CONTENT-ID", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "CID", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "INLINE", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseXWord(offset); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - return PARSE_ERROR; - } - - /** - * If is: "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word and - * set mEncoding. - */ - protected int parsePEncodingVal(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "7BIT", true); - if (ret != PARSE_ERROR) { - mEncoding = "7BIT"; - sum += ret; - return sum; - } - - ret = parseString(offset, "8BIT", true); - if (ret != PARSE_ERROR) { - mEncoding = "8BIT"; - sum += ret; - return sum; - } - - ret = parseString(offset, "QUOTED-PRINTABLE", true); - if (ret != PARSE_ERROR) { - mEncoding = "QUOTED-PRINTABLE"; - sum += ret; - return sum; - } - - ret = parseString(offset, "BASE64", true); - if (ret != PARSE_ERROR) { - mEncoding = "BASE64"; - sum += ret; - return sum; - } - - ret = parseXWord(offset); - if (ret != PARSE_ERROR) { - mEncoding = mBuffer.substring(offset).substring(0, ret); - sum += ret; - return sum; - } - - return PARSE_ERROR; - } - - /** - * Refer to RFC1521, section 7.1
      - * If is: "us-ascii" / "iso-8859-xxx" / "X-" word - */ - protected int parseCharsetVal(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "us-ascii", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-1", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-2", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-3", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-4", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-5", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-6", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-7", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-8", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseString(offset, "iso-8859-9", true); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - ret = parseXWord(offset); - if (ret != PARSE_ERROR) { - sum += ret; - return sum; - } - - return PARSE_ERROR; - } - - /** - * Refer to RFC 1766
      - * like: XXX(sequence letters)-XXX(sequence letters) - */ - protected int parseLangVal(int offset) { - int ret = 0, sum = 0; - - ret = parseTag(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - for (;;) { - ret = parseString(offset, "-", false); - if (PARSE_ERROR == ret) { - break; - } - offset += ret; - sum += ret; - - ret = parseTag(offset); - if (PARSE_ERROR == ret) { - break; - } - offset += ret; - sum += ret; - } - return sum; - } - - /** - * From first 8 position, is sequence LETTER. - */ - protected int parseTag(int offset) { - int sum = 0, i = 0; - - try { - for (i = 0; i < 8; i++) { - char ch = mBuffer.charAt(offset); - if (!isLetter(ch)) { - break; - } - sum++; - offset++; - } - } catch (IndexOutOfBoundsException e) { - ; - } - if (i == 0) { - return PARSE_ERROR; - } - return sum; - } - -} diff --git a/core/java/android/syncml/pim/package.html b/core/java/android/syncml/pim/package.html deleted file mode 100644 index cb4ca466da01a6c1505c3a009638704294f96562..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/package.html +++ /dev/null @@ -1,6 +0,0 @@ - - -Support classes for SyncML. -{@hide} - - \ No newline at end of file diff --git a/core/java/android/syncml/pim/vcalendar/CalendarStruct.java b/core/java/android/syncml/pim/vcalendar/CalendarStruct.java deleted file mode 100644 index 3388ada3eb4acdcc7c5348040ab5213b40026aa1..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/CalendarStruct.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcalendar; - -import java.util.List; -import java.util.ArrayList; - -/** - * Same comment as ContactStruct. - */ -public class CalendarStruct{ - - public static class EventStruct{ - public String description; - public String dtend; - public String dtstart; - public String duration; - public String has_alarm; - public String last_date; - public String rrule; - public String status; - public String title; - public String event_location; - public String uid; - public List reminderList; - - public void addReminderList(String method){ - if(reminderList == null) - reminderList = new ArrayList(); - reminderList.add(method); - } - } - - public String timezone; - public List eventList; - - public void addEventList(EventStruct stru){ - if(eventList == null) - eventList = new ArrayList(); - eventList.add(stru); - } -} diff --git a/core/java/android/syncml/pim/vcalendar/VCalComposer.java b/core/java/android/syncml/pim/vcalendar/VCalComposer.java deleted file mode 100644 index 18b671914aa7323d8399caeb9a7dffa9878dad46..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/VCalComposer.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcalendar; - -/** - * vCalendar string composer class - */ -public class VCalComposer { - - public final static String VERSION_VCALENDAR10 = "vcalendar1.0"; - public final static String VERSION_VCALENDAR20 = "vcalendar2.0"; - - public final static int VERSION_VCAL10_INT = 1; - public final static int VERSION_VCAL20_INT = 2; - - private static String mNewLine = "\r\n"; - private String mVersion = null; - - public VCalComposer() { - } - - /** - * Create a vCalendar String. - * @param struct see more from CalendarStruct class - * @param vcalversion MUST be VERSION_VCAL10 /VERSION_VCAL20 - * @return vCalendar string - * @throws VcalException if version is invalid or create failed - */ - public String createVCal(CalendarStruct struct, int vcalversion) - throws VCalException{ - - StringBuilder returnStr = new StringBuilder(); - - //Version check - if(vcalversion != 1 && vcalversion != 2) - throw new VCalException("version not match 1.0 or 2.0."); - if (vcalversion == 1) - mVersion = VERSION_VCALENDAR10; - else - mVersion = VERSION_VCALENDAR20; - - //Build vCalendar: - returnStr.append("BEGIN:VCALENDAR").append(mNewLine); - - if(vcalversion == VERSION_VCAL10_INT) - returnStr.append("VERSION:1.0").append(mNewLine); - else - returnStr.append("VERSION:2.0").append(mNewLine); - - returnStr.append("PRODID:vCal ID default").append(mNewLine); - - if(!isNull(struct.timezone)){ - if(vcalversion == VERSION_VCAL10_INT) - returnStr.append("TZ:").append(struct.timezone).append(mNewLine); - else//down here MUST have - returnStr.append("BEGIN:VTIMEZONE").append(mNewLine). - append("TZID:vCal default").append(mNewLine). - append("BEGIN:STANDARD").append(mNewLine). - append("DTSTART:16010101T000000").append(mNewLine). - append("TZOFFSETFROM:").append(struct.timezone).append(mNewLine). - append("TZOFFSETTO:").append(struct.timezone).append(mNewLine). - append("END:STANDARD").append(mNewLine). - append("END:VTIMEZONE").append(mNewLine); - } - //Build VEVNET - for(int i = 0; i < struct.eventList.size(); i++){ - String str = buildEventStr( struct.eventList.get(i) ); - returnStr.append(str); - } - - //Build VTODO - //TODO - - returnStr.append("END:VCALENDAR").append(mNewLine).append(mNewLine); - - return returnStr.toString(); - } - - private String buildEventStr(CalendarStruct.EventStruct stru){ - - StringBuilder strbuf = new StringBuilder(); - - strbuf.append("BEGIN:VEVENT").append(mNewLine); - - if(!isNull(stru.uid)) - strbuf.append("UID:").append(stru.uid).append(mNewLine); - - if(!isNull(stru.description)) - strbuf.append("DESCRIPTION:"). - append(foldingString(stru.description)).append(mNewLine); - - if(!isNull(stru.dtend)) - strbuf.append("DTEND:").append(stru.dtend).append(mNewLine); - - if(!isNull(stru.dtstart)) - strbuf.append("DTSTART:").append(stru.dtstart).append(mNewLine); - - if(!isNull(stru.duration)) - strbuf.append("DUE:").append(stru.duration).append(mNewLine); - - if(!isNull(stru.event_location)) - strbuf.append("LOCATION:").append(stru.event_location).append(mNewLine); - - if(!isNull(stru.last_date)) - strbuf.append("COMPLETED:").append(stru.last_date).append(mNewLine); - - if(!isNull(stru.rrule)) - strbuf.append("RRULE:").append(stru.rrule).append(mNewLine); - - if(!isNull(stru.title)) - strbuf.append("SUMMARY:").append(stru.title).append(mNewLine); - - if(!isNull(stru.status)){ - String stat = "TENTATIVE"; - switch (Integer.parseInt(stru.status)){ - case 0://Calendar.Calendars.STATUS_TENTATIVE - stat = "TENTATIVE"; - break; - case 1://Calendar.Calendars.STATUS_CONFIRMED - stat = "CONFIRMED"; - break; - case 2://Calendar.Calendars.STATUS_CANCELED - stat = "CANCELLED"; - break; - } - strbuf.append("STATUS:").append(stat).append(mNewLine); - } - //Alarm - if(!isNull(stru.has_alarm) - && stru.reminderList != null - && stru.reminderList.size() > 0){ - - if (mVersion.equals(VERSION_VCALENDAR10)){ - String prefix = ""; - for(String method : stru.reminderList){ - switch (Integer.parseInt(method)){ - case 0: - prefix = "DALARM"; - break; - case 1: - prefix = "AALARM"; - break; - case 2: - prefix = "MALARM"; - break; - case 3: - default: - prefix = "DALARM"; - break; - } - strbuf.append(prefix).append(":default").append(mNewLine); - } - }else {//version 2.0 only support audio-method now. - strbuf.append("BEGIN:VALARM").append(mNewLine). - append("ACTION:AUDIO").append(mNewLine). - append("TRIGGER:-PT10M").append(mNewLine). - append("END:VALARM").append(mNewLine); - } - } - strbuf.append("END:VEVENT").append(mNewLine); - return strbuf.toString(); - } - - /** Alter str to folding supported format. */ - private String foldingString(String str){ - return str.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n "); - } - - /** is null */ - private boolean isNull(String str){ - if(str == null || str.trim().equals("")) - return true; - return false; - } -} diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser.java b/core/java/android/syncml/pim/vcalendar/VCalParser.java deleted file mode 100644 index bc2d5981fc70fabef79e33f9498ba0a077f0bb31..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/VCalParser.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcalendar; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import android.util.Config; -import android.util.Log; - -import android.syncml.pim.VDataBuilder; -import android.syncml.pim.VParser; - -public class VCalParser{ - - private final static String TAG = "VCalParser"; - - public final static String VERSION_VCALENDAR10 = "vcalendar1.0"; - public final static String VERSION_VCALENDAR20 = "vcalendar2.0"; - - private VParser mParser = null; - private String mVersion = null; - - public VCalParser() { - } - - public boolean parse(String vcalendarStr, VDataBuilder builder) - throws VCalException { - - vcalendarStr = verifyVCal(vcalendarStr); - try{ - boolean isSuccess = mParser.parse( - new ByteArrayInputStream(vcalendarStr.getBytes()), - "US-ASCII", builder); - - if (!isSuccess) { - if (mVersion.equals(VERSION_VCALENDAR10)) { - if(Config.LOGD) - Log.d(TAG, "Parse failed for vCal 1.0 parser." - + " Try to use 2.0 parser."); - mVersion = VERSION_VCALENDAR20; - return parse(vcalendarStr, builder); - }else - throw new VCalException("parse failed.(even use 2.0 parser)"); - } - }catch (IOException e){ - throw new VCalException(e.getMessage()); - } - return true; - } - - /** - * Verify vCalendar string, and initialize mVersion according to it. - * */ - private String verifyVCal(String vcalStr) { - - //Version check - judgeVersion(vcalStr); - - vcalStr = vcalStr.replaceAll("\r\n", "\n"); - String[] strlist = vcalStr.split("\n"); - - StringBuilder replacedStr = new StringBuilder(); - - for (int i = 0; i < strlist.length; i++) { - if (strlist[i].indexOf(":") < 0) { - if (strlist[i].length() == 0 && strlist[i + 1].indexOf(":") > 0) - replacedStr.append(strlist[i]).append("\r\n"); - else - replacedStr.append(" ").append(strlist[i]).append("\r\n"); - } else - replacedStr.append(strlist[i]).append("\r\n"); - } - if(Config.LOGD)Log.d(TAG, "After verify:\r\n" + replacedStr.toString()); - - return replacedStr.toString(); - } - - /** - * If version not given. Search from vcal string of the VERSION property. - * Then instance mParser to appropriate parser. - */ - private void judgeVersion(String vcalStr) { - - if (mVersion == null) { - int versionIdx = vcalStr.indexOf("\nVERSION:"); - - mVersion = VERSION_VCALENDAR10; - - if (versionIdx != -1){ - String versionStr = vcalStr.substring( - versionIdx, vcalStr.indexOf("\n", versionIdx + 1)); - if (versionStr.indexOf("2.0") > 0) - mVersion = VERSION_VCALENDAR20; - } - } - if (mVersion.equals(VERSION_VCALENDAR10)) - mParser = new VCalParser_V10(); - if (mVersion.equals(VERSION_VCALENDAR20)) - mParser = new VCalParser_V20(); - } -} - diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java b/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java deleted file mode 100644 index 1b251f3434453816bc617849ea64e0d7b8b316b9..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcalendar; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import android.syncml.pim.VParser; - -public class VCalParser_V10 extends VParser { - - /* - * The names of the properties whose value are not separated by ";" - */ - private static final HashSet mEvtPropNameGroup1 = new HashSet( - Arrays.asList("ATTACH", "ATTENDEE", "DCREATED", "COMPLETED", - "DESCRIPTION", "DUE", "DTEND", "EXRULE", "LAST-MODIFIED", - "LOCATION", "RNUM", "PRIORITY", "RELATED-TO", "RRULE", - "SEQUENCE", "DTSTART", "SUMMARY", "TRANSP", "URL", "UID", - // above belong to simprop - "CLASS", "STATUS")); - - /* - * The names of properties whose value are separated by ";" - */ - private static final HashSet mEvtPropNameGroup2 = new HashSet( - Arrays.asList("AALARM", "CATEGORIES", "DALARM", "EXDATE", "MALARM", - "PALARM", "RDATE", "RESOURCES")); - - private static final HashSet mValueCAT = new HashSet(Arrays - .asList("APPOINTMENT", "BUSINESS", "EDUCATION", "HOLIDAY", - "MEETING", "MISCELLANEOUS", "PERSONAL", "PHONE CALL", - "SICK DAY", "SPECIAL OCCASION", "TRAVEL", "VACATION")); - - private static final HashSet mValueCLASS = new HashSet(Arrays - .asList("PUBLIC", "PRIVATE", "CONFIDENTIAL")); - - private static final HashSet mValueRES = new HashSet(Arrays - .asList("CATERING", "CHAIRS", "EASEL", "PROJECTOR", "VCR", - "VEHICLE")); - - private static final HashSet mValueSTAT = new HashSet(Arrays - .asList("ACCEPTED", "NEEDS ACTION", "SENT", "TENTATIVE", - "CONFIRMED", "DECLINED", "COMPLETED", "DELEGATED")); - - /* - * The names of properties whose value can contain escape characters - */ - private static final HashSet mEscAllowedProps = new HashSet( - Arrays.asList("DESCRIPTION", "SUMMARY", "AALARM", "DALARM", - "MALARM", "PALARM")); - - private static final HashMap> mSpecialValueSetMap = - new HashMap>(); - - static { - mSpecialValueSetMap.put("CATEGORIES", mValueCAT); - mSpecialValueSetMap.put("CLASS", mValueCLASS); - mSpecialValueSetMap.put("RESOURCES", mValueRES); - mSpecialValueSetMap.put("STATUS", mValueSTAT); - } - - public VCalParser_V10() { - } - - protected int parseVFile(int offset) { - return parseVCalFile(offset); - } - - private int parseVCalFile(int offset) { - int ret = 0, sum = 0; - - /* remove wsls */ - while (PARSE_ERROR != (ret = parseWsls(offset))) { - offset += ret; - sum += ret; - } - - ret = parseVCal(offset); // BEGIN:VCAL ... END:VCAL - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - } else { - return PARSE_ERROR; - } - - /* remove wsls */ - while (PARSE_ERROR != (ret = parseWsls(offset))) { - offset += ret; - sum += ret; - } - return sum; - } - - /** - * "BEGIN" [ws] ":" [ws] "VCALENDAR" [ws] 1*crlf calprop calentities [ws] - * *crlf "END" [ws] ":" [ws] "VCALENDAR" [ws] 1*CRLF - */ - private int parseVCal(int offset) { - int ret = 0, sum = 0; - - /* BEGIN */ - ret = parseString(offset, "BEGIN", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VCALENDAR - ret = parseString(offset, "VCALENDAR", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.startRecord("VCALENDAR"); - } - - /* [ws] */ - ret = removeWs(offset); - offset += ret; - sum += ret; - - // 1*CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - // calprop - ret = parseCalprops(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // calentities - ret = parseCalentities(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // *CRLF - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - // "END" - ret = parseString(offset, "END", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VCALENDAR" - ret = parseString(offset, "VCALENDAR", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endRecord(); - } - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // 1 * CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - return sum; - } - - /** - * calprops * CRLF calprop / calprop - */ - private int parseCalprops(int offset) { - int ret = 0, sum = 0; - - if (mBuilder != null) { - mBuilder.startProperty(); - } - ret = parseCalprop(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endProperty(); - } - - for (;;) { - /* *CRLF */ - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - // follow VEVENT ,it wont reach endProperty - if (mBuilder != null) { - mBuilder.startProperty(); - } - ret = parseCalprop(offset); - if (PARSE_ERROR == ret) { - break; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endProperty(); - } - } - - return sum; - } - - /** - * calentities *CRLF calentity / calentity - */ - private int parseCalentities(int offset) { - int ret = 0, sum = 0; - - ret = parseCalentity(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - for (;;) { - /* *CRLF */ - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - ret = parseCalentity(offset); - if (PARSE_ERROR == ret) { - break; - } - offset += ret; - sum += ret; - } - - return sum; - } - - /** - * calprop = DAYLIGHT/ GEO/ PRODID/ TZ/ VERSION - */ - private int parseCalprop(int offset) { - int ret = 0; - - ret = parseCalprop0(offset, "DAYLIGHT"); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseCalprop0(offset, "GEO"); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseCalprop0(offset, "PRODID"); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseCalprop0(offset, "TZ"); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseCalprop1(offset); - if (PARSE_ERROR != ret) { - return ret; - } - return PARSE_ERROR; - } - - /** - * evententity / todoentity - */ - private int parseCalentity(int offset) { - int ret = 0; - - ret = parseEvententity(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseTodoentity(offset); - if (PARSE_ERROR != ret) { - return ret; - } - return PARSE_ERROR; - - } - - /** - * propName [params] ":" value CRLF - */ - private int parseCalprop0(int offset, String propName) { - int ret = 0, sum = 0, start = 0; - - ret = parseString(offset, propName, true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyName(propName); - } - - ret = parseParams(offset); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - } - - ret = parseString(offset, ":", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - start = offset; - ret = parseValue(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - ArrayList v = new ArrayList(); - v.add(mBuffer.substring(start, offset)); - mBuilder.propertyValues(v); - } - - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - - return sum; - } - - /** - * "VERSION" [params] ":" "1.0" CRLF - */ - private int parseCalprop1(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "VERSION", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyName("VERSION"); - } - - ret = parseParams(offset); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - } - - ret = parseString(offset, ":", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = parseString(offset, "1.0", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - ArrayList v = new ArrayList(); - v.add("1.0"); - mBuilder.propertyValues(v); - } - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - - return sum; - } - - /** - * "BEGIN" [ws] ":" [ws] "VEVENT" [ws] 1*CRLF entprops [ws] *CRLF "END" [ws] - * ":" [ws] "VEVENT" [ws] 1*CRLF - */ - private int parseEvententity(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "BEGIN", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VEVNET" - ret = parseString(offset, "VEVENT", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.startRecord("VEVENT"); - } - - /* [ws] */ - ret = removeWs(offset); - offset += ret; - sum += ret; - - // 1*CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - ret = parseEntprops(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // *CRLF - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - // "END" - ret = parseString(offset, "END", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VEVENT" - ret = parseString(offset, "VEVENT", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endRecord(); - } - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // 1 * CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - return sum; - } - - /** - * "BEGIN" [ws] ":" [ws] "VTODO" [ws] 1*CRLF entprops [ws] *CRLF "END" [ws] - * ":" [ws] "VTODO" [ws] 1*CRLF - */ - private int parseTodoentity(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, "BEGIN", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VTODO" - ret = parseString(offset, "VTODO", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.startRecord("VTODO"); - } - - // 1*CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - ret = parseEntprops(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // *CRLF - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - // "END" - ret = parseString(offset, "END", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // ":" - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // "VTODO" - ret = parseString(offset, "VTODO", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endRecord(); - } - - // [ws] - ret = removeWs(offset); - offset += ret; - sum += ret; - - // 1 * CRLF - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - - return sum; - } - - /** - * entprops *CRLF entprop / entprop - */ - private int parseEntprops(int offset) { - int ret = 0, sum = 0; - if (mBuilder != null) { - mBuilder.startProperty(); - } - - ret = parseEntprop(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endProperty(); - } - - for (;;) { - while (PARSE_ERROR != (ret = parseCrlf(offset))) { - offset += ret; - sum += ret; - } - if (mBuilder != null) { - mBuilder.startProperty(); - } - - ret = parseEntprop(offset); - if (PARSE_ERROR == ret) { - break; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.endProperty(); - } - } - return sum; - } - - /** - * for VEVENT,VTODO prop. entprop0 / entprop1 - */ - private int parseEntprop(int offset) { - int ret = 0; - ret = parseEntprop0(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseEntprop1(offset); - if (PARSE_ERROR != ret) { - return ret; - } - return PARSE_ERROR; - } - - /** - * Same with card. ";" [ws] paramlist - */ - private int parseParams(int offset) { - int ret = 0, sum = 0; - - ret = parseString(offset, ";", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseParamlist(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - - return sum; - } - - /** - * Same with card. paramlist [ws] ";" [ws] param / param - */ - private int parseParamlist(int offset) { - int ret = 0, sum = 0; - - ret = parseParam(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - int offsetTemp = offset; - int sumTemp = sum; - for (;;) { - ret = removeWs(offsetTemp); - offsetTemp += ret; - sumTemp += ret; - - ret = parseString(offsetTemp, ";", false); - if (PARSE_ERROR == ret) { - return sum; - } - offsetTemp += ret; - sumTemp += ret; - - ret = removeWs(offsetTemp); - offsetTemp += ret; - sumTemp += ret; - - ret = parseParam(offsetTemp); - if (PARSE_ERROR == ret) { - break; - } - offsetTemp += ret; - sumTemp += ret; - - // offset = offsetTemp; - sum = sumTemp; - } - return sum; - } - - /** - * param0 - param7 / knowntype - */ - private int parseParam(int offset) { - int ret = 0; - - ret = parseParam0(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam1(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam2(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam3(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam4(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam5(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam6(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseParam7(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - int start = offset; - ret = parseKnownType(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(null); - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return ret; - } - - /** - * simprop AND "CLASS" AND "STATUS" The value of these properties are not - * seperated by ";" - * - * [ws] simprop [params] ":" value CRLF - */ - private int parseEntprop0(int offset) { - int ret = 0, sum = 0, start = 0; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - String propName = getWord(offset).toUpperCase(); - if (!mEvtPropNameGroup1.contains(propName)) { - if (PARSE_ERROR == parseXWord(offset)) - return PARSE_ERROR; - } - ret = propName.length(); - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyName(propName); - } - - ret = parseParams(offset); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - } - - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - start = offset; - ret = parseValue(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - ArrayList v = new ArrayList(); - v.add(exportEntpropValue(propName, mBuffer.substring(start, - offset))); - mBuilder.propertyValues(v); - // Filter value,match string, REFER:RFC - if (PARSE_ERROR == valueFilter(propName, v)) - return PARSE_ERROR; - } - - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - return sum; - } - - /** - * other event prop names except simprop AND "CLASS" AND "STATUS" The value - * of these properties are seperated by ";" [ws] proper name [params] ":" - * value CRLF - */ - private int parseEntprop1(int offset) { - int ret = 0, sum = 0; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - String propName = getWord(offset).toUpperCase(); - if (!mEvtPropNameGroup2.contains(propName)) { - return PARSE_ERROR; - } - ret = propName.length(); - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyName(propName); - } - - ret = parseParams(offset); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - } - - ret = parseString(offset, ":", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - int start = offset; - ret = parseValue(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - // mutil-values - if (mBuilder != null) { - int end = 0; - ArrayList v = new ArrayList(); - Pattern p = Pattern - .compile("([^;\\\\]*(\\\\[\\\\;:,])*[^;\\\\]*)(;?)"); - Matcher m = p.matcher(mBuffer.substring(start, offset)); - while (m.find()) { - String s = exportEntpropValue(propName, m.group(1)); - v.add(s); - end = m.end(); - if (offset == start + end) { - String endValue = m.group(3); - if (";".equals(endValue)) { - v.add(""); - } - break; - } - } - mBuilder.propertyValues(v); - // Filter value,match string, REFER:RFC - if (PARSE_ERROR == valueFilter(propName, v)) - return PARSE_ERROR; - } - - ret = parseCrlf(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - sum += ret; - return sum; - } - - /** - * "TYPE" [ws] = [ws] ptypeval - */ - private int parseParam0(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseString(offset, "TYPE", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", false); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parsePtypeval(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - return sum; - } - - /** - * ["VALUE" [ws] "=" [ws]] pvalueval - */ - private int parseParam1(int offset) { - int ret = 0, sum = 0, start = offset; - boolean flag = false; - - ret = parseString(offset, "VALUE", true); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - flag = true; - } - if (flag == true && mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR != ret) { - if (flag == false) { // "VALUE" does not exist - return PARSE_ERROR; - } - offset += ret; - sum += ret; - } else { - if (flag == true) { // "VALUE" exists - return PARSE_ERROR; - } - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parsePValueVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - } - - /** ["ENCODING" [ws] "=" [ws]] pencodingval */ - private int parseParam2(int offset) { - int ret = 0, sum = 0, start = offset; - boolean flag = false; - - ret = parseString(offset, "ENCODING", true); - if (PARSE_ERROR != ret) { - offset += ret; - sum += ret; - flag = true; - } - if (flag == true && mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR != ret) { - if (flag == false) { // "VALUE" does not exist - return PARSE_ERROR; - } - offset += ret; - sum += ret; - } else { - if (flag == true) { // "VALUE" exists - return PARSE_ERROR; - } - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parsePEncodingVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - } - - /** - * "CHARSET" [WS] "=" [WS] charsetval - */ - private int parseParam3(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseString(offset, "CHARSET", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parseCharsetVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - } - - /** - * "LANGUAGE" [ws] "=" [ws] langval - */ - private int parseParam4(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseString(offset, "LANGUAGE", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parseLangVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - } - - /** - * "ROLE" [ws] "=" [ws] roleval - */ - private int parseParam5(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseString(offset, "ROLE", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parseRoleVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - } - - /** - * "STATUS" [ws] = [ws] statuval - */ - private int parseParam6(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseString(offset, "STATUS", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parseStatuVal(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - - } - - /** - * XWord [ws] "=" [ws] word - */ - private int parseParam7(int offset) { - int ret = 0, sum = 0, start = offset; - - ret = parseXWord(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamType(mBuffer.substring(start, offset)); - } - - ret = removeWs(offset); - offset += ret; - sum += ret; - - ret = parseString(offset, "=", true); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - - ret = removeWs(offset); - offset += ret; - sum += ret; - - start = offset; - ret = parseWord(offset); - if (PARSE_ERROR == ret) { - return PARSE_ERROR; - } - offset += ret; - sum += ret; - if (mBuilder != null) { - mBuilder.propertyParamValue(mBuffer.substring(start, offset)); - } - - return sum; - - } - - /* - * "WAVE" / "PCM" / "VCARD" / XWORD - */ - private int parseKnownType(int offset) { - int ret = 0; - - ret = parseString(offset, "WAVE", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "PCM", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "VCARD", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseXWord(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - return PARSE_ERROR; - } - - /* - * knowntype / Xword - */ - private int parsePtypeval(int offset) { - int ret = 0; - - ret = parseKnownType(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseXWord(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - return PARSE_ERROR; - } - - /** - * "ATTENDEE" / "ORGANIZER" / "OWNER" / XWORD - */ - private int parseRoleVal(int offset) { - int ret = 0; - - ret = parseString(offset, "ATTENDEE", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "ORGANIZER", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "OWNER", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseXWord(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - return PARSE_ERROR; - } - - /** - * "ACCEPTED" / "NEED ACTION" / "SENT" / "TENTATIVE" / "CONFIRMED" / - * "DECLINED" / "COMPLETED" / "DELEGATED / XWORD - */ - private int parseStatuVal(int offset) { - int ret = 0; - - ret = parseString(offset, "ACCEPTED", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "NEED ACTION", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseString(offset, "TENTATIVE", true); - if (PARSE_ERROR != ret) { - return ret; - } - ret = parseString(offset, "CONFIRMED", true); - if (PARSE_ERROR != ret) { - return ret; - } - ret = parseString(offset, "DECLINED", true); - if (PARSE_ERROR != ret) { - return ret; - } - ret = parseString(offset, "COMPLETED", true); - if (PARSE_ERROR != ret) { - return ret; - } - ret = parseString(offset, "DELEGATED", true); - if (PARSE_ERROR != ret) { - return ret; - } - - ret = parseXWord(offset); - if (PARSE_ERROR != ret) { - return ret; - } - - return PARSE_ERROR; - } - - /** - * Check 4 special propName and it's value to match Hash. - * - * @return PARSE_ERROR:value not match. 1:go on,like nothing happen. - */ - private int valueFilter(String propName, ArrayList values) { - if (propName == null || propName.equals("") || values == null - || values.isEmpty()) - return 1; // go on, like nothing happen. - - if (mSpecialValueSetMap.containsKey(propName)) { - for (String value : values) { - if (!mSpecialValueSetMap.get(propName).contains(value)) { - if (!value.startsWith("X-")) - return PARSE_ERROR; - } - } - } - - return 1; - } - - /** - * - * Translate escape characters("\\", "\;") which define in vcalendar1.0 - * spec. But for fault tolerance, we will translate "\:" and "\,", which - * isn't define in vcalendar1.0 explicitly, as the same behavior as other - * client. - * - * Though vcalendar1.0 spec does not defined the value of property - * "description", "summary", "aalarm", "dalarm", "malarm" and "palarm" could - * contain escape characters, we do support escape characters in these - * properties. - * - * @param str: - * the value string will be translated. - * @return the string which do not contain any escape character in - * vcalendar1.0 - */ - private String exportEntpropValue(String propName, String str) { - if (null == propName || null == str) - return null; - if ("".equals(propName) || "".equals(str)) - return ""; - - if (!mEscAllowedProps.contains(propName)) - return str; - - String tmp = str.replace("\\\\", "\n\r\n"); - tmp = tmp.replace("\\;", ";"); - tmp = tmp.replace("\\:", ":"); - tmp = tmp.replace("\\,", ","); - tmp = tmp.replace("\n\r\n", "\\"); - return tmp; - } -} diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java b/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java deleted file mode 100644 index 5748379a4d96ff0e7ff1d69f41ba74f6293713af..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcalendar; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.HashSet; - -import android.syncml.pim.VBuilder; - -public class VCalParser_V20 extends VCalParser_V10 { - private static final String V10LINEBREAKER = "\r\n"; - - private static final HashSet acceptableComponents = new HashSet( - Arrays.asList("VEVENT", "VTODO", "VALARM", "VTIMEZONE")); - - private static final HashSet acceptableV20Props = new HashSet( - Arrays.asList("DESCRIPTION", "DTEND", "DTSTART", "DUE", - "COMPLETED", "RRULE", "STATUS", "SUMMARY", "LOCATION")); - - private boolean hasTZ = false; // MUST only have one TZ property - - private String[] lines; - - private int index; - - @Override - public boolean parse(InputStream is, String encoding, VBuilder builder) - throws IOException { - // get useful info for android calendar, and alter to vcal 1.0 - byte[] bytes = new byte[is.available()]; - is.read(bytes); - String scStr = new String(bytes); - StringBuilder v10str = new StringBuilder(""); - - lines = splitProperty(scStr); - index = 0; - - if ("BEGIN:VCALENDAR".equals(lines[index])) - v10str.append("BEGIN:VCALENDAR" + V10LINEBREAKER); - else - return false; - index++; - if (false == parseV20Calbody(lines, v10str) - || index > lines.length - 1) - return false; - - if (lines.length - 1 == index && "END:VCALENDAR".equals(lines[index])) - v10str.append("END:VCALENDAR" + V10LINEBREAKER); - else - return false; - - return super.parse( - // use vCal 1.0 parser - new ByteArrayInputStream(v10str.toString().getBytes()), - encoding, builder); - } - - /** - * Parse and pick acceptable iCalendar body and translate it to - * calendarV1.0 format. - * @param lines iCalendar components/properties line list. - * @param buffer calendarV10 format string buffer - * @return true for success, or false - */ - private boolean parseV20Calbody(String[] lines, StringBuilder buffer) { - try { - while (!"VERSION:2.0".equals(lines[index])) - index++; - buffer.append("VERSION:1.0" + V10LINEBREAKER); - - index++; - for (; index < lines.length - 1; index++) { - String[] keyAndValue = lines[index].split(":", 2); - String key = keyAndValue[0]; - String value = keyAndValue[1]; - - if ("BEGIN".equals(key.trim())) { - if (!key.equals(key.trim())) - return false; // MUST be "BEGIN:componentname" - index++; - if (false == parseV20Component(value, buffer)) - return false; - } - } - } catch (ArrayIndexOutOfBoundsException e) { - return false; - } - - return true; - } - - /** - * Parse and pick acceptable calendar V2.0's component and translate it to - * V1.0 format. - * @param compName component name - * @param buffer calendarV10 format string buffer - * @return true for success, or false - * @throws ArrayIndexOutOfBoundsException - */ - private boolean parseV20Component(String compName, StringBuilder buffer) - throws ArrayIndexOutOfBoundsException { - String endTag = "END:" + compName; - String[] propAndValue; - String propName, value; - - if (acceptableComponents.contains(compName)) { - if ("VEVENT".equals(compName) || "VTODO".equals(compName)) { - buffer.append("BEGIN:" + compName + V10LINEBREAKER); - while (!endTag.equals(lines[index])) { - propAndValue = lines[index].split(":", 2); - propName = propAndValue[0].split(";", 2)[0]; - value = propAndValue[1]; - - if ("".equals(lines[index])) - buffer.append(V10LINEBREAKER); - else if (acceptableV20Props.contains(propName)) { - buffer.append(propName + ":" + value + V10LINEBREAKER); - } else if ("BEGIN".equals(propName.trim())) { - // MUST be BEGIN:VALARM - if (propName.equals(propName.trim()) - && "VALARM".equals(value)) { - buffer.append("AALARM:default" + V10LINEBREAKER); - while (!"END:VALARM".equals(lines[index])) - index++; - } else - return false; - } - index++; - } // end while - buffer.append(endTag + V10LINEBREAKER); - } else if ("VALARM".equals(compName)) { // VALARM component MUST - // only appear within either VEVENT or VTODO - return false; - } else if ("VTIMEZONE".equals(compName)) { - do { - if (false == hasTZ) {// MUST only have 1 time TZ property - propAndValue = lines[index].split(":", 2); - propName = propAndValue[0].split(";", 2)[0]; - - if ("TZOFFSETFROM".equals(propName)) { - value = propAndValue[1]; - buffer.append("TZ" + ":" + value + V10LINEBREAKER); - hasTZ = true; - } - } - index++; - } while (!endTag.equals(lines[index])); - } else - return false; - } else { - while (!endTag.equals(lines[index])) - index++; - } - - return true; - } - - /** split ever property line to String[], not split folding line. */ - private String[] splitProperty(String scStr) { - /* - * Property splitted by \n, and unfold folding lines by removing - * CRLF+LWSP-char - */ - scStr = scStr.replaceAll("\r\n", "\n").replaceAll("\n ", "") - .replaceAll("\n\t", ""); - String[] strs = scStr.split("\n"); - return strs; - } -} diff --git a/core/java/android/syncml/pim/vcalendar/package.html b/core/java/android/syncml/pim/vcalendar/package.html deleted file mode 100644 index cb4ca466da01a6c1505c3a009638704294f96562..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcalendar/package.html +++ /dev/null @@ -1,6 +0,0 @@ - - -Support classes for SyncML. -{@hide} - - \ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/ContactStruct.java b/core/java/android/syncml/pim/vcard/ContactStruct.java deleted file mode 100644 index ecd719da069d61efb48a553a4eb819eb7b6a5777..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcard/ContactStruct.java +++ /dev/null @@ -1,978 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcard; - -import android.content.AbstractSyncableContentProvider; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.net.Uri; -import android.provider.Contacts; -import android.provider.Contacts.ContactMethods; -import android.provider.Contacts.Extensions; -import android.provider.Contacts.GroupMembership; -import android.provider.Contacts.Organizations; -import android.provider.Contacts.People; -import android.provider.Contacts.Phones; -import android.provider.Contacts.Photos; -import android.syncml.pim.PropertyNode; -import android.syncml.pim.VNode; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; - -/** - * The parameter class of VCardComposer. - * This class standy by the person-contact in - * Android system, we must use this class instance as parameter to transmit to - * VCardComposer so that create vCard string. - */ -// TODO: rename the class name, next step -public class ContactStruct { - private static final String LOG_TAG = "ContactStruct"; - - // Note: phonetic name probably should be "LAST FIRST MIDDLE" for European languages, and - // space should be added between each element while it should not be in Japanese. - // But unfortunately, we currently do not have the data and are not sure whether we should - // support European version of name ordering. - // - // TODO: Implement the logic described above if we really need European version of - // phonetic name handling. Also, adding the appropriate test case of vCard would be - // highly appreciated. - public static final int NAME_ORDER_TYPE_ENGLISH = 0; - public static final int NAME_ORDER_TYPE_JAPANESE = 1; - - /** MUST exist */ - public String name; - public String phoneticName; - /** maybe folding */ - public List notes = new ArrayList(); - /** maybe folding */ - public String title; - /** binary bytes of pic. */ - public byte[] photoBytes; - /** The type of Photo (e.g. JPEG, BMP, etc.) */ - public String photoType; - /** Only for GET. Use addPhoneList() to PUT. */ - public List phoneList; - /** Only for GET. Use addContactmethodList() to PUT. */ - public List contactmethodList; - /** Only for GET. Use addOrgList() to PUT. */ - public List organizationList; - /** Only for GET. Use addExtension() to PUT */ - public Map> extensionMap; - - // Use organizationList instead when handling ORG. - @Deprecated - public String company; - - public static class PhoneData { - public int type; - /** maybe folding */ - public String data; - public String label; - public boolean isPrimary; - } - - public static class ContactMethod { - // Contacts.KIND_EMAIL, Contacts.KIND_POSTAL - public int kind; - // e.g. Contacts.ContactMethods.TYPE_HOME, Contacts.PhoneColumns.TYPE_HOME - // If type == Contacts.PhoneColumns.TYPE_CUSTOM, label is used. - public int type; - public String data; - // Used only when TYPE is TYPE_CUSTOM. - public String label; - public boolean isPrimary; - } - - public static class OrganizationData { - public int type; - public String companyName; - public String positionName; - public boolean isPrimary; - } - - /** - * Add a phone info to phoneList. - * @param data phone number - * @param type type col of content://contacts/phones - * @param label lable col of content://contacts/phones - */ - public void addPhone(int type, String data, String label, boolean isPrimary){ - if (phoneList == null) { - phoneList = new ArrayList(); - } - PhoneData phoneData = new PhoneData(); - phoneData.type = type; - - StringBuilder builder = new StringBuilder(); - String trimed = data.trim(); - int length = trimed.length(); - for (int i = 0; i < length; i++) { - char ch = trimed.charAt(i); - if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { - builder.append(ch); - } - } - phoneData.data = PhoneNumberUtils.formatNumber(builder.toString()); - phoneData.label = label; - phoneData.isPrimary = isPrimary; - phoneList.add(phoneData); - } - - /** - * Add a contactmethod info to contactmethodList. - * @param kind integer value defined in Contacts.java - * (e.g. Contacts.KIND_EMAIL) - * @param type type col of content://contacts/contact_methods - * @param data contact data - * @param label extra string used only when kind is Contacts.KIND_CUSTOM. - */ - public void addContactmethod(int kind, int type, String data, - String label, boolean isPrimary){ - if (contactmethodList == null) { - contactmethodList = new ArrayList(); - } - ContactMethod contactMethod = new ContactMethod(); - contactMethod.kind = kind; - contactMethod.type = type; - contactMethod.data = data; - contactMethod.label = label; - contactMethod.isPrimary = isPrimary; - contactmethodList.add(contactMethod); - } - - /** - * Add a Organization info to organizationList. - */ - public void addOrganization(int type, String companyName, String positionName, - boolean isPrimary) { - if (organizationList == null) { - organizationList = new ArrayList(); - } - OrganizationData organizationData = new OrganizationData(); - organizationData.type = type; - organizationData.companyName = companyName; - organizationData.positionName = positionName; - organizationData.isPrimary = isPrimary; - organizationList.add(organizationData); - } - - /** - * Set "position" value to the appropriate data. If there's more than one - * OrganizationData objects, the value is set to the last one. If there's no - * OrganizationData object, a new OrganizationData is created, whose company name is - * empty. - * - * TODO: incomplete logic. fix this: - * - * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not - * know how to handle it in general cases... - * ---- - * TITLE:Software Engineer - * ORG:Google - * ---- - */ - public void setPosition(String positionValue) { - if (organizationList == null) { - organizationList = new ArrayList(); - } - int size = organizationList.size(); - if (size == 0) { - addOrganization(Contacts.OrganizationColumns.TYPE_OTHER, "", null, false); - size = 1; - } - OrganizationData lastData = organizationList.get(size - 1); - lastData.positionName = positionValue; - } - - public void addExtension(PropertyNode propertyNode) { - if (propertyNode.propValue.length() == 0) { - return; - } - // Now store the string into extensionMap. - List list; - String name = propertyNode.propName; - if (extensionMap == null) { - extensionMap = new HashMap>(); - } - if (!extensionMap.containsKey(name)){ - list = new ArrayList(); - extensionMap.put(name, list); - } else { - list = extensionMap.get(name); - } - - list.add(propertyNode.encode()); - } - - private static String getNameFromNProperty(List elems, int nameOrderType) { - // Family, Given, Middle, Prefix, Suffix. (1 - 5) - int size = elems.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - boolean builderIsEmpty = true; - // Prefix - if (size > 3 && elems.get(3).length() > 0) { - builder.append(elems.get(3)); - builderIsEmpty = false; - } - String first, second; - if (nameOrderType == NAME_ORDER_TYPE_JAPANESE) { - first = elems.get(0); - second = elems.get(1); - } else { - first = elems.get(1); - second = elems.get(0); - } - if (first.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(first); - builderIsEmpty = false; - } - // Middle name - if (size > 2 && elems.get(2).length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(elems.get(2)); - builderIsEmpty = false; - } - if (second.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(second); - builderIsEmpty = false; - } - // Suffix - if (size > 4 && elems.get(4).length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(elems.get(4)); - builderIsEmpty = false; - } - return builder.toString(); - } else if (size == 1) { - return elems.get(0); - } else { - return ""; - } - } - - public static ContactStruct constructContactFromVNode(VNode node, - int nameOrderType) { - if (!node.VName.equals("VCARD")) { - // Impossible in current implementation. Just for safety. - Log.e(LOG_TAG, "Non VCARD data is inserted."); - return null; - } - - // For name, there are three fields in vCard: FN, N, NAME. - // We prefer FN, which is a required field in vCard 3.0 , but not in vCard 2.1. - // Next, we prefer NAME, which is defined only in vCard 3.0. - // Finally, we use N, which is a little difficult to parse. - String fullName = null; - String nameFromNProperty = null; - - // Some vCard has "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", and - // "X-PHONETIC-LAST-NAME" - String xPhoneticFirstName = null; - String xPhoneticMiddleName = null; - String xPhoneticLastName = null; - - ContactStruct contact = new ContactStruct(); - - // Each Column of four properties has ISPRIMARY field - // (See android.provider.Contacts) - // If false even after the following loop, we choose the first - // entry as a "primary" entry. - boolean prefIsSetAddress = false; - boolean prefIsSetPhone = false; - boolean prefIsSetEmail = false; - boolean prefIsSetOrganization = false; - - for (PropertyNode propertyNode: node.propList) { - String name = propertyNode.propName; - - if (TextUtils.isEmpty(propertyNode.propValue)) { - continue; - } - - if (name.equals("VERSION")) { - // vCard version. Ignore this. - } else if (name.equals("FN")) { - fullName = propertyNode.propValue; - } else if (name.equals("NAME") && fullName == null) { - // Only in vCard 3.0. Use this if FN does not exist. - // Though, note that vCard 3.0 requires FN. - fullName = propertyNode.propValue; - } else if (name.equals("N")) { - nameFromNProperty = getNameFromNProperty(propertyNode.propValue_vector, - nameOrderType); - } else if (name.equals("SORT-STRING")) { - contact.phoneticName = propertyNode.propValue; - } else if (name.equals("SOUND")) { - if (propertyNode.paramMap_TYPE.contains("X-IRMC-N") && - contact.phoneticName == null) { - // Some Japanese mobile phones use this field for phonetic name, - // since vCard 2.1 does not have "SORT-STRING" type. - // Also, in some cases, the field has some ';' in it. - // We remove them. - StringBuilder builder = new StringBuilder(); - String value = propertyNode.propValue; - int length = value.length(); - for (int i = 0; i < length; i++) { - char ch = value.charAt(i); - if (ch != ';') { - builder.append(ch); - } - } - contact.phoneticName = builder.toString(); - } else { - contact.addExtension(propertyNode); - } - } else if (name.equals("ADR")) { - List values = propertyNode.propValue_vector; - boolean valuesAreAllEmpty = true; - for (String value : values) { - if (value.length() > 0) { - valuesAreAllEmpty = false; - break; - } - } - if (valuesAreAllEmpty) { - continue; - } - - int kind = Contacts.KIND_POSTAL; - int type = -1; - String label = ""; - boolean isPrimary = false; - for (String typeString : propertyNode.paramMap_TYPE) { - if (typeString.equals("PREF") && !prefIsSetAddress) { - // Only first "PREF" is considered. - prefIsSetAddress = true; - isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; - label = ""; - } else if (typeString.equalsIgnoreCase("WORK") || - typeString.equalsIgnoreCase("COMPANY")) { - // "COMPANY" seems emitted by Windows Mobile, which is not - // specifically supported by vCard 2.1. We assume this is same - // as "WORK". - type = Contacts.ContactMethodsColumns.TYPE_WORK; - label = ""; - } else if (typeString.equalsIgnoreCase("POSTAL")) { - kind = Contacts.KIND_POSTAL; - } else if (typeString.equalsIgnoreCase("PARCEL") || - typeString.equalsIgnoreCase("DOM") || - typeString.equalsIgnoreCase("INTL")) { - // We do not have a kind or type matching these. - // TODO: fix this. We may need to split entries into two. - // (e.g. entries for KIND_POSTAL and KIND_PERCEL) - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0) { - // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters - // emit non-standard types. We do not handle their values now. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString; - } - } - // We use "HOME" as default - if (type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; - } - - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name - String address; - List list = propertyNode.propValue_vector; - int size = list.size(); - if (size > 1) { - StringBuilder builder = new StringBuilder(); - boolean builderIsEmpty = true; - if (Locale.getDefault().getCountry().equals(Locale.JAPAN.getCountry())) { - // In Japan, the order is reversed. - for (int i = size - 1; i >= 0; i--) { - String addressPart = list.get(i); - if (addressPart.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(addressPart); - builderIsEmpty = false; - } - } - } else { - for (int i = 0; i < size; i++) { - String addressPart = list.get(i); - if (addressPart.length() > 0) { - if (!builderIsEmpty) { - builder.append(' '); - } - builder.append(addressPart); - builderIsEmpty = false; - } - } - } - address = builder.toString().trim(); - } else { - address = propertyNode.propValue; - } - contact.addContactmethod(kind, type, address, label, isPrimary); - } else if (name.equals("ORG")) { - // vCard specification does not specify other types. - int type = Contacts.OrganizationColumns.TYPE_WORK; - boolean isPrimary = false; - - for (String typeString : propertyNode.paramMap_TYPE) { - if (typeString.equals("PREF") && !prefIsSetOrganization) { - // vCard specification officially does not have PREF in ORG. - // This is just for safety. - prefIsSetOrganization = true; - isPrimary = true; - } - // XXX: Should we cope with X- words? - } - - List list = propertyNode.propValue_vector; - int size = list.size(); - StringBuilder builder = new StringBuilder(); - for (Iterator iter = list.iterator(); iter.hasNext();) { - builder.append(iter.next()); - if (iter.hasNext()) { - builder.append(' '); - } - } - - contact.addOrganization(type, builder.toString(), "", isPrimary); - } else if (name.equals("TITLE")) { - contact.setPosition(propertyNode.propValue); - } else if (name.equals("ROLE")) { - contact.setPosition(propertyNode.propValue); - } else if (name.equals("PHOTO")) { - // We prefer PHOTO to LOGO. - String valueType = propertyNode.paramMap.getAsString("VALUE"); - if (valueType != null && valueType.equals("URL")) { - // TODO: do something. - } else { - // Assume PHOTO is stored in BASE64. In that case, - // data is already stored in propValue_bytes in binary form. - // It should be automatically done by VBuilder (VDataBuilder/VCardDatabuilder) - contact.photoBytes = propertyNode.propValue_bytes; - String type = propertyNode.paramMap.getAsString("TYPE"); - if (type != null) { - contact.photoType = type; - } - } - } else if (name.equals("LOGO")) { - // When PHOTO is not available this is not URL, - // we use this instead of PHOTO. - String valueType = propertyNode.paramMap.getAsString("VALUE"); - if (valueType != null && valueType.equals("URL")) { - // TODO: do something. - } else if (contact.photoBytes == null) { - contact.photoBytes = propertyNode.propValue_bytes; - String type = propertyNode.paramMap.getAsString("TYPE"); - if (type != null) { - contact.photoType = type; - } - } - } else if (name.equals("EMAIL")) { - int type = -1; - String label = null; - boolean isPrimary = false; - for (String typeString : propertyNode.paramMap_TYPE) { - if (typeString.equals("PREF") && !prefIsSetEmail) { - // Only first "PREF" is considered. - prefIsSetEmail = true; - isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.ContactMethodsColumns.TYPE_HOME; - } else if (typeString.equalsIgnoreCase("WORK")) { - type = Contacts.ContactMethodsColumns.TYPE_WORK; - } else if (typeString.equalsIgnoreCase("CELL")) { - // We do not have Contacts.ContactMethodsColumns.TYPE_MOBILE yet. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME; - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0) { - // vCard 3.0 allows iana-token. - // We may have INTERNET (specified in vCard spec), - // SCHOOL, etc. - type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; - label = typeString; - } - } - // We use "OTHER" as default. - if (type < 0) { - type = Contacts.ContactMethodsColumns.TYPE_OTHER; - } - contact.addContactmethod(Contacts.KIND_EMAIL, - type, propertyNode.propValue,label, isPrimary); - } else if (name.equals("TEL")) { - int type = -1; - String label = null; - boolean isPrimary = false; - boolean isFax = false; - for (String typeString : propertyNode.paramMap_TYPE) { - if (typeString.equals("PREF") && !prefIsSetPhone) { - // Only first "PREF" is considered. - prefIsSetPhone = true; - isPrimary = true; - } else if (typeString.equalsIgnoreCase("HOME")) { - type = Contacts.PhonesColumns.TYPE_HOME; - } else if (typeString.equalsIgnoreCase("WORK")) { - type = Contacts.PhonesColumns.TYPE_WORK; - } else if (typeString.equalsIgnoreCase("CELL")) { - type = Contacts.PhonesColumns.TYPE_MOBILE; - } else if (typeString.equalsIgnoreCase("PAGER")) { - type = Contacts.PhonesColumns.TYPE_PAGER; - } else if (typeString.equalsIgnoreCase("FAX")) { - isFax = true; - } else if (typeString.equalsIgnoreCase("VOICE") || - typeString.equalsIgnoreCase("MSG")) { - // Defined in vCard 3.0. Ignore these because they - // conflict with "HOME", "WORK", etc. - // XXX: do something? - } else if (typeString.toUpperCase().startsWith("X-") && - type < 0) { - type = Contacts.PhonesColumns.TYPE_CUSTOM; - label = typeString.substring(2); - } else if (type < 0){ - // We may have MODEM, CAR, ISDN, etc... - type = Contacts.PhonesColumns.TYPE_CUSTOM; - label = typeString; - } - } - // We use "HOME" as default - if (type < 0) { - type = Contacts.PhonesColumns.TYPE_HOME; - } - if (isFax) { - if (type == Contacts.PhonesColumns.TYPE_HOME) { - type = Contacts.PhonesColumns.TYPE_FAX_HOME; - } else if (type == Contacts.PhonesColumns.TYPE_WORK) { - type = Contacts.PhonesColumns.TYPE_FAX_WORK; - } - } - - contact.addPhone(type, propertyNode.propValue, label, isPrimary); - } else if (name.equals("NOTE")) { - contact.notes.add(propertyNode.propValue); - } else if (name.equals("BDAY")) { - contact.addExtension(propertyNode); - } else if (name.equals("URL")) { - contact.addExtension(propertyNode); - } else if (name.equals("REV")) { - // Revision of this VCard entry. I think we can ignore this. - contact.addExtension(propertyNode); - } else if (name.equals("UID")) { - contact.addExtension(propertyNode); - } else if (name.equals("KEY")) { - // Type is X509 or PGP? I don't know how to handle this... - contact.addExtension(propertyNode); - } else if (name.equals("MAILER")) { - contact.addExtension(propertyNode); - } else if (name.equals("TZ")) { - contact.addExtension(propertyNode); - } else if (name.equals("GEO")) { - contact.addExtension(propertyNode); - } else if (name.equals("NICKNAME")) { - // vCard 3.0 only. - contact.addExtension(propertyNode); - } else if (name.equals("CLASS")) { - // vCard 3.0 only. - // e.g. CLASS:CONFIDENTIAL - contact.addExtension(propertyNode); - } else if (name.equals("PROFILE")) { - // VCard 3.0 only. Must be "VCARD". I think we can ignore this. - contact.addExtension(propertyNode); - } else if (name.equals("CATEGORIES")) { - // VCard 3.0 only. - // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY - contact.addExtension(propertyNode); - } else if (name.equals("SOURCE")) { - // VCard 3.0 only. - contact.addExtension(propertyNode); - } else if (name.equals("PRODID")) { - // VCard 3.0 only. - // To specify the identifier for the product that created - // the vCard object. - contact.addExtension(propertyNode); - } else if (name.equals("X-PHONETIC-FIRST-NAME")) { - xPhoneticFirstName = propertyNode.propValue; - } else if (name.equals("X-PHONETIC-MIDDLE-NAME")) { - xPhoneticMiddleName = propertyNode.propValue; - } else if (name.equals("X-PHONETIC-LAST-NAME")) { - xPhoneticLastName = propertyNode.propValue; - } else { - // Unknown X- words and IANA token. - contact.addExtension(propertyNode); - } - } - - if (fullName != null) { - contact.name = fullName; - } else if(nameFromNProperty != null) { - contact.name = nameFromNProperty; - } else { - contact.name = ""; - } - - if (contact.phoneticName == null && - (xPhoneticFirstName != null || xPhoneticMiddleName != null || - xPhoneticLastName != null)) { - // Note: In Europe, this order should be "LAST FIRST MIDDLE". See the comment around - // NAME_ORDER_TYPE_* for more detail. - String first; - String second; - if (nameOrderType == NAME_ORDER_TYPE_JAPANESE) { - first = xPhoneticLastName; - second = xPhoneticFirstName; - } else { - first = xPhoneticFirstName; - second = xPhoneticLastName; - } - StringBuilder builder = new StringBuilder(); - if (first != null) { - builder.append(first); - } - if (xPhoneticMiddleName != null) { - builder.append(xPhoneticMiddleName); - } - if (second != null) { - builder.append(second); - } - contact.phoneticName = builder.toString(); - } - - // Remove unnecessary white spaces. - // It is found that some mobile phone emits phonetic name with just one white space - // when a user does not specify one. - // This logic is effective toward such kind of weird data. - if (contact.phoneticName != null) { - contact.phoneticName = contact.phoneticName.trim(); - } - - // If there is no "PREF", we choose the first entries as primary. - if (!prefIsSetPhone && - contact.phoneList != null && - contact.phoneList.size() > 0) { - contact.phoneList.get(0).isPrimary = true; - } - - if (!prefIsSetAddress && contact.contactmethodList != null) { - for (ContactMethod contactMethod : contact.contactmethodList) { - if (contactMethod.kind == Contacts.KIND_POSTAL) { - contactMethod.isPrimary = true; - break; - } - } - } - if (!prefIsSetEmail && contact.contactmethodList != null) { - for (ContactMethod contactMethod : contact.contactmethodList) { - if (contactMethod.kind == Contacts.KIND_EMAIL) { - contactMethod.isPrimary = true; - break; - } - } - } - if (!prefIsSetOrganization && - contact.organizationList != null && - contact.organizationList.size() > 0) { - contact.organizationList.get(0).isPrimary = true; - } - - return contact; - } - - public String displayString() { - if (name.length() > 0) { - return name; - } - if (contactmethodList != null && contactmethodList.size() > 0) { - for (ContactMethod contactMethod : contactmethodList) { - if (contactMethod.kind == Contacts.KIND_EMAIL && contactMethod.isPrimary) { - return contactMethod.data; - } - } - } - if (phoneList != null && phoneList.size() > 0) { - for (PhoneData phoneData : phoneList) { - if (phoneData.isPrimary) { - return phoneData.data; - } - } - } - return ""; - } - - private void pushIntoContentProviderOrResolver(Object contentSomething, - long myContactsGroupId) { - ContentResolver resolver = null; - AbstractSyncableContentProvider provider = null; - if (contentSomething instanceof ContentResolver) { - resolver = (ContentResolver)contentSomething; - } else if (contentSomething instanceof AbstractSyncableContentProvider) { - provider = (AbstractSyncableContentProvider)contentSomething; - } else { - Log.e(LOG_TAG, "Unsupported object came."); - return; - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(People.NAME, name); - contentValues.put(People.PHONETIC_NAME, phoneticName); - - if (notes.size() > 1) { - StringBuilder builder = new StringBuilder(); - for (String note : notes) { - builder.append(note); - builder.append("\n"); - } - contentValues.put(People.NOTES, builder.toString()); - } else if (notes.size() == 1){ - contentValues.put(People.NOTES, notes.get(0)); - } - - Uri personUri; - long personId = 0; - if (resolver != null) { - personUri = Contacts.People.createPersonInMyContactsGroup( - resolver, contentValues); - if (personUri != null) { - personId = ContentUris.parseId(personUri); - } - } else { - personUri = provider.nonTransactionalInsert(People.CONTENT_URI, contentValues); - if (personUri != null) { - personId = ContentUris.parseId(personUri); - ContentValues values = new ContentValues(); - values.put(GroupMembership.PERSON_ID, personId); - values.put(GroupMembership.GROUP_ID, myContactsGroupId); - Uri resultUri = provider.nonTransactionalInsert( - GroupMembership.CONTENT_URI, values); - if (resultUri == null) { - Log.e(LOG_TAG, "Faild to insert the person to MyContact."); - provider.nonTransactionalDelete(personUri, null, null); - personUri = null; - } - } - } - - if (personUri == null) { - Log.e(LOG_TAG, "Failed to create the contact."); - return; - } - - if (photoBytes != null) { - if (resolver != null) { - People.setPhotoData(resolver, personUri, photoBytes); - } else { - Uri photoUri = Uri.withAppendedPath(personUri, Contacts.Photos.CONTENT_DIRECTORY); - ContentValues values = new ContentValues(); - values.put(Photos.DATA, photoBytes); - provider.update(photoUri, values, null, null); - } - } - - long primaryPhoneId = -1; - if (phoneList != null && phoneList.size() > 0) { - for (PhoneData phoneData : phoneList) { - ContentValues values = new ContentValues(); - values.put(Contacts.PhonesColumns.TYPE, phoneData.type); - if (phoneData.type == Contacts.PhonesColumns.TYPE_CUSTOM) { - values.put(Contacts.PhonesColumns.LABEL, phoneData.label); - } - // Already formatted. - values.put(Contacts.PhonesColumns.NUMBER, phoneData.data); - - // Not sure about Contacts.PhonesColumns.NUMBER_KEY ... - values.put(Contacts.PhonesColumns.ISPRIMARY, 1); - values.put(Contacts.Phones.PERSON_ID, personId); - Uri phoneUri; - if (resolver != null) { - phoneUri = resolver.insert(Phones.CONTENT_URI, values); - } else { - phoneUri = provider.nonTransactionalInsert(Phones.CONTENT_URI, values); - } - if (phoneData.isPrimary) { - primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment()); - } - } - } - - long primaryOrganizationId = -1; - if (organizationList != null && organizationList.size() > 0) { - for (OrganizationData organizationData : organizationList) { - ContentValues values = new ContentValues(); - // Currently, we do not use TYPE_CUSTOM. - values.put(Contacts.OrganizationColumns.TYPE, - organizationData.type); - values.put(Contacts.OrganizationColumns.COMPANY, - organizationData.companyName); - values.put(Contacts.OrganizationColumns.TITLE, - organizationData.positionName); - values.put(Contacts.OrganizationColumns.ISPRIMARY, 1); - values.put(Contacts.OrganizationColumns.PERSON_ID, personId); - - Uri organizationUri; - if (resolver != null) { - organizationUri = resolver.insert(Organizations.CONTENT_URI, values); - } else { - organizationUri = provider.nonTransactionalInsert( - Organizations.CONTENT_URI, values); - } - if (organizationData.isPrimary) { - primaryOrganizationId = Long.parseLong(organizationUri.getLastPathSegment()); - } - } - } - - long primaryEmailId = -1; - if (contactmethodList != null && contactmethodList.size() > 0) { - for (ContactMethod contactMethod : contactmethodList) { - ContentValues values = new ContentValues(); - values.put(Contacts.ContactMethodsColumns.KIND, contactMethod.kind); - values.put(Contacts.ContactMethodsColumns.TYPE, contactMethod.type); - if (contactMethod.type == Contacts.ContactMethodsColumns.TYPE_CUSTOM) { - values.put(Contacts.ContactMethodsColumns.LABEL, contactMethod.label); - } - values.put(Contacts.ContactMethodsColumns.DATA, contactMethod.data); - values.put(Contacts.ContactMethodsColumns.ISPRIMARY, 1); - values.put(Contacts.ContactMethods.PERSON_ID, personId); - - if (contactMethod.kind == Contacts.KIND_EMAIL) { - Uri emailUri; - if (resolver != null) { - emailUri = resolver.insert(ContactMethods.CONTENT_URI, values); - } else { - emailUri = provider.nonTransactionalInsert( - ContactMethods.CONTENT_URI, values); - } - if (contactMethod.isPrimary) { - primaryEmailId = Long.parseLong(emailUri.getLastPathSegment()); - } - } else { // probably KIND_POSTAL - if (resolver != null) { - resolver.insert(ContactMethods.CONTENT_URI, values); - } else { - provider.nonTransactionalInsert( - ContactMethods.CONTENT_URI, values); - } - } - } - } - - if (extensionMap != null && extensionMap.size() > 0) { - ArrayList contentValuesArray; - if (resolver != null) { - contentValuesArray = new ArrayList(); - } else { - contentValuesArray = null; - } - for (Entry> entry : extensionMap.entrySet()) { - String key = entry.getKey(); - List list = entry.getValue(); - for (String value : list) { - ContentValues values = new ContentValues(); - values.put(Extensions.NAME, key); - values.put(Extensions.VALUE, value); - values.put(Extensions.PERSON_ID, personId); - if (resolver != null) { - contentValuesArray.add(values); - } else { - provider.nonTransactionalInsert(Extensions.CONTENT_URI, values); - } - } - } - if (resolver != null) { - resolver.bulkInsert(Extensions.CONTENT_URI, - contentValuesArray.toArray(new ContentValues[0])); - } - } - - if (primaryPhoneId >= 0 || primaryOrganizationId >= 0 || primaryEmailId >= 0) { - ContentValues values = new ContentValues(); - if (primaryPhoneId >= 0) { - values.put(People.PRIMARY_PHONE_ID, primaryPhoneId); - } - if (primaryOrganizationId >= 0) { - values.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId); - } - if (primaryEmailId >= 0) { - values.put(People.PRIMARY_EMAIL_ID, primaryEmailId); - } - if (resolver != null) { - resolver.update(personUri, values, null, null); - } else { - provider.nonTransactionalUpdate(personUri, values, null, null); - } - } - } - - /** - * Push this object into database in the resolver. - */ - public void pushIntoContentResolver(ContentResolver resolver) { - pushIntoContentProviderOrResolver(resolver, 0); - } - - /** - * Push this object into AbstractSyncableContentProvider object. - */ - public void pushIntoAbstractSyncableContentProvider( - AbstractSyncableContentProvider provider, long myContactsGroupId) { - boolean successful = false; - provider.beginTransaction(); - try { - pushIntoContentProviderOrResolver(provider, myContactsGroupId); - successful = true; - } finally { - provider.endTransaction(successful); - } - } - - public boolean isIgnorable() { - return TextUtils.isEmpty(name) && - TextUtils.isEmpty(phoneticName) && - (phoneList == null || phoneList.size() == 0) && - (contactmethodList == null || contactmethodList.size() == 0); - } -} diff --git a/core/java/android/syncml/pim/vcard/VCardComposer.java b/core/java/android/syncml/pim/vcard/VCardComposer.java deleted file mode 100644 index 192736ae59d9d14636928fc3c8e6d84c008c07ce..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcard/VCardComposer.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.syncml.pim.vcard; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import org.apache.commons.codec.binary.Base64; - -import android.provider.Contacts; -import android.syncml.pim.vcard.ContactStruct.PhoneData; - -/** - * Compose VCard string - */ -public class VCardComposer { - final public static int VERSION_VCARD21_INT = 1; - - final public static int VERSION_VCARD30_INT = 2; - - /** - * A new line - */ - private String mNewline; - - /** - * The composed string - */ - private StringBuilder mResult; - - /** - * The email's type - */ - static final private HashSet emailTypes = new HashSet( - Arrays.asList("CELL", "AOL", "APPLELINK", "ATTMAIL", "CIS", - "EWORLD", "INTERNET", "IBMMAIL", "MCIMAIL", "POWERSHARE", - "PRODIGY", "TLX", "X400")); - - static final private HashSet phoneTypes = new HashSet( - Arrays.asList("PREF", "WORK", "HOME", "VOICE", "FAX", "MSG", - "CELL", "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO")); - - static final private String TAG = "VCardComposer"; - - public VCardComposer() { - } - - private static final HashMap phoneTypeMap = new HashMap(); - - private static final HashMap emailTypeMap = new HashMap(); - - static { - phoneTypeMap.put(Contacts.Phones.TYPE_HOME, "HOME"); - phoneTypeMap.put(Contacts.Phones.TYPE_MOBILE, "CELL"); - phoneTypeMap.put(Contacts.Phones.TYPE_WORK, "WORK"); - // FAX_WORK not exist in vcard spec. The approximate is the combine of - // WORK and FAX, here only map to FAX - phoneTypeMap.put(Contacts.Phones.TYPE_FAX_WORK, "WORK;FAX"); - phoneTypeMap.put(Contacts.Phones.TYPE_FAX_HOME, "HOME;FAX"); - phoneTypeMap.put(Contacts.Phones.TYPE_PAGER, "PAGER"); - phoneTypeMap.put(Contacts.Phones.TYPE_OTHER, "X-OTHER"); - emailTypeMap.put(Contacts.ContactMethods.TYPE_HOME, "HOME"); - emailTypeMap.put(Contacts.ContactMethods.TYPE_WORK, "WORK"); - } - - /** - * Create a vCard String. - * - * @param struct - * see more from ContactStruct class - * @param vcardversion - * MUST be VERSION_VCARD21 /VERSION_VCARD30 - * @return vCard string - * @throws VCardException - * struct.name is null /vcardversion not match - */ - public String createVCard(ContactStruct struct, int vcardversion) - throws VCardException { - - mResult = new StringBuilder(); - // check exception: - if (struct.name == null || struct.name.trim().equals("")) { - throw new VCardException(" struct.name MUST have value."); - } - if (vcardversion == VERSION_VCARD21_INT) { - mNewline = "\r\n"; - } else if (vcardversion == VERSION_VCARD30_INT) { - mNewline = "\n"; - } else { - throw new VCardException( - " version not match VERSION_VCARD21 or VERSION_VCARD30."); - } - // build vcard: - mResult.append("BEGIN:VCARD").append(mNewline); - - if (vcardversion == VERSION_VCARD21_INT) { - mResult.append("VERSION:2.1").append(mNewline); - } else { - mResult.append("VERSION:3.0").append(mNewline); - } - - if (!isNull(struct.name)) { - appendNameStr(struct.name); - } - - if (!isNull(struct.company)) { - mResult.append("ORG:").append(struct.company).append(mNewline); - } - - if (struct.notes.size() > 0 && !isNull(struct.notes.get(0))) { - mResult.append("NOTE:").append( - foldingString(struct.notes.get(0), vcardversion)).append(mNewline); - } - - if (!isNull(struct.title)) { - mResult.append("TITLE:").append( - foldingString(struct.title, vcardversion)).append(mNewline); - } - - if (struct.photoBytes != null) { - appendPhotoStr(struct.photoBytes, struct.photoType, vcardversion); - } - - if (struct.phoneList != null) { - appendPhoneStr(struct.phoneList, vcardversion); - } - - if (struct.contactmethodList != null) { - appendContactMethodStr(struct.contactmethodList, vcardversion); - } - - mResult.append("END:VCARD").append(mNewline); - return mResult.toString(); - } - - /** - * Alter str to folding supported format. - * - * @param str - * the string to be folded - * @param version - * the vcard version - * @return the folded string - */ - private String foldingString(String str, int version) { - if (str.endsWith("\r\n")) { - str = str.substring(0, str.length() - 2); - } else if (str.endsWith("\n")) { - str = str.substring(0, str.length() - 1); - } else { - return null; - } - - str = str.replaceAll("\r\n", "\n"); - if (version == VERSION_VCARD21_INT) { - return str.replaceAll("\n", "\r\n "); - } else if (version == VERSION_VCARD30_INT) { - return str.replaceAll("\n", "\n "); - } else { - return null; - } - } - - /** - * Build LOGO property. format LOGO's param and encode value as base64. - * - * @param bytes - * the binary string to be converted - * @param type - * the type of the content - * @param version - * the version of vcard - */ - private void appendPhotoStr(byte[] bytes, String type, int version) - throws VCardException { - String value, encodingStr; - try { - value = foldingString(new String(Base64.encodeBase64(bytes, true)), - version); - } catch (Exception e) { - throw new VCardException(e.getMessage()); - } - - if (isNull(type) || type.toUpperCase().indexOf("JPEG") >= 0) { - type = "JPEG"; - } else if (type.toUpperCase().indexOf("GIF") >= 0) { - type = "GIF"; - } else if (type.toUpperCase().indexOf("BMP") >= 0) { - type = "BMP"; - } else { - // Handle the string like "image/tiff". - int indexOfSlash = type.indexOf("/"); - if (indexOfSlash >= 0) { - type = type.substring(indexOfSlash + 1).toUpperCase(); - } else { - type = type.toUpperCase(); - } - } - - mResult.append("LOGO;TYPE=").append(type); - if (version == VERSION_VCARD21_INT) { - encodingStr = ";ENCODING=BASE64:"; - value = value + mNewline; - } else if (version == VERSION_VCARD30_INT) { - encodingStr = ";ENCODING=b:"; - } else { - return; - } - mResult.append(encodingStr).append(value).append(mNewline); - } - - private boolean isNull(String str) { - if (str == null || str.trim().equals("")) { - return true; - } - return false; - } - - /** - * Build FN and N property. format N's value. - * - * @param name - * the name of the contact - */ - private void appendNameStr(String name) { - mResult.append("FN:").append(name).append(mNewline); - mResult.append("N:").append(name).append(mNewline); - /* - * if(name.indexOf(";") > 0) - * mResult.append("N:").append(name).append(mNewline); else - * if(name.indexOf(" ") > 0) mResult.append("N:").append(name.replace(' ', - * ';')). append(mNewline); else - * mResult.append("N:").append(name).append("; ").append(mNewline); - */ - } - - /** Loop append TEL property. */ - private void appendPhoneStr(List phoneList, - int version) { - HashMap numMap = new HashMap(); - String joinMark = version == VERSION_VCARD21_INT ? ";" : ","; - - for (ContactStruct.PhoneData phone : phoneList) { - String type; - if (!isNull(phone.data)) { - type = getPhoneTypeStr(phone); - if (version == VERSION_VCARD30_INT && type.indexOf(";") != -1) { - type = type.replace(";", ","); - } - if (numMap.containsKey(phone.data)) { - type = numMap.get(phone.data) + joinMark + type; - } - numMap.put(phone.data, type); - } - } - - for (Map.Entry num : numMap.entrySet()) { - if (version == VERSION_VCARD21_INT) { - mResult.append("TEL;"); - } else { // vcard3.0 - mResult.append("TEL;TYPE="); - } - mResult.append(num.getValue()).append(":").append(num.getKey()) - .append(mNewline); - } - } - - private String getPhoneTypeStr(PhoneData phone) { - - int phoneType = phone.type; - String typeStr, label; - - if (phoneTypeMap.containsKey(phoneType)) { - typeStr = phoneTypeMap.get(phoneType); - } else if (phoneType == Contacts.Phones.TYPE_CUSTOM) { - label = phone.label.toUpperCase(); - if (phoneTypes.contains(label) || label.startsWith("X-")) { - typeStr = label; - } else { - typeStr = "X-CUSTOM-" + label; - } - } else { - // TODO: need be updated with the provider's future changes - typeStr = "VOICE"; // the default type is VOICE in spec. - } - return typeStr; - } - - /** Loop append ADR / EMAIL property. */ - private void appendContactMethodStr( - List contactMList, int version) { - - HashMap emailMap = new HashMap(); - String joinMark = version == VERSION_VCARD21_INT ? ";" : ","; - for (ContactStruct.ContactMethod contactMethod : contactMList) { - // same with v2.1 and v3.0 - switch (contactMethod.kind) { - case Contacts.KIND_EMAIL: - String mailType = "INTERNET"; - if (!isNull(contactMethod.data)) { - int methodType = new Integer(contactMethod.type).intValue(); - if (emailTypeMap.containsKey(methodType)) { - mailType = emailTypeMap.get(methodType); - } else if (emailTypes.contains(contactMethod.label - .toUpperCase())) { - mailType = contactMethod.label.toUpperCase(); - } - if (emailMap.containsKey(contactMethod.data)) { - mailType = emailMap.get(contactMethod.data) + joinMark - + mailType; - } - emailMap.put(contactMethod.data, mailType); - } - break; - case Contacts.KIND_POSTAL: - if (!isNull(contactMethod.data)) { - mResult.append("ADR;TYPE=POSTAL:").append( - foldingString(contactMethod.data, version)).append( - mNewline); - } - break; - default: - break; - } - } - for (Map.Entry email : emailMap.entrySet()) { - if (version == VERSION_VCARD21_INT) { - mResult.append("EMAIL;"); - } else { - mResult.append("EMAIL;TYPE="); - } - mResult.append(email.getValue()).append(":").append(email.getKey()) - .append(mNewline); - } - } -} diff --git a/core/java/android/syncml/pim/vcard/VCardParser.java b/core/java/android/syncml/pim/vcard/VCardParser.java deleted file mode 100644 index 6dad852d1e56e6b692a0d91166423c77c1ce134f..0000000000000000000000000000000000000000 --- a/core/java/android/syncml/pim/vcard/VCardParser.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 android.syncml.pim.vcard; - -import android.syncml.pim.VDataBuilder; -import android.syncml.pim.VParser; -import android.util.Config; -import android.util.Log; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class VCardParser { - - // TODO: fix this. - VCardParser_V21 mParser = null; - - public final static String VERSION_VCARD21 = "vcard2.1"; - - public final static String VERSION_VCARD30 = "vcard3.0"; - - final public static int VERSION_VCARD21_INT = 1; - - final public static int VERSION_VCARD30_INT = 2; - - String mVersion = null; - - static final private String TAG = "VCardParser"; - - public VCardParser() { - } - - /** - * If version not given. Search from vcard string of the VERSION property. - * Then instance mParser to appropriate parser. - * - * @param vcardStr - * the content of vcard data - */ - private void judgeVersion(String vcardStr) { - if (mVersion == null) {// auto judge - int verIdx = vcardStr.indexOf("\nVERSION:"); - if (verIdx == -1) // if not have VERSION, v2.1 default - mVersion = VERSION_VCARD21; - else { - String verStr = vcardStr.substring(verIdx, vcardStr.indexOf( - "\n", verIdx + 1)); - if (verStr.indexOf("2.1") > 0) - mVersion = VERSION_VCARD21; - else if (verStr.indexOf("3.0") > 0) - mVersion = VERSION_VCARD30; - else - mVersion = VERSION_VCARD21; - } - } - if (mVersion.equals(VERSION_VCARD21)) - mParser = new VCardParser_V21(); - if (mVersion.equals(VERSION_VCARD30)) - mParser = new VCardParser_V30(); - } - - /** - * To make sure the vcard string has proper wrap character - * - * @param vcardStr - * the string to be checked - * @return string after verified - */ - private String verifyVCard(String vcardStr) { - this.judgeVersion(vcardStr); - // -- indent line: - vcardStr = vcardStr.replaceAll("\r\n", "\n"); - String[] strlist = vcardStr.split("\n"); - StringBuilder v21str = new StringBuilder(""); - for (int i = 0; i < strlist.length; i++) { - if (strlist[i].indexOf(":") < 0) { - if (strlist[i].length() == 0 && strlist[i + 1].indexOf(":") > 0) - v21str.append(strlist[i]).append("\r\n"); - else - v21str.append(" ").append(strlist[i]).append("\r\n"); - } else - v21str.append(strlist[i]).append("\r\n"); - } - return v21str.toString(); - } - - /** - * Set current version - * - * @param version - * the new version - */ - private void setVersion(String version) { - this.mVersion = version; - } - - /** - * Parse the given vcard string - * - * @param vcardStr - * to content to be parsed - * @param builder - * the data builder to hold data - * @return true if the string is successfully parsed, else return false - * @throws VCardException - * @throws IOException - */ - public boolean parse(String vcardStr, VDataBuilder builder) - throws VCardException, IOException { - - vcardStr = this.verifyVCard(vcardStr); - - boolean isSuccess = mParser.parse(new ByteArrayInputStream(vcardStr - .getBytes()), "US-ASCII", builder); - if (!isSuccess) { - if (mVersion.equals(VERSION_VCARD21)) { - if (Config.LOGD) - Log.d(TAG, "Parse failed for vCard 2.1 parser." - + " Try to use 3.0 parser."); - - this.setVersion(VERSION_VCARD30); - - return this.parse(vcardStr, builder); - } - throw new VCardException("parse failed.(even use 3.0 parser)"); - } - return true; - } -} diff --git a/core/java/android/test/AndroidTestCase.java b/core/java/android/test/AndroidTestCase.java index de0587ab7366d3c8284a95baf1ced06e641ba013..1015506ba227ac612c192d0d431324b838de24ac 100644 --- a/core/java/android/test/AndroidTestCase.java +++ b/core/java/android/test/AndroidTestCase.java @@ -30,6 +30,7 @@ import java.lang.reflect.Field; public class AndroidTestCase extends TestCase { protected Context mContext; + private Context mTestContext; @Override protected void setUp() throws Exception { @@ -43,7 +44,7 @@ public class AndroidTestCase extends TestCase { public void testAndroidTestCaseSetupProperly() { assertNotNull("Context is null. setContext should be called before tests are run", - mContext); + mContext); } public void setContext(Context context) { @@ -54,6 +55,25 @@ public class AndroidTestCase extends TestCase { return mContext; } + /** + * Test context can be used to access resources from the test's own package + * as opposed to the resources from the test target package. Access to the + * latter is provided by the context set with the {@link #setContext} + * method. + * + * @hide + */ + public void setTestContext(Context context) { + mTestContext = context; + } + + /** + * @hide + */ + public Context getTestContext() { + return mTestContext; + } + /** * Asserts that launching a given activity is protected by a particular permission by * attempting to start the activity and validating that a {@link SecurityException} @@ -125,9 +145,9 @@ public class AndroidTestCase extends TestCase { * to scrub out any class variables. This protects against memory leaks in the case where a * test case creates a non-static inner class (thus referencing the test case) and gives it to * someone else to hold onto. - * + * * @param testCaseClass The class of the derived TestCase implementation. - * + * * @throws IllegalAccessException */ protected void scrubClass(final Class testCaseClass) diff --git a/core/java/android/test/InstrumentationTestCase.java b/core/java/android/test/InstrumentationTestCase.java index 2145d7cc7891c3f1f19060af7ee1278791c6f0dc..22d95d16b2e7b364d157e6d61103dccfa05f7fa9 100644 --- a/core/java/android/test/InstrumentationTestCase.java +++ b/core/java/android/test/InstrumentationTestCase.java @@ -43,10 +43,24 @@ public class InstrumentationTestCase extends TestCase { * * @param instrumentation the instrumentation to use with this instance */ - public void injectInsrumentation(Instrumentation instrumentation) { + public void injectInstrumentation(Instrumentation instrumentation) { mInstrumentation = instrumentation; } + /** + * Injects instrumentation into this test case. This method is + * called by the test runner during test setup. + * + * @param instrumentation the instrumentation to use with this instance + * + * @deprecated Incorrect spelling, + * use {@link #injectInstrumentation(android.app.Instrumentation) instead. + */ + @Deprecated + public void injectInsrumentation(Instrumentation instrumentation) { + injectInstrumentation(instrumentation); + } + /** * Inheritors can access the instrumentation using this. * @return instrumentation diff --git a/core/java/android/test/InstrumentationTestSuite.java b/core/java/android/test/InstrumentationTestSuite.java index 2ab949e02f2ff2f5c39bd433e2ebea1fa7326f3e..7a78ffbb226d4f8a40817a236a39b65d0d7d7284 100644 --- a/core/java/android/test/InstrumentationTestSuite.java +++ b/core/java/android/test/InstrumentationTestSuite.java @@ -65,7 +65,7 @@ public class InstrumentationTestSuite extends TestSuite { public void runTest(Test test, TestResult result) { if (test instanceof InstrumentationTestCase) { - ((InstrumentationTestCase) test).injectInsrumentation(mInstrumentation); + ((InstrumentationTestCase) test).injectInstrumentation(mInstrumentation); } // run the test as usual diff --git a/core/java/android/test/TimedTest.java b/core/java/android/test/TimedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3a60a256823b701bbaea8ee8cf1574cd8e747238 --- /dev/null +++ b/core/java/android/test/TimedTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation can be used on an {@link junit.framework.TestCase}'s test + * methods. When the annotation is present, the test method is timed and the + * results written through instrumentation output. It can also be used on the + * class itself, which is equivalent to tagging all test methods with this + * annotation. + * + * {@hide} Pending approval for public API. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface TimedTest { } \ No newline at end of file diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 843754b7713c08dd227cabc21b12f0b9cb84b040..944f7354fdb2cd326976fc6ae7d14c6b961095cd 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -19,6 +19,7 @@ package android.text; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.text.style.ParagraphStyle; import android.util.FloatMath; /** @@ -262,6 +263,14 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback TextUtils.recycle(temp); + if (boring && text instanceof Spanned) { + Spanned sp = (Spanned) text; + Object[] styles = sp.getSpans(0, text.length(), ParagraphStyle.class); + if (styles.length > 0) { + boring = false; + } + } + if (boring) { Metrics fm = metrics; if (fm == null) { diff --git a/core/java/android/text/InputType.java b/core/java/android/text/InputType.java index d50684af911d54ff53c7825fd3565c7c4222f7a0..14b8308cbc577d7643d63be8c3deb158245bccb8 100644 --- a/core/java/android/text/InputType.java +++ b/core/java/android/text/InputType.java @@ -128,6 +128,15 @@ public interface InputType { */ public static final int TYPE_TEXT_FLAG_IME_MULTI_LINE = 0x00040000; + /** + * Flag for {@link #TYPE_CLASS_TEXT}: the input method does not need to + * display any dictionary-based candidates. This is useful for text views that + * do not contain words from the language and do not benefit from any + * dictionary-based completions or corrections. It overrides the + * {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} value when set. + */ + public static final int TYPE_TEXT_FLAG_NO_SUGGESTIONS = 0x00080000; + // ---------------------------------------------------------------------- /** diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index c133cf2b30a00e41ea663ee9773c26cee2b5df3b..f0a5ffd67127dbe091747fb44301708f6bf3c0f8 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -968,7 +968,13 @@ extends Layout fm.bottom = bottom; for (int i = 0; i < chooseht.length; i++) { - chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm); + if (chooseht[i] instanceof LineHeightSpan.WithDensity) { + ((LineHeightSpan.WithDensity) chooseht[i]). + chooseHeight(text, start, end, choosehtv[i], v, fm, paint); + + } else { + chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm); + } } above = fm.ascent; diff --git a/core/java/android/text/TextPaint.java b/core/java/android/text/TextPaint.java index f13820d3a88505ffff63515d51a9dc5c03527e0b..f9e7cac6c50505ca8d9698564f6f430243dda40b 100644 --- a/core/java/android/text/TextPaint.java +++ b/core/java/android/text/TextPaint.java @@ -27,6 +27,7 @@ public class TextPaint extends Paint { public int baselineShift; public int linkColor; public int[] drawableState; + public float density = 1.0f; public TextPaint() { super(); @@ -51,5 +52,6 @@ public class TextPaint extends Paint { baselineShift = tp.baselineShift; linkColor = tp.linkColor; drawableState = tp.drawableState; + density = tp.density; } } diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 1a4eb699b56d76be42025260411a2b7f90cfd075..9dd8ceba39d56828d8ab8def8e8b03c934802e1c 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -25,7 +25,9 @@ import android.pim.DateException; import java.util.Calendar; import java.util.Date; +import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.Locale; import java.util.TimeZone; /** @@ -1038,6 +1040,31 @@ public class DateUtils lval.setTimeInMillis(rval.getTimeInMillis()); } + /** + * Formats a date or a time range according to the local conventions. + *

      + * Note that this is a convenience method. Using it involves creating an + * internal {@link java.util.Formatter} instance on-the-fly, which is + * somewhat costly in terms of memory and time. This is probably acceptable + * if you use the method only rarely, but if you rely on it for formatting a + * large number of dates, consider creating and reusing your own + * {@link java.util.Formatter} instance and use the version of + * {@link #formatDateRange(Context, long, long, int) formatDateRange} + * that takes a {@link java.util.Formatter}. + * + * @param context the context is required only if the time is shown + * @param startMillis the start time in UTC milliseconds + * @param endMillis the end time in UTC milliseconds + * @param flags a bit mask of options See + * {@link #formatDateRange(Context, long, long, int) formatDateRange} + * @return a string containing the formatted date/time range. + */ + public static String formatDateRange(Context context, long startMillis, + long endMillis, int flags) { + Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); + return formatDateRange(context, f, startMillis, endMillis, flags).toString(); + } + /** * Formats a date or a time range according to the local conventions. * @@ -1181,14 +1208,17 @@ public class DateUtils * instead of "December 31, 2008". * * @param context the context is required only if the time is shown + * @param formatter the Formatter used for formatting the date range. + * Note: be sure to call setLength(0) on StringBuilder passed to + * the Formatter constructor unless you want the results to accumulate. * @param startMillis the start time in UTC milliseconds * @param endMillis the end time in UTC milliseconds * @param flags a bit mask of options * - * @return a string containing the formatted date/time range. + * @return the formatter with the formatted date/time range appended to the string buffer. */ - public static String formatDateRange(Context context, long startMillis, - long endMillis, int flags) { + public static Formatter formatDateRange(Context context, Formatter formatter, long startMillis, + long endMillis, int flags) { Resources res = Resources.getSystem(); boolean showTime = (flags & FORMAT_SHOW_TIME) != 0; boolean showWeekDay = (flags & FORMAT_SHOW_WEEKDAY) != 0; @@ -1423,8 +1453,7 @@ public class DateUtils if (noMonthDay && startMonthNum == endMonthNum) { // Example: "January, 2008" - String startDateString = startDate.format(defaultDateFormat); - return startDateString; + return formatter.format("%s", startDate.format(defaultDateFormat)); } if (startYear != endYear || noMonthDay) { @@ -1436,10 +1465,9 @@ public class DateUtils // The values that are used in a fullFormat string are specified // by position. - dateRange = String.format(fullFormat, + return formatter.format(fullFormat, startWeekDayString, startDateString, startTimeString, endWeekDayString, endDateString, endTimeString); - return dateRange; } // Get the month, day, and year strings for the start and end dates @@ -1476,12 +1504,11 @@ public class DateUtils // The values that are used in a fullFormat string are specified // by position. - dateRange = String.format(fullFormat, + return formatter.format(fullFormat, startWeekDayString, startMonthString, startMonthDayString, startYearString, startTimeString, endWeekDayString, endMonthString, endMonthDayString, endYearString, endTimeString); - return dateRange; } if (startDay != endDay) { @@ -1496,12 +1523,11 @@ public class DateUtils // The values that are used in a fullFormat string are specified // by position. - dateRange = String.format(fullFormat, + return formatter.format(fullFormat, startWeekDayString, startMonthString, startMonthDayString, startYearString, startTimeString, endWeekDayString, endMonthString, endMonthDayString, endYearString, endTimeString); - return dateRange; } // Same start and end day @@ -1522,6 +1548,7 @@ public class DateUtils } else { // Example: "10:00 - 11:00 am" String timeFormat = res.getString(com.android.internal.R.string.time1_time2); + // Don't use the user supplied Formatter because the result will pollute the buffer. timeString = String.format(timeFormat, startTimeString, endTimeString); } } @@ -1545,7 +1572,7 @@ public class DateUtils fullFormat = res.getString(com.android.internal.R.string.time_date); } else { // Example: "Oct 9" - return dateString; + return formatter.format("%s", dateString); } } } else if (showWeekDay) { @@ -1554,16 +1581,15 @@ public class DateUtils fullFormat = res.getString(com.android.internal.R.string.time_wday); } else { // Example: "Tue" - return startWeekDayString; + return formatter.format("%s", startWeekDayString); } } else if (showTime) { - return timeString; + return formatter.format("%s", timeString); } // The values that are used in a fullFormat string are specified // by position. - dateRange = String.format(fullFormat, timeString, startWeekDayString, dateString); - return dateRange; + return formatter.format(fullFormat, timeString, startWeekDayString, dateString); } /** diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index 367b26ce45d3947d1a7122d42eddde92068ff91e..baaa3ceae2a34505cae8afd82fe7253acda3e8d0 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -32,6 +32,18 @@ public final class Formatter { * @return formated string with the number */ public static String formatFileSize(Context context, long number) { + return formatFileSize(context, number, false); + } + + /** + * Like {@link #formatFileSize}, but trying to generate shorter numbers + * (showing fewer digits of precisin). + */ + public static String formatShortFileSize(Context context, long number) { + return formatFileSize(context, number, true); + } + + private static String formatFileSize(Context context, long number, boolean shorter) { if (context == null) { return ""; } @@ -58,13 +70,24 @@ public final class Formatter { suffix = com.android.internal.R.string.petabyteShort; result = result / 1024; } - if (result < 100) { - String value = String.format("%.2f", result); - return context.getResources(). - getString(com.android.internal.R.string.fileSizeSuffix, - value, context.getString(suffix)); + String value; + if (result < 1) { + value = String.format("%.2f", result); + } else if (result < 10) { + if (shorter) { + value = String.format("%.1f", result); + } else { + value = String.format("%.2f", result); + } + } else if (result < 100) { + if (shorter) { + value = String.format("%.0f", result); + } else { + value = String.format("%.2f", result); + } + } else { + value = String.format("%.0f", result); } - String value = String.format("%.0f", result); return context.getResources(). getString(com.android.internal.R.string.fileSizeSuffix, value, context.getString(suffix)); diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 92f628967d713f2fd637d2c970247d297eb9d7c8..ab33cb37c1ebf1d06d170eba3190eadb5d6726e6 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -22,6 +22,7 @@ import android.graphics.Rect; import android.text.*; import android.widget.TextView; import android.view.View; +import android.view.ViewConfiguration; import android.view.MotionEvent; // XXX this doesn't extend MetaKeyKeyListener because the signatures @@ -256,8 +257,32 @@ implements MovementMethod (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0); + DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), + DoubleTapState.class); + boolean doubletap = false; + + if (tap.length > 0) { + if (event.getEventTime() - tap[0].mWhen <= + ViewConfiguration.getDoubleTapTimeout()) { + if (sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { + doubletap = true; + } + } + + tap[0].mWhen = event.getEventTime(); + } else { + DoubleTapState newtap = new DoubleTapState(); + newtap.mWhen = event.getEventTime(); + buffer.setSpan(newtap, 0, buffer.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + if (cap) { Selection.extendSelection(buffer, off); + } else if (doubletap) { + Selection.setSelection(buffer, + findWordStart(buffer, off), + findWordEnd(buffer, off)); } else { Selection.setSelection(buffer, off); } @@ -272,6 +297,62 @@ implements MovementMethod return handled; } + private static class DoubleTapState implements NoCopySpan { + long mWhen; + } + + private static boolean sameWord(CharSequence text, int one, int two) { + int start = findWordStart(text, one); + int end = findWordEnd(text, one); + + if (end == start) { + return false; + } + + return start == findWordStart(text, two) && + end == findWordEnd(text, two); + } + + // TODO: Unify with TextView.getWordForDictionary() + private static int findWordStart(CharSequence text, int start) { + for (; start > 0; start--) { + char c = text.charAt(start - 1); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } + } + + return start; + } + + // TODO: Unify with TextView.getWordForDictionary() + private static int findWordEnd(CharSequence text, int end) { + int len = text.length(); + + for (; end < len; end++) { + char c = text.charAt(end); + int type = Character.getType(c); + + if (c != '\'' && + type != Character.UPPERCASE_LETTER && + type != Character.LOWERCASE_LETTER && + type != Character.TITLECASE_LETTER && + type != Character.MODIFIER_LETTER && + type != Character.DECIMAL_DIGIT_NUMBER) { + break; + } + } + + return end; + } + public boolean canSelectArbitrarily() { return true; } diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java index 3c4067511804970c62fa97f255e2d7f1941e31a3..880e46daf1aa888781c1d7bccbb14aef71dd667d 100644 --- a/core/java/android/text/method/CharacterPickerDialog.java +++ b/core/java/android/text/method/CharacterPickerDialog.java @@ -25,15 +25,14 @@ import android.text.*; import android.view.LayoutInflater; import android.view.View.OnClickListener; import android.view.View; -import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup; +import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.GridView; -import android.widget.TextView; /** * Dialog for choosing accented characters related to a base character. @@ -45,6 +44,7 @@ public class CharacterPickerDialog extends Dialog private String mOptions; private boolean mInsert; private LayoutInflater mInflater; + private Button mCancelButton; /** * Creates a new CharacterPickerDialog that presents the specified @@ -54,7 +54,7 @@ public class CharacterPickerDialog extends Dialog public CharacterPickerDialog(Context context, View view, Editable text, String options, boolean insert) { - super(context); + super(context, com.android.internal.R.style.Theme_Panel); mView = view; mText = text; @@ -70,28 +70,32 @@ public class CharacterPickerDialog extends Dialog WindowManager.LayoutParams params = getWindow().getAttributes(); params.token = mView.getApplicationWindowToken(); params.type = params.TYPE_APPLICATION_ATTACHED_DIALOG; + params.flags = params.flags | Window.FEATURE_NO_TITLE; - setTitle(R.string.select_character); setContentView(R.layout.character_picker); GridView grid = (GridView) findViewById(R.id.characterPicker); grid.setAdapter(new OptionsAdapter(getContext())); grid.setOnItemClickListener(this); - findViewById(R.id.cancel).setOnClickListener(this); + mCancelButton = (Button) findViewById(R.id.cancel); + mCancelButton.setOnClickListener(this); } /** * Handles clicks on the character buttons. */ public void onItemClick(AdapterView parent, View view, int position, long id) { - int selEnd = Selection.getSelectionEnd(mText); String result = String.valueOf(mOptions.charAt(position)); + replaceCharacterAndClose(result); + } + private void replaceCharacterAndClose(CharSequence replace) { + int selEnd = Selection.getSelectionEnd(mText); if (mInsert || selEnd == 0) { - mText.insert(selEnd, result); + mText.insert(selEnd, replace); } else { - mText.replace(selEnd - 1, selEnd, result); + mText.replace(selEnd - 1, selEnd, replace); } dismiss(); @@ -101,21 +105,25 @@ public class CharacterPickerDialog extends Dialog * Handles clicks on the Cancel button. */ public void onClick(View v) { - dismiss(); + if (v == mCancelButton) { + dismiss(); + } else if (v instanceof Button) { + CharSequence result = ((Button) v).getText(); + replaceCharacterAndClose(result); + } } private class OptionsAdapter extends BaseAdapter { - private Context mContext; public OptionsAdapter(Context context) { super(); - mContext = context; } public View getView(int position, View convertView, ViewGroup parent) { Button b = (Button) mInflater.inflate(R.layout.character_picker_button, null); b.setText(String.valueOf(mOptions.charAt(position))); + b.setOnClickListener(CharacterPickerDialog.this); return b; } diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java index e420c27516e304f0c702d103a26fc072ffa167eb..2e764707b43a65edf0243b8f7db58b00bf7c67c7 100644 --- a/core/java/android/text/method/QwertyKeyListener.java +++ b/core/java/android/text/method/QwertyKeyListener.java @@ -401,20 +401,17 @@ public class QwertyKeyListener extends BaseKeyListener { private static SparseArray PICKER_SETS = new SparseArray(); static { - PICKER_SETS.put('!', "\u00A1"); - PICKER_SETS.put('<', "\u00AB"); - PICKER_SETS.put('>', "\u00BB"); - PICKER_SETS.put('?', "\u00BF"); PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100"); PICKER_SETS.put('C', "\u00C7\u0106\u010C"); PICKER_SETS.put('D', "\u010E"); PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112"); + PICKER_SETS.put('G', "\u011E"); PICKER_SETS.put('L', "\u0141"); - PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A"); + PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130"); PICKER_SETS.put('N', "\u00D1\u0143\u0147"); PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C"); PICKER_SETS.put('R', "\u0158"); - PICKER_SETS.put('S', "\u015A\u0160"); + PICKER_SETS.put('S', "\u015A\u0160\u015E"); PICKER_SETS.put('T', "\u0164"); PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A"); PICKER_SETS.put('Y', "\u00DD\u0178"); @@ -423,18 +420,47 @@ public class QwertyKeyListener extends BaseKeyListener { PICKER_SETS.put('c', "\u00E7\u0107\u010D"); PICKER_SETS.put('d', "\u010F"); PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113"); - PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B"); + PICKER_SETS.put('g', "\u011F"); + PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131"); PICKER_SETS.put('l', "\u0142"); PICKER_SETS.put('n', "\u00F1\u0144\u0148"); PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D"); PICKER_SETS.put('r', "\u0159"); - PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161"); + PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F"); PICKER_SETS.put('t', "\u0165"); PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B"); PICKER_SETS.put('y', "\u00FD\u00FF"); PICKER_SETS.put('z', "\u017A\u017C\u017E"); PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, - "\u2026\u00A5\u2022\u00AE\u00A9\u00B1"); + "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\"); + PICKER_SETS.put('/', "\\"); + + // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml + + PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b"); + PICKER_SETS.put('2', "\u00b2\u2154"); + PICKER_SETS.put('3', "\u00b3\u00be\u215c"); + PICKER_SETS.put('4', "\u2074"); + PICKER_SETS.put('5', "\u215d"); + PICKER_SETS.put('7', "\u215e"); + PICKER_SETS.put('0', "\u207f\u2205"); + PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1"); + PICKER_SETS.put('%', "\u2030"); + PICKER_SETS.put('*', "\u2020\u2021"); + PICKER_SETS.put('-', "\u2013\u2014"); + PICKER_SETS.put('+', "\u00b1"); + PICKER_SETS.put('(', "[{<"); + PICKER_SETS.put(')', "]}>"); + PICKER_SETS.put('!', "\u00a1"); + PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd"); + PICKER_SETS.put('?', "\u00bf"); + PICKER_SETS.put(',', "\u201a\u201e"); + + // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml + + PICKER_SETS.put('=', "\u2260\u2248\u221e"); + PICKER_SETS.put('<', "\u2264\u00ab\u2039"); + PICKER_SETS.put('>', "\u2265\u00bb\u203a"); }; private boolean showCharacterPicker(View view, Editable content, char c, diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index dfc16f5adf7fbfdb5ba4753156df71f4a2b8b02c..6995107a96287d3811b52723f213a94003fff2b5 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -71,6 +71,24 @@ public class Touch { widget.scrollTo(x, y); } + /** + * @hide + * Returns the maximum scroll value in x. + */ + public static int getMaxScrollX(TextView widget, Layout layout, int y) { + int top = layout.getLineForVertical(y); + int bottom = layout.getLineForVertical(y + widget.getHeight() + - widget.getTotalPaddingTop() -widget.getTotalPaddingBottom()); + int left = Integer.MAX_VALUE; + int right = 0; + for (int i = top; i <= bottom; i++) { + left = (int) Math.min(left, layout.getLineLeft(i)); + right = (int) Math.max(right, layout.getLineRight(i)); + } + return right - left - widget.getWidth() - widget.getTotalPaddingLeft() + - widget.getTotalPaddingRight(); + } + /** * Handles touch events for dragging. You may want to do other actions * like moving the cursor on touch as well. diff --git a/core/java/android/text/style/AbsoluteSizeSpan.java b/core/java/android/text/style/AbsoluteSizeSpan.java index 484f8ce410a2923752e5d8c50fb1ebc90917ab81..1214040a0a9fbfe5864e48fdbec304dfbf6b02c3 100644 --- a/core/java/android/text/style/AbsoluteSizeSpan.java +++ b/core/java/android/text/style/AbsoluteSizeSpan.java @@ -24,13 +24,28 @@ import android.text.TextUtils; public class AbsoluteSizeSpan extends MetricAffectingSpan implements ParcelableSpan { private final int mSize; + private boolean mDip; + /** + * Set the text size to size physical pixels. + */ public AbsoluteSizeSpan(int size) { mSize = size; } + /** + * Set the text size to size physical pixels, + * or to size device-independent pixels if + * dip is true. + */ + public AbsoluteSizeSpan(int size, boolean dip) { + mSize = size; + mDip = dip; + } + public AbsoluteSizeSpan(Parcel src) { mSize = src.readInt(); + mDip = src.readInt() != 0; } public int getSpanTypeId() { @@ -43,19 +58,32 @@ public class AbsoluteSizeSpan extends MetricAffectingSpan implements ParcelableS public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSize); + dest.writeInt(mDip ? 1 : 0); } public int getSize() { return mSize; } + public boolean getDip() { + return mDip; + } + @Override public void updateDrawState(TextPaint ds) { - ds.setTextSize(mSize); + if (mDip) { + ds.setTextSize(mSize * ds.density); + } else { + ds.setTextSize(mSize); + } } @Override public void updateMeasureState(TextPaint ds) { - ds.setTextSize(mSize); + if (mDip) { + ds.setTextSize(mSize * ds.density); + } else { + ds.setTextSize(mSize); + } } } diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java index 86ef5f68bc7c0da107d0592a0bfc1e4958cddaf7..74b9463c38890fd0108ef80d6c8b5d5f9dbfebed 100644 --- a/core/java/android/text/style/ImageSpan.java +++ b/core/java/android/text/style/ImageSpan.java @@ -36,6 +36,7 @@ public class ImageSpan extends DynamicDrawableSpan { /** * @deprecated Use {@link #ImageSpan(Context, Bitmap)} instead. */ + @Deprecated public ImageSpan(Bitmap b) { this(null, b, ALIGN_BOTTOM); } @@ -43,6 +44,7 @@ public class ImageSpan extends DynamicDrawableSpan { /** * @deprecated Use {@link #ImageSpan(Context, Bitmap, int) instead. */ + @Deprecated public ImageSpan(Bitmap b, int verticalAlignment) { this(null, b, verticalAlignment); } diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java index c0ef97c483e9efa20eb0bab3efb8b3b3e8a522cf..44a170623bcddb711c4018b16ece3c3cf5c45fcf 100644 --- a/core/java/android/text/style/LineHeightSpan.java +++ b/core/java/android/text/style/LineHeightSpan.java @@ -19,6 +19,7 @@ package android.text.style; import android.graphics.Paint; import android.graphics.Canvas; import android.text.Layout; +import android.text.TextPaint; public interface LineHeightSpan extends ParagraphStyle, WrapTogetherSpan @@ -26,4 +27,10 @@ extends ParagraphStyle, WrapTogetherSpan public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm); + + public interface WithDensity extends LineHeightSpan { + public void chooseHeight(CharSequence text, int start, int end, + int spanstartv, int v, + Paint.FontMetricsInt fm, TextPaint paint); + } } diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index d61e888e18158fa0242912925162f8ea03ae0ce5..ce25c47cfec22829ce325a46201fd09dc6ee5c4a 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -208,7 +208,7 @@ public class Linkify { if ((mask & WEB_URLS) != 0) { gatherLinks(links, text, Regex.WEB_URL_PATTERN, - new String[] { "http://", "https://" }, + new String[] { "http://", "https://", "rtsp://" }, sUrlMatchFilter, null); } diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java index a349b82dbad672f5320e4aa14d61745877f93a77..a6844a464ffcf7b31c1835f31c72af38d6833f9a 100644 --- a/core/java/android/text/util/Regex.java +++ b/core/java/android/text/util/Regex.java @@ -65,7 +65,7 @@ public class Regex { */ public static final Pattern WEB_URL_PATTERN = Pattern.compile( - "((?:(http|https|Http|Https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}\\.)+" // named host diff --git a/core/java/android/text/util/Rfc822InputFilter.java b/core/java/android/text/util/Rfc822InputFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..8c8b7fc2aba521b311cd5e8daeedaa3d31fc35a9 --- /dev/null +++ b/core/java/android/text/util/Rfc822InputFilter.java @@ -0,0 +1,58 @@ +package android.text.util; + +import android.text.InputFilter; +import android.text.Spanned; +import android.text.SpannableStringBuilder; + +/** + * Implements special address cleanup rules: + * The first space key entry following an "@" symbol that is followed by any combination + * of letters and symbols, including one+ dots and zero commas, should insert an extra + * comma (followed by the space). + * + * @hide + */ +public class Rfc822InputFilter implements InputFilter { + + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, + int dstart, int dend) { + + // quick check - did they enter a single space? + if (end-start != 1 || source.charAt(start) != ' ') { + return null; + } + + // determine if the characters before the new space fit the pattern + // follow backwards and see if we find a comma, dot, or @ + int scanBack = dstart; + boolean dotFound = false; + while (scanBack > 0) { + char c = dest.charAt(--scanBack); + switch (c) { + case '.': + dotFound = true; // one or more dots are req'd + break; + case ',': + return null; + case '@': + if (!dotFound) { + return null; + } + // we have found a comma-insert case. now just do it + // in the least expensive way we can. + if (source instanceof Spanned) { + SpannableStringBuilder sb = new SpannableStringBuilder(","); + sb.append(source); + return sb; + } else { + return ", "; + } + default: + // just keep going + } + } + + // no termination cases were found, so don't edit the input + return null; + } +} diff --git a/core/java/android/text/util/Rfc822Token.java b/core/java/android/text/util/Rfc822Token.java index 7fe11bc430e6168e5c3a0a79bc162625cf0e0e06..0edeeb5397769b44af7a80c6e8916963295cb76d 100644 --- a/core/java/android/text/util/Rfc822Token.java +++ b/core/java/android/text/util/Rfc822Token.java @@ -168,5 +168,31 @@ public class Rfc822Token { return sb.toString(); } + + public int hashCode() { + int result = 17; + if (mName != null) result = 31 * result + mName.hashCode(); + if (mAddress != null) result = 31 * result + mAddress.hashCode(); + if (mComment != null) result = 31 * result + mComment.hashCode(); + return result; + } + + private static boolean stringEquals(String a, String b) { + if (a == null) { + return (b == null); + } else { + return (a.equals(b)); + } + } + + public boolean equals(Object o) { + if (!(o instanceof Rfc822Token)) { + return false; + } + Rfc822Token other = (Rfc822Token) o; + return (stringEquals(mName, other.mName) && + stringEquals(mAddress, other.mAddress) && + stringEquals(mComment, other.mComment)); + } } diff --git a/core/java/android/text/util/Rfc822Tokenizer.java b/core/java/android/text/util/Rfc822Tokenizer.java index d4e78b0dd2801f0e73a57ea4bf1a56a5af517e0c..cb39f7de0c1e9ed294edf7dfaf289953ce3c3c5a 100644 --- a/core/java/android/text/util/Rfc822Tokenizer.java +++ b/core/java/android/text/util/Rfc822Tokenizer.java @@ -19,6 +19,7 @@ package android.text.util; import android.widget.MultiAutoCompleteTextView; import java.util.ArrayList; +import java.util.Collection; /** * This class works as a Tokenizer for MultiAutoCompleteTextView for @@ -27,18 +28,22 @@ import java.util.ArrayList; * into a series of Rfc822Tokens. */ public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { + /** * This constructor will try to take a string like * "Foo Bar (something) <foo\@google.com>, * blah\@google.com (something)" - * and convert it into one or more Rfc822Tokens. + * and convert it into one or more Rfc822Tokens, output into the supplied + * collection. + * * It does *not* decode MIME encoded-words; charset conversion * must already have taken place if necessary. * It will try to be tolerant of broken syntax instead of * returning an error. + * + * @hide */ - public static Rfc822Token[] tokenize(CharSequence text) { - ArrayList out = new ArrayList(); + public static void tokenize(CharSequence text, Collection out) { StringBuilder name = new StringBuilder(); StringBuilder address = new StringBuilder(); StringBuilder comment = new StringBuilder(); @@ -148,7 +153,21 @@ public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { name.toString(), comment.toString())); } + } + /** + * This method will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens. + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + */ + public static Rfc822Token[] tokenize(CharSequence text) { + ArrayList out = new ArrayList(); + tokenize(text, out); return out.toArray(new Rfc822Token[out.size()]); } diff --git a/core/java/android/util/AttributeSet.java b/core/java/android/util/AttributeSet.java index 01a7ad4840c26729ef1c20d18ee256d246bcf9f9..82592b9f7e061d0e3dcef3dc47f3ffe3573728c2 100644 --- a/core/java/android/util/AttributeSet.java +++ b/core/java/android/util/AttributeSet.java @@ -34,13 +34,13 @@ package android.util; *

      This interface provides an efficient mechanism for retrieving * data from compiled XML files, which can be retrieved for a particular * XmlPullParser through {@link Xml#asAttributeSet - * Xml.getAttributeSet()}. Normally this will return an implementation + * Xml.asAttributeSet()}. Normally this will return an implementation * of the interface that works on top of a generic XmlPullParser, however it * is more useful in conjunction with compiled XML resources: * *

        * XmlPullParser parser = resources.getXml(myResouce);
      - * AttributeSet attributes = Xml.getAttributeSet(parser);
      + * AttributeSet attributes = Xml.asAttributeSet(parser);
    * *

    The implementation returned here, unlike using * the implementation on top of a generic XmlPullParser, diff --git a/core/java/android/util/Config.java b/core/java/android/util/Config.java index 9571041f361ecfe233840c3e1bcba7d5a71c90ec..924b49de9e7e668914cc24a8cf1d3ce993d3d10b 100644 --- a/core/java/android/util/Config.java +++ b/core/java/android/util/Config.java @@ -34,25 +34,25 @@ public final class Config */ /** - * Always the inverse of DEBUG. + * @deprecated Use {@link #DEBUG} instead. */ @Deprecated public static final boolean RELEASE = !DEBUG; /** - * Always false. + * @deprecated Always false. */ @Deprecated public static final boolean PROFILE = false; /** - * Always false. + * @deprecated Always false. */ @Deprecated public static final boolean LOGV = false; /** - * Always true. + * @deprecated Always true. */ @Deprecated public static final boolean LOGD = true; diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index 74f01cc8719756ac238db0b4c8b9585d5892047e..2628eb4026d58d6210a617a76d0511c670209fe6 100644 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -69,7 +69,7 @@ public class DisplayMetrics { * Density Independent Pixel unit, where one DIP is one pixel on an * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), * providing the baseline of the system's display. Thus on a 160dpi screen - * this density value will be 1; on a 106 dpi screen it would be .75; etc. + * this density value will be 1; on a 120 dpi screen it would be .75; etc. * *

    This value does not exactly follow the real screen size (as given by * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java index 24b4f73e3d057c38d4f7448a5031be8aed8b1427..81dd96e128843669ee124e9f684d9e58e23e7b14 100644 --- a/core/java/android/util/EventLog.java +++ b/core/java/android/util/EventLog.java @@ -73,7 +73,7 @@ import java.util.List; * *

  • *
  • '\n': 1 byte - an automatically generated newline, used to help detect and recover from log - * corruption and enable stansard unix tools like grep, tail and wc to operate + * corruption and enable standard unix tools like grep, tail and wc to operate * on event logs.
  • * * @@ -124,10 +124,6 @@ public class EventLog { "A List must have fewer than " + Byte.MAX_VALUE + " items in it."); } - if (items.length < 1) { - throw new IllegalArgumentException( - "A List must have at least one item in it."); - } for (int i = 0; i < items.length; i++) { final Object item = items[i]; if (item == null) { @@ -192,17 +188,21 @@ public class EventLog { return decodeObject(); } + public byte[] getRawData() { + return mBuffer.array(); + } + /** @return the loggable item at the current position in mBuffer. */ private Object decodeObject() { if (mBuffer.remaining() < 1) return null; switch (mBuffer.get()) { case INT: if (mBuffer.remaining() < 4) return null; - return mBuffer.getInt(); + return (Integer) mBuffer.getInt(); case LONG: if (mBuffer.remaining() < 8) return null; - return mBuffer.getLong(); + return (Long) mBuffer.getLong(); case STRING: try { @@ -219,7 +219,7 @@ public class EventLog { case LIST: if (mBuffer.remaining() < 1) return null; int length = mBuffer.get(); - if (length <= 0) return null; + if (length < 0) return null; Object[] array = new Object[length]; for (int i = 0; i < length; ++i) { array[i] = decodeObject(); @@ -285,4 +285,13 @@ public class EventLog { */ public static native void readEvents(int[] tags, Collection output) throws IOException; + + /** + * Read events from a file. + * @param path to read from + * @param output container to add events into + * @throws IOException if something goes wrong reading events + */ + public static native void readEvents(String path, Collection output) + throws IOException; } diff --git a/core/java/android/util/MathUtils.java b/core/java/android/util/MathUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..b35dd1eac0debd00d66d4eb027cdd3cf10d7b79e --- /dev/null +++ b/core/java/android/util/MathUtils.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.util.Random; + +/** + * A class that contains utility methods related to numbers. + * + * @hide Pending API council approval + */ +public final class MathUtils { + private static final Random sRandom = new Random(); + private static final float DEG_TO_RAD = 3.1415926f / 180.0f; + private static final float RAD_TO_DEG = 180.0f / 3.1415926f; + + private MathUtils() { + } + + public static float abs(float v) { + return v > 0 ? v : -v; + } + + public static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static float log(float a) { + return (float) Math.log(a); + } + + public static float exp(float a) { + return (float) Math.exp(a); + } + + public static float pow(float a, float b) { + return (float) Math.pow(a, b); + } + + public static float max(float a, float b) { + return a > b ? a : b; + } + + public static float max(int a, int b) { + return a > b ? a : b; + } + + public static float max(float a, float b, float c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float max(int a, int b, int c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float min(float a, float b) { + return a < b ? a : b; + } + + public static float min(int a, int b) { + return a < b ? a : b; + } + + public static float min(float a, float b, float c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float min(int a, int b, int c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float) Math.sqrt(x * x + y * y); + } + + public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + final float z = (z2 - z1); + return (float) Math.sqrt(x * x + y * y + z * z); + } + + public static float mag(float a, float b) { + return (float) Math.sqrt(a * a + b * b); + } + + public static float mag(float a, float b, float c) { + return (float) Math.sqrt(a * a + b * b + c * c); + } + + public static float sq(float v) { + return v * v; + } + + public static float radians(float degrees) { + return degrees * DEG_TO_RAD; + } + + public static float degrees(float radians) { + return radians * RAD_TO_DEG; + } + + public static float acos(float value) { + return (float) Math.acos(value); + } + + public static float asin(float value) { + return (float) Math.asin(value); + } + + public static float atan(float value) { + return (float) Math.atan(value); + } + + public static float atan2(float a, float b) { + return (float) Math.atan2(a, b); + } + + public static float tan(float angle) { + return (float) Math.tan(angle); + } + + public static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + public static float norm(float start, float stop, float value) { + return (value - start) / (stop - start); + } + + public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) { + return maxStart + (maxStart - maxStop) * ((value - minStart) / (minStop - minStart)); + } + + public static int random(int howbig) { + return (int) (sRandom.nextFloat() * howbig); + } + + public static int random(int howsmall, int howbig) { + if (howsmall >= howbig) return howsmall; + return (int) (sRandom.nextFloat() * (howbig - howsmall) + howsmall); + } + + public static float random(float howbig) { + return sRandom.nextFloat() * howbig; + } + + public static float random(float howsmall, float howbig) { + if (howsmall >= howbig) return howsmall; + return sRandom.nextFloat() * (howbig - howsmall) + howsmall; + } + + public static void randomSeed(long seed) { + sRandom.setSeed(seed); + } +} diff --git a/core/java/android/util/Pair.java b/core/java/android/util/Pair.java new file mode 100644 index 0000000000000000000000000000000000000000..bf25306540fd569867142695130ebb6a1fffc287 --- /dev/null +++ b/core/java/android/util/Pair.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. If either are null then equals() and hashCode() will throw + * a NullPointerException. + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective equals() methods. + * @param o the Pair to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered equals() + */ + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Pair)) return false; + final Pair other; + try { + other = (Pair) o; + } catch (ClassCastException e) { + return false; + } + return first.equals(other.first) && second.equals(other.second); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * @return a hashcode of the Pair + */ + public int hashCode() { + int result = 17; + result = 31 * result + first.hashCode(); + result = 31 * result + second.hashCode(); + return result; + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 841066c7ba525572c352edb41680a1e4b547c9fa..e1f2823989b628bf8e7a9f927c9a662f39e0abde 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -24,8 +24,29 @@ public class HapticFeedbackConstants { private HapticFeedbackConstants() {} + /** + * The user has performed a long press on an object that is resulting + * in an action being performed. + */ public static final int LONG_PRESS = 0; + /** + * The user has pressed on a virtual on-screen key. + */ + public static final int VIRTUAL_KEY = 1; + + /** + * This is a private constant. Feel free to renumber as desired. + * @hide + */ + public static final int SAFE_MODE_DISABLED = 10000; + + /** + * This is a private constant. Feel free to renumber as desired. + * @hide + */ + public static final int SAFE_MODE_ENABLED = 10001; + /** * Flag for {@link View#performHapticFeedback(int, int) * View.performHapticFeedback(int, int)}: Ignore the setting in the diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 99d5c0caee36f75e0f497887e242bd80d887e648..71302cb5ce20ff1690c98dc567a0d8b8ee4aaaaa 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -18,11 +18,11 @@ package android.view; import android.graphics.Rect; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.view.KeyEvent; import android.view.MotionEvent; -import android.os.ParcelFileDescriptor; - /** * API back to a client window that the Window Manager uses to inform it of * interesting things happening. @@ -46,8 +46,8 @@ oneway interface IWindow { void resized(int w, int h, in Rect coveredInsets, in Rect visibleInsets, boolean reportDraw); void dispatchKey(in KeyEvent event); - void dispatchPointer(in MotionEvent event, long eventTime); - void dispatchTrackball(in MotionEvent event, long eventTime); + void dispatchPointer(in MotionEvent event, long eventTime, boolean callWhenDone); + void dispatchTrackball(in MotionEvent event, long eventTime, boolean callWhenDone); void dispatchAppVisibility(boolean visible); void dispatchGetNewSurface(); @@ -56,4 +56,14 @@ oneway interface IWindow { * to date on the current state showing navigational focus (touch mode) too. */ void windowFocusChanged(boolean hasFocus, boolean inTouchMode); + + void closeSystemDialogs(String reason); + + /** + * Called for wallpaper windows when their offsets change. + */ + void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, boolean sync); + + void dispatchWallpaperCommand(String action, int x, int y, + int z, in Bundle extras, boolean sync); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 5607d4bb6783855322ddd702c67756c2f3f2a364..23e7fb737f85fead37a0ef303f7543b9acb28d72 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -71,6 +71,7 @@ interface IWindowManager void setFocusedApp(IBinder token, boolean moveFocusNow); void prepareAppTransition(int transit); int getPendingAppTransition(); + void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim); void executeAppTransition(); void setAppStartingWindow(IBinder token, String pkg, int theme, CharSequence nonLocalizedLabel, int labelRes, @@ -90,6 +91,7 @@ interface IWindowManager void exitKeyguardSecurely(IOnKeyguardExitResult callback); boolean inKeyguardRestrictedInputMode(); + void closeSystemDialogs(String reason); // These can only be called with the SET_ANIMATON_SCALE permission. float getAnimationScale(int which); diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 11568566940627e640c9a2990947b63dda837237..b6b009b33a0bd287bb28ab867ae6e2ccaa40868d 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -19,6 +19,7 @@ package android.view; import android.graphics.Rect; import android.graphics.Region; +import android.os.Bundle; import android.view.IWindow; import android.view.MotionEvent; import android.view.WindowManager; @@ -108,4 +109,19 @@ interface IWindowSession { boolean getInTouchMode(); boolean performHapticFeedback(IWindow window, int effectId, boolean always); + + /** + * For windows with the wallpaper behind them, and the wallpaper is + * larger than the screen, set the offset within the screen. + * For multi screen launcher type applications, xstep and ystep indicate + * how big the increment is from one screen to another. + */ + void setWallpaperPosition(IBinder windowToken, float x, float y, float xstep, float ystep); + + void wallpaperOffsetsComplete(IBinder window); + + Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + int z, in Bundle extras, boolean sync); + + void wallpaperCommandComplete(IBinder window, in Bundle result); } diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 6349288b7e4be46bd7a1d9e5dd1af86f071793a4..d4f978756f4d4284968ee0d8f2d458a5e21c07fd 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -18,6 +18,8 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; +import android.util.SparseIntArray; import android.view.KeyCharacterMap; import android.view.KeyCharacterMap.KeyData; @@ -257,6 +259,51 @@ public class KeyEvent implements Parcelable { */ public static final int FLAG_EDITOR_ACTION = 0x10; + /** + * When associated with up key events, this indicates that the key press + * has been canceled. Typically this is used with virtual touch screen + * keys, where the user can slide from the virtual key area on to the + * display: in that case, the application will receive a canceled up + * event and should not perform the action normally associated with the + * key. Note that for this to work, the application can not perform an + * action for a key until it receives an up or the long press timeout has + * expired. + */ + public static final int FLAG_CANCELED = 0x20; + + /** + * This key event was generated by a virtual (on-screen) hard key area. + * Typically this is an area of the touchscreen, outside of the regular + * display, dedicated to "hardware" buttons. + */ + public static final int FLAG_VIRTUAL_HARD_KEY = 0x40; + + /** + * This flag is set for the first key repeat that occurs after the + * long press timeout. + */ + public static final int FLAG_LONG_PRESS = 0x80; + + /** + * Set when a key event has {@link #FLAG_CANCELED} set because a long + * press action was executed while it was down. + */ + public static final int FLAG_CANCELED_LONG_PRESS = 0x100; + + /** + * Set for {@link #ACTION_UP} when this event's key code is still being + * tracked from its initial down. That is, somebody requested that tracking + * started on the key down and a long press has not caused + * the tracking to be canceled. + */ + public static final int FLAG_TRACKING = 0x200; + + /** + * Private control to determine when an app is tracking a key sequence. + * @hide + */ + public static final int FLAG_START_TRACKING = 0x40000000; + /** * Returns the maximum keycode. */ @@ -273,6 +320,9 @@ public class KeyEvent implements Parcelable { return KeyCharacterMap.getDeadChar(accent, c); } + static final boolean DEBUG = false; + static final String TAG = "KeyEvent"; + private int mMetaState; private int mAction; private int mKeyCode; @@ -286,7 +336,11 @@ public class KeyEvent implements Parcelable { public interface Callback { /** - * Called when a key down event has occurred. + * Called when a key down event has occurred. If you return true, + * you can first call {@link KeyEvent#startTracking() + * KeyEvent.startTracking()} to have the framework track the event + * through its {@link #onKeyUp(int, KeyEvent)} and also call your + * {@link #onKeyLongPress(int, KeyEvent)} if it occurs. * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. @@ -296,6 +350,22 @@ public class KeyEvent implements Parcelable { */ boolean onKeyDown(int keyCode, KeyEvent event); + /** + * Called when a long press has occurred. If you return true, + * the final key up will have {@link KeyEvent#FLAG_CANCELED} and + * {@link KeyEvent#FLAG_CANCELED_LONG_PRESS} set. Note that in + * order to receive this callback, someone in the event change + * must return true from {@link #onKeyDown} and + * call {@link KeyEvent#startTracking()} on the event. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * + * @return If you handled the event, return true. If you want to allow + * the event to be handled by the next receiver, return false. + */ + boolean onKeyLongPress(int keyCode, KeyEvent event); + /** * Called when a key up event has occurred. * @@ -481,11 +551,15 @@ public class KeyEvent implements Parcelable { /** * Copy an existing key event, modifying its time and repeat count. * + * @deprecated Use {@link #changeTimeRepeat(KeyEvent, long, int)} + * instead. + * * @param origEvent The existing event to be copied. * @param eventTime The new event time * (in {@link android.os.SystemClock#uptimeMillis}) of the event. * @param newRepeat The new repeat count of the event. */ + @Deprecated public KeyEvent(KeyEvent origEvent, long eventTime, int newRepeat) { mDownTime = origEvent.mDownTime; mEventTime = eventTime; @@ -513,6 +587,26 @@ public class KeyEvent implements Parcelable { return new KeyEvent(event, eventTime, newRepeat); } + /** + * Create a new key event that is the same as the given one, but whose + * event time and repeat count are replaced with the given value. + * + * @param event The existing event to be copied. This is not modified. + * @param eventTime The new event time + * (in {@link android.os.SystemClock#uptimeMillis}) of the event. + * @param newRepeat The new repeat count of the event. + * @param newFlags New flags for the event, replacing the entire value + * in the original event. + */ + public static KeyEvent changeTimeRepeat(KeyEvent event, long eventTime, + int newRepeat, int newFlags) { + KeyEvent ret = new KeyEvent(event); + ret.mEventTime = eventTime; + ret.mRepeatCount = newRepeat; + ret.mFlags = newFlags; + return ret; + } + /** * Copy an existing key event, modifying its action. * @@ -693,6 +787,42 @@ public class KeyEvent implements Parcelable { return mAction; } + /** + * For {@link #ACTION_UP} events, indicates that the event has been + * canceled as per {@link #FLAG_CANCELED}. + */ + public final boolean isCanceled() { + return (mFlags&FLAG_CANCELED) != 0; + } + + /** + * Call this during {@link Callback#onKeyDown} to have the system track + * the key through its final up (possibly including a long press). Note + * that only one key can be tracked at a time -- if another key down + * event is received while a previous one is being tracked, tracking is + * stopped on the previous event. + */ + public final void startTracking() { + mFlags |= FLAG_START_TRACKING; + } + + /** + * For {@link #ACTION_UP} events, indicates that the event is still being + * tracked from its initial down event as per + * {@link #FLAG_TRACKING}. + */ + public final boolean isTracking() { + return (mFlags&FLAG_TRACKING) != 0; + } + + /** + * For {@link #ACTION_DOWN} events, indicates that the event has been + * canceled as per {@link #FLAG_LONG_PRESS}. + */ + public final boolean isLongPress() { + return (mFlags&FLAG_LONG_PRESS) != 0; + } + /** * Retrieve the key code of the key event. This is the physical key that * was pressed, not the Unicode character. @@ -878,20 +1008,56 @@ public class KeyEvent implements Parcelable { return KeyCharacterMap.load(mDeviceId).isPrintingKey(mKeyCode); } + /** + * @deprecated Use {@link #dispatch(Callback, DispatcherState, Object)} instead. + */ + @Deprecated + public final boolean dispatch(Callback receiver) { + return dispatch(receiver, null, null); + } + /** * Deliver this key event to a {@link Callback} interface. If this is * an ACTION_MULTIPLE event and it is not handled, then an attempt will * be made to deliver a single normal event. * * @param receiver The Callback that will be given the event. + * @param state State information retained across events. + * @param target The target of the dispatch, for use in tracking. * * @return The return value from the Callback method that was called. */ - public final boolean dispatch(Callback receiver) { + public final boolean dispatch(Callback receiver, DispatcherState state, + Object target) { switch (mAction) { - case ACTION_DOWN: - return receiver.onKeyDown(mKeyCode, this); + case ACTION_DOWN: { + mFlags &= ~FLAG_START_TRACKING; + if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state + + ": " + this); + boolean res = receiver.onKeyDown(mKeyCode, this); + if (state != null) { + if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) { + if (DEBUG) Log.v(TAG, " Start tracking!"); + state.startTracking(this, target); + } else if (isLongPress() && state.isTracking(this)) { + try { + if (receiver.onKeyLongPress(mKeyCode, this)) { + if (DEBUG) Log.v(TAG, " Clear from long press!"); + state.performedLongPress(this); + res = true; + } + } catch (AbstractMethodError e) { + } + } + } + return res; + } case ACTION_UP: + if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state + + ": " + this); + if (state != null) { + state.handleUpEvent(this); + } return receiver.onKeyUp(mKeyCode, this); case ACTION_MULTIPLE: final int count = mRepeatCount; @@ -911,10 +1077,103 @@ public class KeyEvent implements Parcelable { mRepeatCount = count; return handled; } + return false; } return false; } + /** + * Use with {@link KeyEvent#dispatch(Callback, DispatcherState, Object)} + * for more advanced key dispatching, such as long presses. + */ + public static class DispatcherState { + int mDownKeyCode; + Object mDownTarget; + SparseIntArray mActiveLongPresses = new SparseIntArray(); + + /** + * Reset back to initial state. + */ + public void reset() { + if (DEBUG) Log.v(TAG, "Reset: " + this); + mDownKeyCode = 0; + mDownTarget = null; + mActiveLongPresses.clear(); + } + + /** + * Stop any tracking associated with this target. + */ + public void reset(Object target) { + if (mDownTarget == target) { + if (DEBUG) Log.v(TAG, "Reset in " + target + ": " + this); + mDownKeyCode = 0; + mDownTarget = null; + } + } + + /** + * Start tracking the key code associated with the given event. This + * can only be called on a key down. It will allow you to see any + * long press associated with the key, and will result in + * {@link KeyEvent#isTracking} return true on the long press and up + * events. + * + *

    This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyDown}. + */ + public void startTracking(KeyEvent event, Object target) { + if (event.getAction() != ACTION_DOWN) { + throw new IllegalArgumentException( + "Can only start tracking on a down event"); + } + if (DEBUG) Log.v(TAG, "Start trackingt in " + target + ": " + this); + mDownKeyCode = event.getKeyCode(); + mDownTarget = target; + } + + /** + * Return true if the key event is for a key code that is currently + * being tracked by the dispatcher. + */ + public boolean isTracking(KeyEvent event) { + return mDownKeyCode == event.getKeyCode(); + } + + /** + * Keep track of the given event's key code as having performed an + * action with a long press, so no action should occur on the up. + *

    This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyLongPress}. + */ + public void performedLongPress(KeyEvent event) { + mActiveLongPresses.put(event.getKeyCode(), 1); + } + + /** + * Handle key up event to stop tracking. This resets the dispatcher state, + * and updates the key event state based on it. + *

    This is only needed if you are directly dispatching events, rather + * than handling them in {@link Callback#onKeyUp}. + */ + public void handleUpEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + if (DEBUG) Log.v(TAG, "Handle key up " + event + ": " + this); + int index = mActiveLongPresses.indexOfKey(keyCode); + if (index >= 0) { + if (DEBUG) Log.v(TAG, " Index: " + index); + event.mFlags |= FLAG_CANCELED | FLAG_CANCELED_LONG_PRESS; + mActiveLongPresses.removeAt(index); + } + if (mDownKeyCode == keyCode) { + if (DEBUG) Log.v(TAG, " Tracking!"); + event.mFlags |= FLAG_TRACKING; + mDownKeyCode = 0; + mDownTarget = null; + } + } + } + public String toString() { return "KeyEvent{action=" + mAction + " code=" + mKeyCode + " repeat=" + mRepeatCount diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 94acd3ffa7f7930e7b2cc43053aa6fb7333d776b..e5985c179e9d4394c53e7f58bd2c38d54c7b208e 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -458,11 +458,12 @@ public abstract class LayoutInflater { public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor constructor = sConstructorMap.get(name); + Class clazz = null; try { if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it - Class clazz = mContext.getClassLoader().loadClass( + clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name); if (mFilter != null && clazz != null) { @@ -480,7 +481,7 @@ public abstract class LayoutInflater { Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed - Class clazz = mContext.getClassLoader().loadClass( + clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); @@ -511,7 +512,7 @@ public abstract class LayoutInflater { } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " - + (constructor == null ? "" : constructor.getClass().getName())); + + (clazz == null ? "" : clazz.getName())); ie.initCause(e); throw ie; } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index a224ed306b0a6dbb41201392ac1f4b678d28f170..ca907af108dc2f46973624c9d7bf290bfec0a322 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -19,7 +19,7 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.util.Config; +import android.util.Log; /** * Object used to report movement (mouse, pen, finger, trackball) events. This @@ -27,17 +27,26 @@ import android.util.Config; * it is being used for. */ public final class MotionEvent implements Parcelable { + static final boolean DEBUG_POINTERS = false; + + /** + * Bit mask of the parts of the action code that are the action itself. + */ + public static final int ACTION_MASK = 0xff; + /** * Constant for {@link #getAction}: A pressed gesture has started, the * motion contains the initial starting location. */ public static final int ACTION_DOWN = 0; + /** * Constant for {@link #getAction}: A pressed gesture has finished, the * motion contains the final release location as well as any intermediate * points since the last down or move event. */ public static final int ACTION_UP = 1; + /** * Constant for {@link #getAction}: A change has happened during a * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}). @@ -45,12 +54,14 @@ public final class MotionEvent implements Parcelable { * points since the last down or move event. */ public static final int ACTION_MOVE = 2; + /** * Constant for {@link #getAction}: The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ public static final int ACTION_CANCEL = 3; + /** * Constant for {@link #getAction}: A movement has happened outside of the * normal bounds of the UI element. This does not provide a full gesture, @@ -58,6 +69,70 @@ public final class MotionEvent implements Parcelable { */ public static final int ACTION_OUTSIDE = 4; + /** + * A non-primary pointer has gone down. The bits in + * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + */ + public static final int ACTION_POINTER_DOWN = 5; + + /** + * Synonym for {@link #ACTION_POINTER_DOWN} with + * {@link #ACTION_POINTER_ID_MASK} of 0: the primary pointer has gone done. + */ + public static final int ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN | 0x0000; + + /** + * Synonym for {@link #ACTION_POINTER_DOWN} with + * {@link #ACTION_POINTER_ID_MASK} of 1: the secondary pointer has gone done. + */ + public static final int ACTION_POINTER_2_DOWN = ACTION_POINTER_DOWN | 0x0100; + + /** + * Synonym for {@link #ACTION_POINTER_DOWN} with + * {@link #ACTION_POINTER_ID_MASK} of 2: the tertiary pointer has gone done. + */ + public static final int ACTION_POINTER_3_DOWN = ACTION_POINTER_DOWN | 0x0200; + + /** + * A non-primary pointer has gone up. The bits in + * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + */ + public static final int ACTION_POINTER_UP = 6; + + /** + * Synonym for {@link #ACTION_POINTER_UP} with + * {@link #ACTION_POINTER_ID_MASK} of 0: the primary pointer has gone up. + */ + public static final int ACTION_POINTER_1_UP = ACTION_POINTER_UP | 0x0000; + + /** + * Synonym for {@link #ACTION_POINTER_UP} with + * {@link #ACTION_POINTER_ID_MASK} of 1: the secondary pointer has gone up. + */ + public static final int ACTION_POINTER_2_UP = ACTION_POINTER_UP | 0x0100; + + /** + * Synonym for {@link #ACTION_POINTER_UP} with + * {@link #ACTION_POINTER_ID_MASK} of 2: the tertiary pointer has gone up. + */ + public static final int ACTION_POINTER_3_UP = ACTION_POINTER_UP | 0x0200; + + /** + * Bits in the action code that represent a pointer ID, used with + * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Pointer IDs + * start at 0, with 0 being the primary (first) pointer in the motion. Note + * that this not not an index into the array of pointer values, + * which is compacted to only contain pointers that are down; the pointer + * ID for a particular index can be found with {@link #findPointerIndex}. + */ + public static final int ACTION_POINTER_ID_MASK = 0xff00; + + /** + * Bit shift for the action bits holding the pointer identifier as + * defined by {@link #ACTION_POINTER_ID_MASK}. + */ + public static final int ACTION_POINTER_ID_SHIFT = 8; + private static final boolean TRACK_RECYCLED_LOCATION = false; /** @@ -80,34 +155,83 @@ public final class MotionEvent implements Parcelable { */ public static final int EDGE_RIGHT = 0x00000008; + /** + * Offset for the sample's X coordinate. + * @hide + */ + static public final int SAMPLE_X = 0; + + /** + * Offset for the sample's Y coordinate. + * @hide + */ + static public final int SAMPLE_Y = 1; + + /** + * Offset for the sample's X coordinate. + * @hide + */ + static public final int SAMPLE_PRESSURE = 2; + + /** + * Offset for the sample's X coordinate. + * @hide + */ + static public final int SAMPLE_SIZE = 3; + + /** + * Number of data items for each sample. + * @hide + */ + static public final int NUM_SAMPLE_DATA = 4; + + /** + * Number of possible pointers. + * @hide + */ + static public final int BASE_AVAIL_POINTERS = 5; + + static private final int BASE_AVAIL_SAMPLES = 8; + static private final int MAX_RECYCLED = 10; static private Object gRecyclerLock = new Object(); static private int gRecyclerUsed = 0; static private MotionEvent gRecyclerTop = null; private long mDownTime; - private long mEventTime; + private long mEventTimeNano; private int mAction; - private float mX; - private float mY; private float mRawX; private float mRawY; - private float mPressure; - private float mSize; - private int mMetaState; - private int mNumHistory; - private float[] mHistory; - private long[] mHistoryTimes; private float mXPrecision; private float mYPrecision; private int mDeviceId; private int mEdgeFlags; + private int mMetaState; + + // Here is the actual event data. Note that the order of the array + // is a little odd: the first entry is the most recent, and the ones + // following it are the historical data from oldest to newest. This + // allows us to easily retrieve the most recent data, without having + // to copy the arrays every time a new sample is added. + + private int mNumPointers; + private int mNumSamples; + // Array of mNumPointers size of identifiers for each pointer of data. + private int[] mPointerIdentifiers; + // Array of (mNumSamples * mNumPointers * NUM_SAMPLE_DATA) size of event data. + private float[] mDataSamples; + // Array of mNumSamples size of time stamps. + private long[] mTimeSamples; private MotionEvent mNext; private RuntimeException mRecycledLocation; private boolean mRecycled; private MotionEvent() { + mPointerIdentifiers = new int[BASE_AVAIL_POINTERS]; + mDataSamples = new float[BASE_AVAIL_POINTERS*BASE_AVAIL_SAMPLES*NUM_SAMPLE_DATA]; + mTimeSamples = new long[BASE_AVAIL_SAMPLES]; } static private MotionEvent obtain() { @@ -124,6 +248,86 @@ public final class MotionEvent implements Parcelable { } } + /** + * Create a new MotionEvent, filling in all of the basic values that + * define the motion. + * + * @param downTime The time (in ms) when the user originally pressed down to start + * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTime The the time (in ms) when this specific event was generated. This + * must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTimeNano The the time (in ns) when this specific event was generated. This + * must be obtained from {@link System#nanoTime()}. + * @param action The kind of action being performed -- one of either + * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or + * {@link #ACTION_CANCEL}. + * @param pointers The number of points that will be in this event. + * @param inPointerIds An array of pointers values providing + * an identifier for each pointer. + * @param inData An array of pointers*NUM_SAMPLE_DATA of initial + * data samples for the event. + * @param metaState The state of any meta / modifier keys that were in effect when + * the event was generated. + * @param xPrecision The precision of the X coordinate being reported. + * @param yPrecision The precision of the Y coordinate being reported. + * @param deviceId The id for the device that this event came from. An id of + * zero indicates that the event didn't come from a physical device; other + * numbers are arbitrary and you shouldn't depend on the values. + * @param edgeFlags A bitfield indicating which edges, if any, where touched by this + * MotionEvent. + * + * @hide + */ + static public MotionEvent obtainNano(long downTime, long eventTime, long eventTimeNano, + int action, int pointers, int[] inPointerIds, float[] inData, int metaState, + float xPrecision, float yPrecision, int deviceId, int edgeFlags) { + MotionEvent ev = obtain(); + ev.mDeviceId = deviceId; + ev.mEdgeFlags = edgeFlags; + ev.mDownTime = downTime; + ev.mEventTimeNano = eventTimeNano; + ev.mAction = action; + ev.mMetaState = metaState; + ev.mRawX = inData[SAMPLE_X]; + ev.mRawY = inData[SAMPLE_Y]; + ev.mXPrecision = xPrecision; + ev.mYPrecision = yPrecision; + ev.mNumPointers = pointers; + ev.mNumSamples = 1; + + int[] pointerIdentifiers = ev.mPointerIdentifiers; + if (pointerIdentifiers.length < pointers) { + ev.mPointerIdentifiers = pointerIdentifiers = new int[pointers]; + } + System.arraycopy(inPointerIds, 0, pointerIdentifiers, 0, pointers); + + final int ND = pointers * NUM_SAMPLE_DATA; + float[] dataSamples = ev.mDataSamples; + if (dataSamples.length < ND) { + ev.mDataSamples = dataSamples = new float[ND]; + } + System.arraycopy(inData, 0, dataSamples, 0, ND); + + ev.mTimeSamples[0] = eventTime; + + if (DEBUG_POINTERS) { + StringBuilder sb = new StringBuilder(128); + sb.append("New:"); + for (int i=0; i= NS) { + System.arraycopy(o.mTimeSamples, 0, ev.mTimeSamples, 0, NS); + } else { + ev.mTimeSamples = (long[])o.mTimeSamples.clone(); } + + final int NP = (ev.mNumPointers=o.mNumPointers); + if (ev.mPointerIdentifiers.length >= NP) { + System.arraycopy(o.mPointerIdentifiers, 0, ev.mPointerIdentifiers, 0, NP); + } else { + ev.mPointerIdentifiers = (int[])o.mPointerIdentifiers.clone(); + } + + final int ND = NP * NS * NUM_SAMPLE_DATA; + if (ev.mDataSamples.length >= ND) { + System.arraycopy(o.mDataSamples, 0, ev.mDataSamples, 0, ND); + } else { + ev.mDataSamples = (float[])o.mDataSamples.clone(); + } + + return ev; } /** - * Create a new MotionEvent, copying from an existing one. + * Create a new MotionEvent, copying from an existing one, but not including + * any historical point information. */ - static public MotionEvent obtain(MotionEvent o) { + static public MotionEvent obtainNoHistory(MotionEvent o) { MotionEvent ev = obtain(); ev.mDeviceId = o.mDeviceId; ev.mEdgeFlags = o.mEdgeFlags; ev.mDownTime = o.mDownTime; - ev.mEventTime = o.mEventTime; + ev.mEventTimeNano = o.mEventTimeNano; ev.mAction = o.mAction; - ev.mX = o.mX; + ev.mNumPointers = o.mNumPointers; ev.mRawX = o.mRawX; - ev.mY = o.mY; ev.mRawY = o.mRawY; - ev.mPressure = o.mPressure; - ev.mSize = o.mSize; ev.mMetaState = o.mMetaState; ev.mXPrecision = o.mXPrecision; ev.mYPrecision = o.mYPrecision; - final int N = o.mNumHistory; - ev.mNumHistory = N; - if (N > 0) { - // could be more efficient about this... - ev.mHistory = (float[])o.mHistory.clone(); - ev.mHistoryTimes = (long[])o.mHistoryTimes.clone(); + + ev.mNumSamples = 1; + ev.mTimeSamples[0] = o.mTimeSamples[0]; + + final int NP = (ev.mNumPointers=o.mNumPointers); + if (ev.mPointerIdentifiers.length >= NP) { + System.arraycopy(o.mPointerIdentifiers, 0, ev.mPointerIdentifiers, 0, NP); + } else { + ev.mPointerIdentifiers = (int[])o.mPointerIdentifiers.clone(); } + + final int ND = NP * NUM_SAMPLE_DATA; + if (ev.mDataSamples.length >= ND) { + System.arraycopy(o.mDataSamples, 0, ev.mDataSamples, 0, ND); + } else { + ev.mDataSamples = (float[])o.mDataSamples.clone(); + } + return ev; } @@ -305,7 +608,7 @@ public final class MotionEvent implements Parcelable { synchronized (gRecyclerLock) { if (gRecyclerUsed < MAX_RECYCLED) { gRecyclerUsed++; - mNumHistory = 0; + mNumSamples = 0; mNext = gRecyclerTop; gRecyclerTop = this; } @@ -333,44 +636,145 @@ public final class MotionEvent implements Parcelable { * Returns the time (in ms) when this specific event was generated. */ public final long getEventTime() { - return mEventTime; + return mTimeSamples[0]; } /** - * Returns the X coordinate of this event. Whole numbers are pixels; the - * value may have a fraction for input devices that are sub-pixel precise. + * Returns the time (in ns) when this specific event was generated. + * The value is in nanosecond precision but it may not have nanosecond accuracy. + * + * @hide + */ + public final long getEventTimeNano() { + return mEventTimeNano; + } + + /** + * {@link #getX(int)} for the first pointer index (may be an + * arbitrary pointer identifier). */ public final float getX() { - return mX; + return mDataSamples[SAMPLE_X]; } /** - * Returns the Y coordinate of this event. Whole numbers are pixels; the - * value may have a fraction for input devices that are sub-pixel precise. + * {@link #getY(int)} for the first pointer index (may be an + * arbitrary pointer identifier). */ public final float getY() { - return mY; + return mDataSamples[SAMPLE_Y]; + } + + /** + * {@link #getPressure(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getPressure() { + return mDataSamples[SAMPLE_PRESSURE]; + } + + /** + * {@link #getSize(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getSize() { + return mDataSamples[SAMPLE_SIZE]; + } + + /** + * The number of pointers of data contained in this event. Always + * >= 1. + */ + public final int getPointerCount() { + return mNumPointers; + } + + /** + * Return the pointer identifier associated with a particular pointer + * data index is this event. The identifier tells you the actual pointer + * number associated with the data, accounting for individual pointers + * going up and down since the start of the current gesture. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + */ + public final int getPointerId(int pointerIndex) { + return mPointerIdentifiers[pointerIndex]; + } + + /** + * Given a pointer identifier, find the index of its data in the event. + * + * @param pointerId The identifier of the pointer to be found. + * @return Returns either the index of the pointer (for use with + * {@link #getX(int) et al.), or -1 if there is no data available for + * that pointer identifier. + */ + public final int findPointerIndex(int pointerId) { + int i = mNumPointers; + while (i > 0) { + i--; + if (mPointerIdentifiers[i] == pointerId) { + return i; + } + } + return -1; + } + + /** + * Returns the X coordinate of this event for the given pointer + * index (use {@link #getPointerId(int)} to find the pointer + * identifier for this index). + * Whole numbers are pixels; the + * value may have a fraction for input devices that are sub-pixel precise. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + */ + public final float getX(int pointerIndex) { + return mDataSamples[(pointerIndex*NUM_SAMPLE_DATA) + SAMPLE_X]; } /** - * Returns the current pressure of this event. The pressure generally + * Returns the Y coordinate of this event for the given pointer + * index (use {@link #getPointerId(int)} to find the pointer + * identifier for this index). + * Whole numbers are pixels; the + * value may have a fraction for input devices that are sub-pixel precise. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + */ + public final float getY(int pointerIndex) { + return mDataSamples[(pointerIndex*NUM_SAMPLE_DATA) + SAMPLE_Y]; + } + + /** + * Returns the current pressure of this event for the given pointer + * index (use {@link #getPointerId(int)} to find the pointer + * identifier for this index). + * The pressure generally * ranges from 0 (no pressure at all) to 1 (normal pressure), however * values higher than 1 may be generated depending on the calibration of * the input device. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. */ - public final float getPressure() { - return mPressure; + public final float getPressure(int pointerIndex) { + return mDataSamples[(pointerIndex*NUM_SAMPLE_DATA) + SAMPLE_PRESSURE]; } /** - * Returns a scaled value of the approximate size, of the area being pressed when - * touched with the finger. The actual value in pixels corresponding to the finger - * touch is normalized with the device specific range of values + * Returns a scaled value of the approximate size for the given pointer + * index (use {@link #getPointerId(int)} to find the pointer + * identifier for this index). + * This represents some approximation of the area of the screen being + * pressed; the actual value in pixels corresponding to the + * touch is normalized with the device specific range of values * and scaled to a value between 0 and 1. The value of size can be used to * determine fat touch events. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. */ - public final float getSize() { - return mSize; + public final float getSize(int pointerIndex) { + return mDataSamples[(pointerIndex*NUM_SAMPLE_DATA) + SAMPLE_SIZE]; } /** @@ -436,7 +840,7 @@ public final class MotionEvent implements Parcelable { * @return Returns the number of historical points in the event. */ public final int getHistorySize() { - return mNumHistory; + return mNumSamples - 1; } /** @@ -450,63 +854,111 @@ public final class MotionEvent implements Parcelable { * @see #getEventTime */ public final long getHistoricalEventTime(int pos) { - return mHistoryTimes[pos]; + return mTimeSamples[pos + 1]; } /** - * Returns a historical X coordinate that occurred between this event - * and the previous event. Only applies to ACTION_MOVE events. + * {@link #getHistoricalX(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getHistoricalX(int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + SAMPLE_X]; + } + + /** + * {@link #getHistoricalY(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getHistoricalY(int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + SAMPLE_Y]; + } + + /** + * {@link #getHistoricalPressure(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getHistoricalPressure(int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + SAMPLE_PRESSURE]; + } + + /** + * {@link #getHistoricalSize(int)} for the first pointer index (may be an + * arbitrary pointer identifier). + */ + public final float getHistoricalSize(int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + SAMPLE_SIZE]; + } + + /** + * Returns a historical X coordinate, as per {@link #getX(int)}, that + * occurred between this event and the previous event for the given pointer. + * Only applies to ACTION_MOVE events. * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. * @param pos Which historical value to return; must be less than * {@link #getHistorySize} * * @see #getHistorySize * @see #getX */ - public final float getHistoricalX(int pos) { - return mHistory[pos*4]; + public final float getHistoricalX(int pointerIndex, int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + + (pointerIndex * NUM_SAMPLE_DATA) + SAMPLE_X]; } /** - * Returns a historical Y coordinate that occurred between this event - * and the previous event. Only applies to ACTION_MOVE events. + * Returns a historical Y coordinate, as per {@link #getY(int)}, that + * occurred between this event and the previous event for the given pointer. + * Only applies to ACTION_MOVE events. * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. * @param pos Which historical value to return; must be less than * {@link #getHistorySize} * * @see #getHistorySize * @see #getY */ - public final float getHistoricalY(int pos) { - return mHistory[pos*4 + 1]; + public final float getHistoricalY(int pointerIndex, int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + + (pointerIndex * NUM_SAMPLE_DATA) + SAMPLE_Y]; } /** - * Returns a historical pressure coordinate that occurred between this event - * and the previous event. Only applies to ACTION_MOVE events. + * Returns a historical pressure coordinate, as per {@link #getPressure(int)}, + * that occurred between this event and the previous event for the given + * pointer. Only applies to ACTION_MOVE events. * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getPressure */ - public final float getHistoricalPressure(int pos) { - return mHistory[pos*4 + 2]; + public final float getHistoricalPressure(int pointerIndex, int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + + (pointerIndex * NUM_SAMPLE_DATA) + SAMPLE_PRESSURE]; } /** - * Returns a historical size coordinate that occurred between this event - * and the previous event. Only applies to ACTION_MOVE events. + * Returns a historical size coordinate, as per {@link #getSize(int)}, that + * occurred between this event and the previous event for the given pointer. + * Only applies to ACTION_MOVE events. * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getSize */ - public final float getHistoricalSize(int pos) { - return mHistory[pos*4 + 3]; + public final float getHistoricalSize(int pointerIndex, int pos) { + return mDataSamples[((pos + 1) * NUM_SAMPLE_DATA * mNumPointers) + + (pointerIndex * NUM_SAMPLE_DATA) + SAMPLE_SIZE]; } /** @@ -556,16 +1008,11 @@ public final class MotionEvent implements Parcelable { * @param deltaY Amount to add to the current Y coordinate of the event. */ public final void offsetLocation(float deltaX, float deltaY) { - mX += deltaX; - mY += deltaY; - final int N = mNumHistory*4; - if (N <= 0) { - return; - } - final float[] pos = mHistory; - for (int i=0; i CREATOR @@ -663,26 +1171,29 @@ public final class MotionEvent implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeLong(mDownTime); - out.writeLong(mEventTime); + out.writeLong(mEventTimeNano); out.writeInt(mAction); - out.writeFloat(mX); - out.writeFloat(mY); - out.writeFloat(mPressure); - out.writeFloat(mSize); out.writeInt(mMetaState); out.writeFloat(mRawX); out.writeFloat(mRawY); - final int N = mNumHistory; - out.writeInt(N); - if (N > 0) { - final int N4 = N*4; + final int NP = mNumPointers; + out.writeInt(NP); + final int NS = mNumSamples; + out.writeInt(NS); + final int NI = NP*NS; + if (NI > 0) { int i; - float[] history = mHistory; - for (i=0; i 0) { - final int N4 = N*4; - float[] history = mHistory; - if (history == null || history.length < N4) { - mHistory = history = new float[N4 + (4*4)]; + final int NP = in.readInt(); + mNumPointers = NP; + final int NS = in.readInt(); + mNumSamples = NS; + final int NI = NP*NS; + if (NI > 0) { + int[] ids = mPointerIdentifiers; + if (ids.length < NP) { + mPointerIdentifiers = ids = new int[NP]; + } + for (int i=0; i

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

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

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

    Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + */ + public void setZOrderOnTop(boolean onTop) { + mWindowType = onTop ? WindowManager.LayoutParams.TYPE_APPLICATION_PANEL + : WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + } + /** * Hack to allow special layering of windows. The type is one of the * types in WindowManager.LayoutParams. This is a hack so: @@ -279,7 +337,9 @@ public class SurfaceView extends View { return; } ViewRoot viewRoot = (ViewRoot) getRootView().getParent(); - mTranslator = viewRoot.mTranslator; + if (viewRoot != null) { + mTranslator = viewRoot.mTranslator; + } Resources res = getContext().getResources(); if (mTranslator != null || !res.getCompatibilityInfo().supportsScreen()) { @@ -328,7 +388,9 @@ public class SurfaceView extends View { } mLayout.format = mRequestedFormat; - mLayout.flags |=WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + mLayout.flags |=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_SCALED | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE @@ -434,7 +496,7 @@ public class SurfaceView extends View { updateWindow(false); } - private static class MyWindow extends IWindow.Stub { + private static class MyWindow extends BaseIWindow { private final WeakReference mSurfaceView; public MyWindow(SurfaceView surfaceView) { @@ -476,7 +538,8 @@ public class SurfaceView extends View { } } - public void dispatchPointer(MotionEvent event, long eventTime) { + public void dispatchPointer(MotionEvent event, long eventTime, + boolean callWhenDone) { Log.w("SurfaceView", "Unexpected pointer event in surface: " + event); //if (mSession != null && mSurface != null) { // try { @@ -486,7 +549,8 @@ public class SurfaceView extends View { //} } - public void dispatchTrackball(MotionEvent event, long eventTime) { + public void dispatchTrackball(MotionEvent event, long eventTime, + boolean callWhenDone) { Log.w("SurfaceView", "Unexpected trackball event in surface: " + event); //if (mSession != null && mSurface != null) { // try { @@ -568,9 +632,14 @@ public class SurfaceView extends View { public void setType(int type) { switch (type) { - case SURFACE_TYPE_NORMAL: case SURFACE_TYPE_HARDWARE: case SURFACE_TYPE_GPU: + // these are deprecated, treat as "NORMAL" + type = SURFACE_TYPE_NORMAL; + break; + } + switch (type) { + case SURFACE_TYPE_NORMAL: case SURFACE_TYPE_PUSH_BUFFERS: mRequestedType = type; if (mWindow != null) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7ed2712dc7503f4da41826609f56f74abb9f0144..0b8753659087c3e95f85e78cc94ef0366847c455 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Interpolator; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; @@ -46,7 +47,6 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Config; -import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.Pool; @@ -59,6 +59,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; @@ -515,7 +516,8 @@ import java.util.WeakHashMap; * The framework provides basic support for views that wish to internally * scroll their content. This includes keeping track of the X and Y scroll * offset as well as mechanisms for drawing scrollbars. See - * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)} for more details. + * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)}, and + * {@link #awakenScrollBars()} for more details. *

    * *
    @@ -572,6 +574,8 @@ import java.util.WeakHashMap; * @attr ref android.R.styleable#View_scrollbarSize * @attr ref android.R.styleable#View_scrollbarStyle * @attr ref android.R.styleable#View_scrollbars + * @attr ref android.R.styleable#View_scrollbarDefaultDelayBeforeFade + * @attr ref android.R.styleable#View_scrollbarFadeDuration * @attr ref android.R.styleable#View_scrollbarTrackHorizontal * @attr ref android.R.styleable#View_scrollbarThumbHorizontal * @attr ref android.R.styleable#View_scrollbarThumbVertical @@ -2215,12 +2219,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility protected void initializeScrollbars(TypedArray a) { initScrollCache(); - if (mScrollCache.scrollBar == null) { - mScrollCache.scrollBar = new ScrollBarDrawable(); - } - final ScrollabilityCache scrollabilityCache = mScrollCache; - + + if (scrollabilityCache.scrollBar == null) { + scrollabilityCache.scrollBar = new ScrollBarDrawable(); + } + + final boolean fadeScrollbars = a.getBoolean(R.styleable.View_fadeScrollbars, false); + + if (!fadeScrollbars) { + scrollabilityCache.state = ScrollabilityCache.ON; + } + scrollabilityCache.fadeScrollBars = fadeScrollbars; + + + scrollabilityCache.scrollBarFadeDuration = a.getInt( + R.styleable.View_scrollbarFadeDuration, ViewConfiguration + .getScrollBarFadeDuration()); + scrollabilityCache.scrollBarDefaultDelayBeforeFade = a.getInt( + R.styleable.View_scrollbarDefaultDelayBeforeFade, + ViewConfiguration.getScrollDefaultDelay()); + + scrollabilityCache.scrollBarSize = a.getDimensionPixelSize( com.android.internal.R.styleable.View_scrollbarSize, ViewConfiguration.get(mContext).getScaledScrollBarSize()); @@ -2264,7 +2284,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ private void initScrollCache() { if (mScrollCache == null) { - mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext)); + mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext), this); } } @@ -2601,6 +2621,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (mOnFocusChangeListener != null) { mOnFocusChangeListener.onFocusChange(this, gainFocus); } + + if (mAttachInfo != null) { + mAttachInfo.mKeyDispatchState.reset(this); + } } /** @@ -2983,6 +3007,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @param enabled True if this view is enabled, false otherwise. */ public void setEnabled(boolean enabled) { + if (enabled == isEnabled()) return; + setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK); /* @@ -3607,6 +3633,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility ViewDebug.dumpCapturedView(subTag, v); } + /** + * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} + * for this view's window. Returns null if the view is not currently attached + * to the window. Normally you will not need to use this directly, but + * just use the standard high-level event callbacks like {@link #onKeyDown}. + */ + public KeyEvent.DispatcherState getKeyDispatcherState() { + return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null; + } + /** * Dispatch a key event before it is processed by any input method * associated with the view hierarchy. This can be used to intercept @@ -3644,7 +3680,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return true; } - return event.dispatch(this); + return event.dispatch(this, mAttachInfo != null + ? mAttachInfo.mKeyDispatchState : null, this); } /** @@ -3908,6 +3945,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return result; } + /** + * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle + * the event). + */ + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + /** * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) * KeyEvent.Callback.onKeyMultiple()}: perform clicking of the view @@ -4646,7 +4692,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mScrollX = x; mScrollY = y; onScrollChanged(mScrollX, mScrollY, oldX, oldY); - invalidate(); + if (!awakenScrollBars()) { + invalidate(); + } } } @@ -4661,6 +4709,160 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility scrollTo(mScrollX + x, mScrollY + y); } + /** + *

    Trigger the scrollbars to draw. When invoked this method starts an + * animation to fade the scrollbars out after a default delay. If a subclass + * provides animated scrolling, the start delay should equal the duration + * of the scrolling animation.

    + * + *

    The animation starts only if at least one of the scrollbars is + * enabled, as specified by {@link #isHorizontalScrollBarEnabled()} and + * {@link #isVerticalScrollBarEnabled()}. When the animation is started, + * this method returns true, and false otherwise. If the animation is + * started, this method calls {@link #invalidate()}; in that case the + * caller should not call {@link #invalidate()}.

    + * + *

    This method should be invoked every time a subclass directly updates + * the scroll parameters.

    + * + *

    This method is automatically invoked by {@link #scrollBy(int, int)} + * and {@link #scrollTo(int, int)}.

    + * + * @return true if the animation is played, false otherwise + * + * @see #awakenScrollBars(int) + * @see #scrollBy(int, int) + * @see #scrollTo(int, int) + * @see #isHorizontalScrollBarEnabled() + * @see #isVerticalScrollBarEnabled() + * @see #setHorizontalScrollBarEnabled(boolean) + * @see #setVerticalScrollBarEnabled(boolean) + */ + protected boolean awakenScrollBars() { + return mScrollCache != null && + awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade, true); + } + + /** + *

    + * Trigger the scrollbars to draw. When invoked this method starts an + * animation to fade the scrollbars out after a fixed delay. If a subclass + * provides animated scrolling, the start delay should equal the duration of + * the scrolling animation. + *

    + * + *

    + * The animation starts only if at least one of the scrollbars is enabled, + * as specified by {@link #isHorizontalScrollBarEnabled()} and + * {@link #isVerticalScrollBarEnabled()}. When the animation is started, + * this method returns true, and false otherwise. If the animation is + * started, this method calls {@link #invalidate()}; in that case the caller + * should not call {@link #invalidate()}. + *

    + * + *

    + * This method should be invoked everytime a subclass directly updates the + * scroll parameters. + *

    + * + * @param startDelay the delay, in milliseconds, after which the animation + * should start; when the delay is 0, the animation starts + * immediately + * @return true if the animation is played, false otherwise + * + * @see #scrollBy(int, int) + * @see #scrollTo(int, int) + * @see #isHorizontalScrollBarEnabled() + * @see #isVerticalScrollBarEnabled() + * @see #setHorizontalScrollBarEnabled(boolean) + * @see #setVerticalScrollBarEnabled(boolean) + */ + protected boolean awakenScrollBars(int startDelay) { + return awakenScrollBars(startDelay, true); + } + + /** + *

    + * Trigger the scrollbars to draw. When invoked this method starts an + * animation to fade the scrollbars out after a fixed delay. If a subclass + * provides animated scrolling, the start delay should equal the duration of + * the scrolling animation. + *

    + * + *

    + * The animation starts only if at least one of the scrollbars is enabled, + * as specified by {@link #isHorizontalScrollBarEnabled()} and + * {@link #isVerticalScrollBarEnabled()}. When the animation is started, + * this method returns true, and false otherwise. If the animation is + * started, this method calls {@link #invalidate()} if the invalidate parameter + * is set to true; in that case the caller + * should not call {@link #invalidate()}. + *

    + * + *

    + * This method should be invoked everytime a subclass directly updates the + * scroll parameters. + *

    + * + * @param startDelay the delay, in milliseconds, after which the animation + * should start; when the delay is 0, the animation starts + * immediately + * + * @param invalidate Wheter this method should call invalidate + * + * @return true if the animation is played, false otherwise + * + * @see #scrollBy(int, int) + * @see #scrollTo(int, int) + * @see #isHorizontalScrollBarEnabled() + * @see #isVerticalScrollBarEnabled() + * @see #setHorizontalScrollBarEnabled(boolean) + * @see #setVerticalScrollBarEnabled(boolean) + */ + protected boolean awakenScrollBars(int startDelay, boolean invalidate) { + final ScrollabilityCache scrollCache = mScrollCache; + + if (scrollCache == null || !scrollCache.fadeScrollBars) { + return false; + } + + if (scrollCache.scrollBar == null) { + scrollCache.scrollBar = new ScrollBarDrawable(); + } + + if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) { + + if (invalidate) { + // Invalidate to show the scrollbars + invalidate(); + } + + if (scrollCache.state == ScrollabilityCache.OFF) { + // FIXME: this is copied from WindowManagerService. + // We should get this value from the system when it + // is possible to do so. + final int KEY_REPEAT_FIRST_DELAY = 750; + startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay); + } + + // Tell mScrollCache when we should start fading. This may + // extend the fade start time if one was already scheduled + long fadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay; + scrollCache.fadeStartTime = fadeStartTime; + scrollCache.state = ScrollabilityCache.ON; + + // Schedule our fader to run, unscheduling any old ones first + if (mAttachInfo != null) { + mAttachInfo.mHandler.removeCallbacks(scrollCache); + mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime); + } + + return true; + } + + return false; + } + /** * Mark the the area defined by dirty as needing to be drawn. If the view is * visible, {@link #onDraw} will be called at some point in the future. @@ -4754,8 +4956,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * invalidate/draw passes. * * @return True if this View is guaranteed to be fully opaque, false otherwise. - * - * @hide Pending API council approval */ @ViewDebug.ExportedProperty public boolean isOpaque() { @@ -5152,7 +5352,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private void recomputePadding() { setPadding(mPaddingLeft, mPaddingTop, mUserPaddingRight, mUserPaddingBottom); } - + + /** + * Define whether scrollbars will fade when the view is not scrolling. + * + * @param fadeScrollbars wheter to enable fading + * + */ + public void setScrollbarFadingEnabled(boolean fadeScrollbars) { + initScrollCache(); + final ScrollabilityCache scrollabilityCache = mScrollCache; + scrollabilityCache.fadeScrollBars = fadeScrollbars; + if (fadeScrollbars) { + scrollabilityCache.state = ScrollabilityCache.OFF; + } else { + scrollabilityCache.state = ScrollabilityCache.ON; + } + } + + /** + * + * Returns true if scrollbars will fade when this view is not scrolling + * + * @return true if scrollbar fading is enabled + */ + public boolean isScrollbarFadingEnabled() { + return mScrollCache != null && mScrollCache.fadeScrollBars; + } + /** *

    Specify the style of the scrollbars. The scrollbars can be overlaid or * inset. When inset, they add to the padding of the view. And the scrollbars @@ -5319,11 +5546,49 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * scrollbars are painted only if they have been awakened first.

    * * @param canvas the canvas on which to draw the scrollbars + * + * @see #awakenScrollBars(int) */ - private void onDrawScrollBars(Canvas canvas) { + protected final void onDrawScrollBars(Canvas canvas) { // scrollbars are drawn only when the animation is running final ScrollabilityCache cache = mScrollCache; if (cache != null) { + + int state = cache.state; + + if (state == ScrollabilityCache.OFF) { + return; + } + + boolean invalidate = false; + + if (state == ScrollabilityCache.FADING) { + // We're fading -- get our fade interpolation + if (cache.interpolatorValues == null) { + cache.interpolatorValues = new float[1]; + } + + float[] values = cache.interpolatorValues; + + // Stops the animation if we're done + if (cache.scrollBarInterpolator.timeToValues(values) == + Interpolator.Result.FREEZE_END) { + cache.state = ScrollabilityCache.OFF; + } else { + cache.scrollBar.setAlpha(Math.round(values[0])); + } + + // This will make the scroll bars inval themselves after + // drawing. We only want this when we're fading so that + // we prevent excessive redraws + invalidate = true; + } else { + // We're just on -- but we may have been fading before so + // reset alpha + cache.scrollBar.setAlpha(255); + } + + final int viewFlags = mViewFlags; final boolean drawHorizontalScrollBar = @@ -5342,12 +5607,41 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility size = cache.scrollBarSize; } + final int scrollX = mScrollX; + final int scrollY = mScrollY; + final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; + + int left, top, right, bottom; + if (drawHorizontalScrollBar) { - onDrawHorizontalScrollBar(canvas, scrollBar, width, height, size); + scrollBar.setParameters(computeHorizontalScrollRange(), + computeHorizontalScrollOffset(), + computeHorizontalScrollExtent(), false); + final int verticalScrollBarGap = drawVerticalScrollBar ? + getVerticalScrollbarWidth() : 0; + top = scrollY + height - size - (mUserPaddingBottom & inside); + left = scrollX + (mPaddingLeft & inside); + right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap; + bottom = top + size; + onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom); + if (invalidate) { + invalidate(left, top, right, bottom); + } } if (drawVerticalScrollBar) { - onDrawVerticalScrollBar(canvas, scrollBar, width, height, size); + scrollBar.setParameters(computeVerticalScrollRange(), + computeVerticalScrollOffset(), + computeVerticalScrollExtent(), true); + // TODO: Deal with RTL languages to position scrollbar on left + left = scrollX + width - size - (mUserPaddingRight & inside); + top = scrollY + (mPaddingTop & inside); + right = left + size; + bottom = scrollY + height - (mUserPaddingBottom & inside); + onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom); + if (invalidate) { + invalidate(left, top, right, bottom); + } } } } @@ -5367,44 +5661,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility *

    Draw the horizontal scrollbar if * {@link #isHorizontalScrollBarEnabled()} returns true.

    * - *

    The length of the scrollbar and its thumb is computed according to the - * values returned by {@link #computeHorizontalScrollRange()}, - * {@link #computeHorizontalScrollExtent()} and - * {@link #computeHorizontalScrollOffset()}. Refer to - * {@link android.widget.ScrollBarDrawable} for more information about how - * these values relate to each other.

    - * * @param canvas the canvas on which to draw the scrollbar * @param scrollBar the scrollbar's drawable - * @param width the width of the drawing surface - * @param height the height of the drawing surface - * @param size the size of the scrollbar * * @see #isHorizontalScrollBarEnabled() * @see #computeHorizontalScrollRange() * @see #computeHorizontalScrollExtent() * @see #computeHorizontalScrollOffset() * @see android.widget.ScrollBarDrawable + * @hide */ - private void onDrawHorizontalScrollBar(Canvas canvas, ScrollBarDrawable scrollBar, int width, - int height, int size) { - - final int viewFlags = mViewFlags; - final int scrollX = mScrollX; - final int scrollY = mScrollY; - final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; - final int top = scrollY + height - size - (mUserPaddingBottom & inside); - - final int verticalScrollBarGap = - (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL ? - getVerticalScrollbarWidth() : 0; - - scrollBar.setBounds(scrollX + (mPaddingLeft & inside), top, - scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap, top + size); - scrollBar.setParameters( - computeHorizontalScrollRange(), - computeHorizontalScrollOffset(), - computeHorizontalScrollExtent(), false); + protected void onDrawHorizontalScrollBar(Canvas canvas, + Drawable scrollBar, + int l, int t, int r, int b) { + scrollBar.setBounds(l, t, r, b); scrollBar.draw(canvas); } @@ -5412,40 +5682,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility *

    Draw the vertical scrollbar if {@link #isVerticalScrollBarEnabled()} * returns true.

    * - *

    The length of the scrollbar and its thumb is computed according to the - * values returned by {@link #computeVerticalScrollRange()}, - * {@link #computeVerticalScrollExtent()} and - * {@link #computeVerticalScrollOffset()}. Refer to - * {@link android.widget.ScrollBarDrawable} for more information about how - * these values relate to each other.

    - * * @param canvas the canvas on which to draw the scrollbar * @param scrollBar the scrollbar's drawable - * @param width the width of the drawing surface - * @param height the height of the drawing surface - * @param size the size of the scrollbar * * @see #isVerticalScrollBarEnabled() * @see #computeVerticalScrollRange() * @see #computeVerticalScrollExtent() * @see #computeVerticalScrollOffset() * @see android.widget.ScrollBarDrawable + * @hide */ - private void onDrawVerticalScrollBar(Canvas canvas, ScrollBarDrawable scrollBar, int width, - int height, int size) { - - final int scrollX = mScrollX; - final int scrollY = mScrollY; - final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; - // TODO: Deal with RTL languages to position scrollbar on left - final int left = scrollX + width - size - (mUserPaddingRight & inside); - - scrollBar.setBounds(left, scrollY + (mPaddingTop & inside), - left + size, scrollY + height - (mUserPaddingBottom & inside)); - scrollBar.setParameters( - computeVerticalScrollRange(), - computeVerticalScrollOffset(), - computeVerticalScrollExtent(), true); + protected void onDrawVerticalScrollBar(Canvas canvas, + Drawable scrollBar, + int l, int t, int r, int b) { + scrollBar.setBounds(l, t, r, b); scrollBar.draw(canvas); } @@ -5936,11 +6186,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor; - final boolean opaque = drawingCacheBackgroundColor != 0 || - (mBGDrawable != null && mBGDrawable.getOpacity() == PixelFormat.OPAQUE); + final boolean opaque = drawingCacheBackgroundColor != 0 || isOpaque(); + final boolean translucentWindow = attachInfo != null && attachInfo.mTranslucentWindow; if (width <= 0 || height <= 0 || - (width * height * (opaque ? 2 : 4) > // Projected bitmap size in bytes + // Projected bitmap size in bytes + (width * height * (opaque && !translucentWindow ? 2 : 4) > ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize())) { destroyDrawingCache(); return; @@ -5951,7 +6202,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (mUnscaledDrawingCache == null ? null : mUnscaledDrawingCache.get()); if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { - Bitmap.Config quality; if (!opaque) { switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) { @@ -5969,7 +6219,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility break; } } else { - quality = Bitmap.Config.RGB_565; + // Optimization for translucent windows + // If the window is translucent, use a 32 bits bitmap to benefit from memcpy() + quality = translucentWindow ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; } // Try to cleanup memory @@ -5983,6 +6235,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } else { mUnscaledDrawingCache = new SoftReference(bitmap); } + if (opaque && translucentWindow) bitmap.setHasAlpha(false); } catch (OutOfMemoryError e) { // If there is not enough memory to create the bitmap cache, just // ignore the issue as bitmap caches are not required to draw the @@ -6057,16 +6310,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * some form of this public, but should think about the API. */ Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor) { - final int width = mRight - mLeft; - final int height = mBottom - mTop; + int width = mRight - mLeft; + int height = mBottom - mTop; - Bitmap bitmap = Bitmap.createBitmap(width, height, quality); + final AttachInfo attachInfo = mAttachInfo; + final float scale = attachInfo != null ? attachInfo.mApplicationScale : 1.0f; + width = (int) ((width * scale) + 0.5f); + height = (int) ((height * scale) + 0.5f); + + Bitmap bitmap = Bitmap.createBitmap(width > 0 ? width : 1, height > 0 ? height : 1, quality); if (bitmap == null) { throw new OutOfMemoryError(); } + bitmap.setDensity(getResources().getDisplayMetrics().densityDpi); + Canvas canvas; - final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { canvas = attachInfo.mCanvas; if (canvas == null) { @@ -6089,6 +6348,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility computeScroll(); final int restoreCount = canvas.save(); + canvas.scale(scale, scale); canvas.translate(-mScrollX, -mScrollY); // Temporarily remove the dirty mask @@ -6911,7 +7171,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Set the background to a given resource. The resource should refer to - * a Drawable object. + * a Drawable object or 0 to remove the background. * @param resid The identifier of the resource. * @attr ref android.R.styleable#View_background */ @@ -8086,6 +8346,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (flags&HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0); } + /** + * This needs to be a better API (NOT ON VIEW) before it is exposed. If + * it is ever exposed at all. + * @hide + */ + public void onCloseSystemDialogs(String reason) { + } + /** * Given a Drawable whose bounds have been set to draw into this view, * update a Region being computed for {@link #gatherTransparentRegion} so @@ -8553,6 +8821,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ int mWindowTop; + /** + * Indicates whether the window is translucent/transparent + */ + boolean mTranslucentWindow; + /** * For windows that are full-screen but using insets to layout inside * of the screen decorations, these are the current insets for the @@ -8584,6 +8857,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ final ArrayList mScrollContainers = new ArrayList(); + final KeyEvent.DispatcherState mKeyDispatchState + = new KeyEvent.DispatcherState(); + /** * Indicates whether the view's window currently has the focus. */ @@ -8709,21 +8985,62 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * is supported. This avoids keeping too many unused fields in most * instances of View.

    */ - private static class ScrollabilityCache { + private static class ScrollabilityCache implements Runnable { + + /** + * Scrollbars are not visible + */ + public static final int OFF = 0; + + /** + * Scrollbars are visible + */ + public static final int ON = 1; + + /** + * Scrollbars are fading away + */ + public static final int FADING = 2; + + public boolean fadeScrollBars; + public int fadingEdgeLength; + public int scrollBarDefaultDelayBeforeFade; + public int scrollBarFadeDuration; public int scrollBarSize; public ScrollBarDrawable scrollBar; + public float[] interpolatorValues; + public View host; public final Paint paint; public final Matrix matrix; public Shader shader; + public final Interpolator scrollBarInterpolator = new Interpolator(1, 2); + + private final float[] mOpaque = {255.0f}; + private final float[] mTransparent = {0.0f}; + + /** + * When fading should start. This time moves into the future every time + * a new scroll happens. Measured based on SystemClock.uptimeMillis() + */ + public long fadeStartTime; + + + /** + * The current state of the scrollbars: ON, OFF, or FADING + */ + public int state = OFF; + private int mLastColor; - public ScrollabilityCache(ViewConfiguration configuration) { + public ScrollabilityCache(ViewConfiguration configuration, View host) { fadingEdgeLength = configuration.getScaledFadingEdgeLength(); scrollBarSize = configuration.getScaledScrollBarSize(); + scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay(); + scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration(); paint = new Paint(); matrix = new Matrix(); @@ -8733,6 +9050,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + this.host = host; } public void setFadeColor(int color) { @@ -8740,12 +9058,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mLastColor = color; color |= 0xFF000000; - shader = new LinearGradient(0, 0, 0, 1, color, 0, Shader.TileMode.CLAMP); + shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000, + color & 0x00FFFFFF, Shader.TileMode.CLAMP); paint.setShader(shader); // Restore the default transfer mode (src_over) paint.setXfermode(null); } } + + public void run() { + long now = AnimationUtils.currentAnimationTimeMillis(); + if (now >= fadeStartTime) { + + // the animation fades the scrollbars out by changing + // the opacity (alpha) from fully opaque to fully + // transparent + int nextFrame = (int) now; + int framesCount = 0; + + Interpolator interpolator = scrollBarInterpolator; + + // Start opaque + interpolator.setKeyFrame(framesCount++, nextFrame, mOpaque); + + // End transparent + nextFrame += scrollBarFadeDuration; + interpolator.setKeyFrame(framesCount, nextFrame, mTransparent); + + state = FADING; + + // Kick off the fade animation + host.invalidate(); + } + } + } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 0e36ec296d9c9692f564fd8ae5fc5bd194b23f16..993048f5ecf2732084159a781aec113b36d01f55 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -30,6 +30,16 @@ public class ViewConfiguration { */ private static final int SCROLL_BAR_SIZE = 10; + /** + * Duration of the fade when scrollbars fade away in milliseconds + */ + private static final int SCROLL_BAR_FADE_DURATION = 250; + + /** + * Default delay before the scrollbars fade in milliseconds + */ + private static final int SCROLL_BAR_DEFAULT_DELAY = 300; + /** * Defines the length of the fading edges in pixels */ @@ -89,7 +99,7 @@ public class ViewConfiguration { /** * Distance a touch can wander before we think the user is scrolling in pixels */ - private static final int TOUCH_SLOP = 25; + private static final int TOUCH_SLOP = 16; /** * Distance between the first touch and second touch to still be considered a double tap @@ -220,6 +230,20 @@ public class ViewConfiguration { return mScrollbarSize; } + /** + * @return Duration of the fade when scrollbars fade away in milliseconds + */ + public static int getScrollBarFadeDuration() { + return SCROLL_BAR_FADE_DURATION; + } + + /** + * @return Default delay before the scrollbars fade in milliseconds + */ + public static int getScrollDefaultDelay() { + return SCROLL_BAR_DEFAULT_DELAY; + } + /** * @return the length of the fading edges in pixels * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f7b7f029df82e4e89429eb421b3531518f400ae2..e2f15c7b9e40e09e4374220a639ef4f56eef755e 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -150,6 +150,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * When set, the drawing method will call {@link #getChildDrawingOrder(int, int)} * to get the index of the child to draw for that iteration. + * + * @hide */ protected static final int FLAG_USE_CHILD_DRAWING_ORDER = 0x400; @@ -1307,11 +1309,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * if you want to change the drawing order of children. By default, it * returns i. *

    - * NOTE: In order for this method to be called, the - * {@link #FLAG_USE_CHILD_DRAWING_ORDER} must be set. + * NOTE: In order for this method to be called, you must enable child ordering + * first by calling {@link #setChildrenDrawingOrderEnabled(boolean)}. * * @param i The current iteration. * @return The index of the child to draw this iteration. + * + * @see #setChildrenDrawingOrderEnabled(boolean) + * @see #isChildrenDrawingOrderEnabled() */ protected int getChildDrawingOrder(int childCount, int i) { return i; @@ -2706,6 +2711,35 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager setBooleanFlag(FLAG_CHILDREN_DRAWN_WITH_CACHE, enabled); } + /** + * Indicates whether the ViewGroup is drawing its children in the order defined by + * {@link #getChildDrawingOrder(int, int)}. + * + * @return true if children drawing order is defined by {@link #getChildDrawingOrder(int, int)}, + * false otherwise + * + * @see #setChildrenDrawingOrderEnabled(boolean) + * @see #getChildDrawingOrder(int, int) + */ + @ViewDebug.ExportedProperty + protected boolean isChildrenDrawingOrderEnabled() { + return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER; + } + + /** + * Tells the ViewGroup whether to draw its children in the order defined by the method + * {@link #getChildDrawingOrder(int, int)}. + * + * @param enabled true if the order of the children when drawing is determined by + * {@link #getChildDrawingOrder(int, int)}, false otherwise + * + * @see #isChildrenDrawingOrderEnabled() + * @see #getChildDrawingOrder(int, int) + */ + protected void setChildrenDrawingOrderEnabled(boolean enabled) { + setBooleanFlag(FLAG_USE_CHILD_DRAWING_ORDER, enabled); + } + private void setBooleanFlag(int flag, boolean value) { if (value) { mGroupFlags |= flag; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 0c5d853ed4ddd37713f7dd3af38d6d3023929c01..bef3e580a0c5ad037a0b495979ecf567c5a80995 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -79,6 +79,9 @@ public final class ViewRoot extends Handler implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean WATCH_POINTER = false; + private static final boolean MEASURE_LATENCY = false; + private static LatencyTimer lt; + /** * Maximum time we allow the user to roll the trackball enough to generate * a key event, before resetting the counters. @@ -148,7 +151,8 @@ public final class ViewRoot extends Handler implements ViewParent, boolean mWindowAttributesChanged = false; // These can be accessed by any thread, must be protected with a lock. - Surface mSurface; + // Surface can never be reassigned or cleared (use Surface.clear()). + private final Surface mSurface = new Surface(); boolean mAdded; boolean mAddedTouchMode; @@ -188,18 +192,11 @@ public final class ViewRoot extends Handler implements ViewParent, private final int mDensity; - public ViewRoot(Context context) { - super(); - - ++sInstanceCount; - - // Initialize the statics when this class is first instantiated. This is - // done here instead of in the static block because Zygote does not - // allow the spawning of threads. + public static IWindowSession getWindowSession(Looper mainLooper) { synchronized (mStaticInit) { if (!mInitialized) { try { - InputMethodManager imm = InputMethodManager.getInstance(context); + InputMethodManager imm = InputMethodManager.getInstance(mainLooper); sWindowSession = IWindowManager.Stub.asInterface( ServiceManager.getService("window")) .openSession(imm.getClient(), imm.getInputContext()); @@ -207,8 +204,24 @@ public final class ViewRoot extends Handler implements ViewParent, } catch (RemoteException e) { } } + return sWindowSession; } + } + + public ViewRoot(Context context) { + super(); + + if (MEASURE_LATENCY && lt == null) { + lt = new LatencyTimer(100, 1000); + } + + ++sInstanceCount; + // Initialize the statics when this class is first instantiated. This is + // done here instead of in the static block because Zygote does not + // allow the spawning of threads. + getWindowSession(context.getMainLooper()); + mThread = Thread.currentThread(); mLocation = new WindowLeaked(null); mLocation.fillInStackTrace(); @@ -224,7 +237,6 @@ public final class ViewRoot extends Handler implements ViewParent, mTransparentRegion = new Region(); mPreviousTransparentRegion = new Region(); mFirst = true; // true for the first time the view is added - mSurface = new Surface(); mAdded = false; mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); mViewConfiguration = ViewConfiguration.get(context); @@ -396,7 +408,7 @@ public final class ViewRoot extends Handler implements ViewParent, } boolean restore = false; - if (attrs != null && mTranslator != null) { + if (mTranslator != null) { restore = true; attrs.backup(); mTranslator.translateWindowLayout(attrs); @@ -410,7 +422,7 @@ public final class ViewRoot extends Handler implements ViewParent, mSoftInputMode = attrs.softInputMode; mWindowAttributesChanged = true; mAttachInfo.mRootView = view; - mAttachInfo.mScalingRequired = mTranslator == null ? false : true; + mAttachInfo.mScalingRequired = mTranslator != null; mAttachInfo.mApplicationScale = mTranslator == null ? 1.0f : mTranslator.applicationScale; if (panelParentView != null) { @@ -668,13 +680,13 @@ public final class ViewRoot extends Handler implements ViewParent, // object is not initialized to its backing store, but soon it // will be (assuming the window is visible). attachInfo.mSurface = mSurface; + attachInfo.mTranslucentWindow = lp.format != PixelFormat.OPAQUE; attachInfo.mHasWindowFocus = false; attachInfo.mWindowVisibility = viewVisibility; attachInfo.mRecomputeGlobalAttributes = false; attachInfo.mKeepScreenOn = false; viewVisibilityChanged = false; host.dispatchAttachedToWindow(attachInfo, 0); - getRunQueue().executeActions(attachInfo.mHandler); //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn); } else { @@ -707,6 +719,10 @@ public final class ViewRoot extends Handler implements ViewParent, boolean insetsChanged = false; if (mLayoutRequested) { + // Execute enqueued actions on every layout in case a view that was detached + // enqueued an action after being detached + getRunQueue().executeActions(attachInfo.mHandler); + if (mFirst) { host.fitSystemWindows(mAttachInfo.mContentInsets); // make sure touch mode code executes by setting cached value @@ -882,6 +898,7 @@ public final class ViewRoot extends Handler implements ViewParent, // all at once. newSurface = true; fullRedrawNeeded = true; + mPreviousTransparentRegion.setEmpty(); if (mGlWanted && !mUseGL) { initializeGL(); @@ -1555,10 +1572,12 @@ public final class ViewRoot extends Handler implements ViewParent, mView = null; mAttachInfo.mRootView = null; + mAttachInfo.mSurface = null; if (mUseGL) { destroyGL(); } + mSurface.release(); try { sWindowSession.remove(mWindow); @@ -1593,6 +1612,7 @@ public final class ViewRoot extends Handler implements ViewParent, public final static int DISPATCH_KEY_FROM_IME = 1011; public final static int FINISH_INPUT_CONNECTION = 1012; public final static int CHECK_FOCUS = 1013; + public final static int CLOSE_SYSTEM_DIALOGS = 1014; @Override public void handleMessage(Message msg) { @@ -1628,16 +1648,24 @@ public final class ViewRoot extends Handler implements ViewParent, break; case DISPATCH_POINTER: { MotionEvent event = (MotionEvent)msg.obj; - - boolean didFinish; + boolean callWhenDone = msg.arg1 != 0; + if (event == null) { try { + long timeBeforeGettingEvents; + if (MEASURE_LATENCY) { + timeBeforeGettingEvents = System.nanoTime(); + } + event = sWindowSession.getPendingPointerMove(mWindow); + + if (MEASURE_LATENCY && event != null) { + lt.sample("9 Client got events ", System.nanoTime() - event.getEventTimeNano()); + lt.sample("8 Client getting events ", timeBeforeGettingEvents - event.getEventTimeNano()); + } } catch (RemoteException e) { } - didFinish = true; - } else { - didFinish = event.getAction() == MotionEvent.ACTION_OUTSIDE; + callWhenDone = false; } if (event != null && mTranslator != null) { mTranslator.translateEventInScreenToAppWindow(event); @@ -1654,8 +1682,16 @@ public final class ViewRoot extends Handler implements ViewParent, if(Config.LOGV) { captureMotionLog("captureDispatchPointer", event); } - event.offsetLocation(0, mCurScrollY); + if (mCurScrollY != 0) { + event.offsetLocation(0, mCurScrollY); + } + if (MEASURE_LATENCY) { + lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano()); + } handled = mView.dispatchTouchEvent(event); + if (MEASURE_LATENCY) { + lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano()); + } if (!handled && isDown) { int edgeSlop = mViewConfiguration.getScaledEdgeSlop(); @@ -1701,7 +1737,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } finally { - if (!didFinish) { + if (callWhenDone) { try { sWindowSession.finishKey(mWindow); } catch (RemoteException e) { @@ -1716,7 +1752,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } break; case DISPATCH_TRACKBALL: - deliverTrackballEvent((MotionEvent)msg.obj); + deliverTrackballEvent((MotionEvent)msg.obj, msg.arg1 != 0); break; case DISPATCH_APP_VISIBILITY: handleAppVisibility(msg.arg1 != 0); @@ -1779,6 +1815,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (hasWindowFocus && imm != null && mLastWasImTarget) { imm.startGettingWindowFocus(mView); } + mAttachInfo.mKeyDispatchState.reset(); mView.dispatchWindowFocusChanged(hasWindowFocus); } @@ -1806,7 +1843,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } break; case DIE: - dispatchDetachedFromWindow(); + doDie(); break; case DISPATCH_KEY_FROM_IME: { if (LOCAL_LOGV) Log.v( @@ -1833,6 +1870,11 @@ public final class ViewRoot extends Handler implements ViewParent, imm.checkFocus(); } } break; + case CLOSE_SYSTEM_DIALOGS: { + if (mView != null) { + mView.onCloseSystemDialogs((String)msg.obj); + } + } break; } } @@ -1958,16 +2000,13 @@ public final class ViewRoot extends Handler implements ViewParent, } - private void deliverTrackballEvent(MotionEvent event) { - boolean didFinish; + private void deliverTrackballEvent(MotionEvent event, boolean callWhenDone) { if (event == null) { try { event = sWindowSession.getPendingTrackballMove(mWindow); } catch (RemoteException e) { } - didFinish = true; - } else { - didFinish = false; + callWhenDone = false; } if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); @@ -1985,7 +2024,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } finally { if (handled) { - if (!didFinish) { + if (callWhenDone) { try { sWindowSession.finishKey(mWindow); } catch (RemoteException e) { @@ -2101,7 +2140,7 @@ public final class ViewRoot extends Handler implements ViewParent, mLastTrackballTime = curTime; } } finally { - if (!didFinish) { + if (callWhenDone) { try { sWindowSession.finishKey(mWindow); } catch (RemoteException e) { @@ -2483,6 +2522,14 @@ public final class ViewRoot extends Handler implements ViewParent, } public void die(boolean immediate) { + if (immediate) { + doDie(); + } else { + sendEmptyMessage(DIE); + } + } + + void doDie() { checkThread(); if (Config.LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface); synchronized (this) { @@ -2502,15 +2549,11 @@ public final class ViewRoot extends Handler implements ViewParent, } } - mSurface = null; + mSurface.release(); } if (mAdded) { mAdded = false; - if (immediate) { - dispatchDetachedFromWindow(); - } else if (mView != null) { - sendEmptyMessage(DIE); - } + dispatchDetachedFromWindow(); } } } @@ -2564,15 +2607,19 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessageAtTime(msg, event.getEventTime()); } - public void dispatchPointer(MotionEvent event, long eventTime) { + public void dispatchPointer(MotionEvent event, long eventTime, + boolean callWhenDone) { Message msg = obtainMessage(DISPATCH_POINTER); msg.obj = event; + msg.arg1 = callWhenDone ? 1 : 0; sendMessageAtTime(msg, eventTime); } - public void dispatchTrackball(MotionEvent event, long eventTime) { + public void dispatchTrackball(MotionEvent event, long eventTime, + boolean callWhenDone) { Message msg = obtainMessage(DISPATCH_TRACKBALL); msg.obj = event; + msg.arg1 = callWhenDone ? 1 : 0; sendMessageAtTime(msg, eventTime); } @@ -2595,6 +2642,13 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessage(msg); } + public void dispatchCloseSystemDialogs(String reason) { + Message msg = Message.obtain(); + msg.what = CLOSE_SYSTEM_DIALOGS; + msg.obj = reason; + sendMessage(msg); + } + /** * The window is getting focus so if there is anything focused/selected * send an {@link AccessibilityEvent} to announce that. @@ -2745,19 +2799,25 @@ public final class ViewRoot extends Handler implements ViewParent, } } - public void dispatchPointer(MotionEvent event, long eventTime) { + public void dispatchPointer(MotionEvent event, long eventTime, + boolean callWhenDone) { final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchPointer(event, eventTime); + if (viewRoot != null) { + if (MEASURE_LATENCY) { + // Note: eventTime is in milliseconds + ViewRoot.lt.sample("* ViewRoot b4 dispatchPtr", System.nanoTime() - eventTime * 1000000); + } + viewRoot.dispatchPointer(event, eventTime, callWhenDone); } else { new EventCompletion(mMainLooper, this, null, true, event); } } - public void dispatchTrackball(MotionEvent event, long eventTime) { + public void dispatchTrackball(MotionEvent event, long eventTime, + boolean callWhenDone) { final ViewRoot viewRoot = mViewRoot.get(); if (viewRoot != null) { - viewRoot.dispatchTrackball(event, eventTime); + viewRoot.dispatchTrackball(event, eventTime, callWhenDone); } else { new EventCompletion(mMainLooper, this, null, false, event); } @@ -2827,6 +2887,33 @@ public final class ViewRoot extends Handler implements ViewParent, } } } + + public void closeSystemDialogs(String reason) { + final ViewRoot viewRoot = mViewRoot.get(); + if (viewRoot != null) { + viewRoot.dispatchCloseSystemDialogs(reason); + } + } + + public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, + boolean sync) { + if (sync) { + try { + sWindowSession.wallpaperOffsetsComplete(asBinder()); + } catch (RemoteException e) { + } + } + } + + public void dispatchWallpaperCommand(String action, int x, int y, + int z, Bundle extras, boolean sync) { + if (sync) { + try { + sWindowSession.wallpaperCommandComplete(asBinder(), null); + } catch (RemoteException e) { + } + } + } } /** @@ -3107,7 +3194,7 @@ public final class ViewRoot extends Handler implements ViewParent, handler.postDelayed(handlerAction.action, handlerAction.delay); } - mActions.clear(); + actions.clear(); } } @@ -3121,7 +3208,6 @@ public final class ViewRoot extends Handler implements ViewParent, if (o == null || getClass() != o.getClass()) return false; HandlerAction that = (HandlerAction) o; - return !(action != null ? !action.equals(that.action) : that.action != null); } diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index e159de46bf18fc733da07e9a8d5ae69af6288fdf..703a38f299ca4fa9e5d78dbec64d947fb6cd9189 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -23,6 +23,8 @@ import android.util.AttributeSet; import com.android.internal.R; +import java.lang.ref.WeakReference; + /** * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate * layout resources at runtime. @@ -68,6 +70,8 @@ public final class ViewStub extends View { private int mLayoutResource = 0; private int mInflatedId; + private WeakReference mInflatedViewRef; + private OnInflateListener mInflateListener; public ViewStub(Context context) { @@ -196,9 +200,15 @@ public final class ViewStub extends View { */ @Override public void setVisibility(int visibility) { - super.setVisibility(visibility); - - if (visibility == VISIBLE || visibility == INVISIBLE) { + if (mInflatedViewRef != null) { + View view = mInflatedViewRef.get(); + if (view != null) { + view.setVisibility(visibility); + } else { + throw new IllegalStateException("setVisibility called on un-referenced view"); + } + } else if (visibility == VISIBLE || visibility == INVISIBLE) { + super.setVisibility(visibility); inflate(); } } @@ -234,6 +244,8 @@ public final class ViewStub extends View { parent.addView(view, index); } + mInflatedViewRef = new WeakReference(view); + if (mInflateListener != null) { mInflateListener.onInflate(this, view); } diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index a5739835321bbfef8ed720bb02038c374038155e..e21824e0647daf41c0663980f171888f69dd527a 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -23,7 +23,9 @@ import android.content.res.Resources; import android.media.AudioManager; import android.media.AudioService; import android.media.AudioSystem; +import android.media.RingtoneManager; import android.media.ToneGenerator; +import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.Vibrator; @@ -44,7 +46,7 @@ import android.widget.Toast; public class VolumePanel extends Handler { private static final String TAG = "VolumePanel"; - private static boolean LOGD = false || Config.LOGD; + private static boolean LOGD = false; /** * The delay before playing a sound. This small period exists so the user @@ -86,6 +88,7 @@ public class VolumePanel extends Handler protected Context mContext; private AudioManager mAudioManager; protected AudioService mAudioService; + private boolean mRingIsSilent; private final Toast mToast; private final View mView; @@ -138,7 +141,7 @@ public class VolumePanel extends Handler onShowVolumeChanged(streamType, flags); } - if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0) { + if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { removeMessages(MSG_PLAY_SOUND); sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); } @@ -157,6 +160,7 @@ public class VolumePanel extends Handler int index = mAudioService.getStreamVolume(streamType); int message = UNKNOWN_VOLUME_TEXT; int additionalMessage = 0; + mRingIsSilent = false; if (LOGD) { Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType @@ -169,8 +173,15 @@ public class VolumePanel extends Handler switch (streamType) { case AudioManager.STREAM_RING: { + setRingerIcon(); message = RINGTONE_VOLUME_TEXT; - setRingerIcon(index); + Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( + mContext, RingtoneManager.TYPE_RINGTONE); + if (ringuri == null) { + additionalMessage = + com.android.internal.R.string.volume_music_hint_silent_ringtone_selected; + mRingIsSilent = true; + } break; } @@ -208,6 +219,13 @@ public class VolumePanel extends Handler case AudioManager.STREAM_NOTIFICATION: { message = NOTIFICATION_VOLUME_TEXT; setSmallIcon(index); + Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( + mContext, RingtoneManager.TYPE_NOTIFICATION); + if (ringuri == null) { + additionalMessage = + com.android.internal.R.string.volume_music_hint_silent_ringtone_selected; + mRingIsSilent = true; + } break; } @@ -254,7 +272,6 @@ public class VolumePanel extends Handler mAudioService.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER)) { sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); } - } protected void onPlaySound(int streamType, int flags) { @@ -337,17 +354,15 @@ public class VolumePanel extends Handler /** * Makes the ringer icon visible with an icon that is chosen * based on the current ringer mode. - * - * @param index */ - private void setRingerIcon(int index) { + private void setRingerIcon() { mSmallStreamIcon.setVisibility(View.GONE); mLargeStreamIcon.setVisibility(View.VISIBLE); int ringerMode = mAudioService.getRingerMode(); int icon; - if (LOGD) Log.d(TAG, "setRingerIcon(index: " + index+ "), ringerMode: " + ringerMode); + if (LOGD) Log.d(TAG, "setRingerIcon(), ringerMode: " + ringerMode); if (ringerMode == AudioManager.RINGER_MODE_SILENT) { icon = com.android.internal.R.drawable.ic_volume_off; diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 576c8c1a84835e39d3a3e85e99b55c88b95079b9..1932765438f3140365ea020d44b51bf5c461721b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -56,11 +56,11 @@ public abstract class Window { public static final int FEATURE_CONTEXT_MENU = 6; /** Flag for custom title. You cannot combine this feature with other title features. */ public static final int FEATURE_CUSTOM_TITLE = 7; - /* Flag for asking for an OpenGL enabled window. + /** Flag for asking for an OpenGL enabled window. All 2D graphics will be handled by OpenGL ES. - Private for now, until it is better tested (not shipping in 1.0) + @hide */ - private static final int FEATURE_OPENGL = 8; + public static final int FEATURE_OPENGL = 8; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; /** Flag for setting the progress bar's visibility to GONE */ @@ -237,7 +237,6 @@ public abstract class Window { /** * This is called whenever the current window attributes change. * - */ public void onWindowAttributesChanged(WindowManager.LayoutParams attrs); @@ -252,12 +251,28 @@ public abstract class Window { public void onContentChanged(); /** - * This hook is called whenever the window focus changes. + * This hook is called whenever the window focus changes. See + * {@link View#onWindowFocusChanged(boolean) + * View.onWindowFocusChanged(boolean)} for more information. * * @param hasFocus Whether the window now has focus. */ public void onWindowFocusChanged(boolean hasFocus); + /** + * Called when the window has been attached to the window manager. + * See {@link View#onAttachedToWindow() View.onAttachedToWindow()} + * for more information. + */ + public void onAttachedToWindow(); + + /** + * Called when the window has been attached to the window manager. + * See {@link View#onDetachedFromWindow() View.onDetachedFromWindow()} + * for more information. + */ + public void onDetachedFromWindow(); + /** * Called when a panel is being closed. If another logical subsequent * panel is being opened (and this panel is being closed to make room for the subsequent diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index c0be9e8a3e2b0aecd566fcfeb293b1ae43114e52..6696533992cb2cb8176e5b082359a8656c4c5305 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -279,11 +279,6 @@ public interface WindowManager extends ViewManager { */ public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7; - /** - * Window type: panel that slides out from the status bar - */ - public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+8; - /** * Window type: panel that slides out from the status bar */ @@ -313,6 +308,17 @@ public interface WindowManager extends ViewManager { */ public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12; + /** + * Window type: wallpaper window, placed behind any window that wants + * to sit on top of the wallpaper. + */ + public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13; + + /** + * Window type: panel that slides out from the status bar + */ + public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14; + /** * End of types of system windows. */ @@ -323,8 +329,6 @@ public interface WindowManager extends ViewManager { * Default is normal. * * @see #MEMORY_TYPE_NORMAL - * @see #MEMORY_TYPE_HARDWARE - * @see #MEMORY_TYPE_GPU * @see #MEMORY_TYPE_PUSH_BUFFERS */ public int memoryType; @@ -332,10 +336,16 @@ public interface WindowManager extends ViewManager { /** Memory type: The window's surface is allocated in main memory. */ public static final int MEMORY_TYPE_NORMAL = 0; /** Memory type: The window's surface is configured to be accessible - * by DMA engines and hardware accelerators. */ + * by DMA engines and hardware accelerators. + * @deprecated this is ignored, this value is set automatically when needed. + */ + @Deprecated public static final int MEMORY_TYPE_HARDWARE = 1; /** Memory type: The window's surface is configured to be accessible - * by graphics accelerators. */ + * by graphics accelerators. + * @deprecated this is ignored, this value is set automatically when needed. + */ + @Deprecated public static final int MEMORY_TYPE_GPU = 2; /** Memory type: The window's surface doesn't own its buffers and * therefore cannot be locked. Instead the buffers are pushed to @@ -478,17 +488,53 @@ public interface WindowManager extends ViewManager { * is locked. This will let application windows take precedence over * key guard or any other lock screens. Can be used with * {@link #FLAG_KEEP_SCREEN_ON} to turn screen on and display windows - * directly before showing the key guard window - * - * {@hide} */ + * directly before showing the key guard window. Can be used with + * {@link #FLAG_DISMISS_KEYGUARD} to automatically fully dismisss + * non-secure keyguards. This flag only applies to the top-most + * full-screen window. + */ public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000; + /** Window flag: ask that the system wallpaper be shown behind + * your window. The window surface must be translucent to be able + * to actually see the wallpaper behind it; this flag just ensures + * that the wallpaper surface will be there if this window actually + * has translucent regions. + */ + public static final int FLAG_SHOW_WALLPAPER = 0x00100000; + + /** Window flag: when set as a window is being added or made + * visible, once the window has been shown then the system will + * poke the power manager's user activity (as if the user had woken + * up the device) to turn the screen on. */ + public static final int FLAG_TURN_SCREEN_ON = 0x00200000; + + /** Window flag: when set the window will cause the keyguard to + * be dismissed, only if it is not a secure lock keyguard. Because such + * a keyguard is not needed for security, it will never re-appear if + * the user navigates to another window (in contrast to + * {@link #FLAG_SHOW_WHEN_LOCKED}, which will only temporarily + * hide both secure and non-secure keyguards but ensure they reappear + * when the user moves to another UI that doesn't hide them). + * If the keyguard is currently active and is secure (requires an + * unlock pattern) than the user will still need to confirm it before + * seeing this window, unless {@link #FLAG_SHOW_WHEN_LOCKED} has + * also been set. */ + public static final int FLAG_DISMISS_KEYGUARD = 0x00400000; + + /** Window flag: *sigh* The lock screen wants to continue running its + * animation while it is fading. A kind-of hack to allow this. Maybe + * in the future we just make this the default behavior. + * + * {@hide} */ + public static final int FLAG_KEEP_SURFACE_WHILE_ANIMATING = 0x10000000; + /** Window flag: special flag to limit the size of the window to be * original size ([320x480] x density). Used to create window for applications * running under compatibility mode. * * {@hide} */ - public static final int FLAG_COMPATIBLE_WINDOW = 0x00100000; + public static final int FLAG_COMPATIBLE_WINDOW = 0x20000000; /** Window flag: a special option intended for system dialogs. When * this flag is set, the window will demand focus unconditionally when diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 13719323cb68ba62fbad8d7c6d4d7943a6fbfe3a..1ab46fc0a6ab5d8bf688bc078eb62259d836ab08 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -262,6 +262,13 @@ public interface WindowManagerPolicy { */ boolean isVisibleLw(); + /** + * Like {@link #isVisibleLw}, but also counts a window that is currently + * "hidden" behind the keyguard as visible. This allows us to apply + * things like window flags that impact the keyguard. + */ + boolean isVisibleOrBehindKeyguardLw(); + /** * Is this window currently visible to the user on-screen? It is * displayed either if it is visible or it is currently running an @@ -314,36 +321,60 @@ public interface WindowManagerPolicy { public boolean showLw(boolean doAnimation); } - /** No transition happening. */ + /** + * Bit mask that is set for all enter transition. + */ + public final int TRANSIT_ENTER_MASK = 0x1000; + + /** + * Bit mask that is set for all exit transitions. + */ + public final int TRANSIT_EXIT_MASK = 0x2000; + + /** Not set up for a transition. */ + public final int TRANSIT_UNSET = -1; + /** No animation for transition. */ public final int TRANSIT_NONE = 0; /** Window has been added to the screen. */ - public final int TRANSIT_ENTER = 1; + public final int TRANSIT_ENTER = 1 | TRANSIT_ENTER_MASK; /** Window has been removed from the screen. */ - public final int TRANSIT_EXIT = 2; + public final int TRANSIT_EXIT = 2 | TRANSIT_EXIT_MASK; /** Window has been made visible. */ - public final int TRANSIT_SHOW = 3; + public final int TRANSIT_SHOW = 3 | TRANSIT_ENTER_MASK; /** Window has been made invisible. */ - public final int TRANSIT_HIDE = 4; + public final int TRANSIT_HIDE = 4 | TRANSIT_EXIT_MASK; /** The "application starting" preview window is no longer needed, and will * animate away to show the real window. */ public final int TRANSIT_PREVIEW_DONE = 5; /** A window in a new activity is being opened on top of an existing one * in the same task. */ - public final int TRANSIT_ACTIVITY_OPEN = 6; + public final int TRANSIT_ACTIVITY_OPEN = 6 | TRANSIT_ENTER_MASK; /** The window in the top-most activity is being closed to reveal the * previous activity in the same task. */ - public final int TRANSIT_ACTIVITY_CLOSE = 7; + public final int TRANSIT_ACTIVITY_CLOSE = 7 | TRANSIT_EXIT_MASK; /** A window in a new task is being opened on top of an existing one * in another activity's task. */ - public final int TRANSIT_TASK_OPEN = 8; + public final int TRANSIT_TASK_OPEN = 8 | TRANSIT_ENTER_MASK; /** A window in the top-most activity is being closed to reveal the * previous activity in a different task. */ - public final int TRANSIT_TASK_CLOSE = 9; + public final int TRANSIT_TASK_CLOSE = 9 | TRANSIT_EXIT_MASK; /** A window in an existing task is being displayed on top of an existing one * in another activity's task. */ - public final int TRANSIT_TASK_TO_FRONT = 10; + public final int TRANSIT_TASK_TO_FRONT = 10 | TRANSIT_ENTER_MASK; /** A window in an existing task is being put below all other tasks. */ - public final int TRANSIT_TASK_TO_BACK = 11; + public final int TRANSIT_TASK_TO_BACK = 11 | TRANSIT_EXIT_MASK; + /** A window in a new activity that doesn't have a wallpaper is being + * opened on top of one that does, effectively closing the wallpaper. */ + public final int TRANSIT_WALLPAPER_CLOSE = 12 | TRANSIT_EXIT_MASK; + /** A window in a new activity that does have a wallpaper is being + * opened on one that didn't, effectively opening the wallpaper. */ + public final int TRANSIT_WALLPAPER_OPEN = 13 | TRANSIT_ENTER_MASK; + /** A window in a new activity is being opened on top of an existing one, + * and both are on top of the wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_OPEN = 14 | TRANSIT_ENTER_MASK; + /** The window in the top-most activity is being closed to reveal the + * previous activity, and both are on top of he wallpaper. */ + public final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15 | TRANSIT_EXIT_MASK; /** Screen turned off because of power button */ public final int OFF_BECAUSE_OF_USER = 1; @@ -423,6 +454,27 @@ public interface WindowManagerPolicy { */ public int subWindowTypeToLayerLw(int type); + /** + * Get the highest layer (actually one more than) that the wallpaper is + * allowed to be in. + */ + public int getMaxWallpaperLayer(); + + /** + * Return whether the given window should forcibly hide everything + * behind it. Typically returns true for the keyguard. + */ + public boolean doesForceHide(WindowState win, WindowManager.LayoutParams attrs); + + /** + * Determine if a window that is behind one that is force hiding + * (as determined by {@link #doesForceHide}) should actually be hidden. + * For example, typically returns false for the status bar. Be careful + * to return false for any window that you may hide yourself, since this + * will conflict with what you set. + */ + public boolean canBeForceHidden(WindowState win, WindowManager.LayoutParams attrs); + /** * Called when the system would like to show a UI to indicate that an * application is starting. You can use this to add a @@ -503,6 +555,11 @@ public interface WindowManagerPolicy { */ public int selectAnimationLw(WindowState win, int transit); + /** + * Create and return an animation to re-display a force hidden window. + */ + public Animation createForceHideEnterAnimation(); + /** * Called from the key queue thread before a key is dispatched to the * input thread. @@ -533,14 +590,15 @@ public interface WindowManagerPolicy { * @param win The window that currently has focus. This is where the key * event will normally go. * @param code Key code. - * @param metaKeys TODO + * @param metaKeys bit mask of meta keys that are held. * @param down Is this a key press (true) or release (false)? * @param repeatCount Number of times a key down has repeated. + * @param flags event's flags. * @return Returns true if the policy consumed the event and it should * not be further dispatched. */ public boolean interceptKeyTi(WindowState win, int code, - int metaKeys, boolean down, int repeatCount); + int metaKeys, boolean down, int repeatCount, int flags); /** * Called when layout of the windows is about to start. @@ -581,11 +639,18 @@ public interface WindowManagerPolicy { * returned, all windows given to layoutWindow() must have had a * frame assigned. * - * @return Return true if layout state may have changed (so that another - * layout will be performed). + * @return Return any bit set of {@link #FINISH_LAYOUT_REDO_LAYOUT} + * and {@link #FINISH_LAYOUT_REDO_CONFIG}. */ - public boolean finishLayoutLw(); + public int finishLayoutLw(); + /** Layout state may have changed (so another layout will be performed) */ + static final int FINISH_LAYOUT_REDO_LAYOUT = 0x0001; + /** Configuration state may have changed */ + static final int FINISH_LAYOUT_REDO_CONFIG = 0x0002; + /** Wallpaper may need to move */ + static final int FINISH_LAYOUT_REDO_WALLPAPER = 0x0004; + /** * Called when animation of the windows is about to start. * @@ -755,7 +820,7 @@ public interface WindowManagerPolicy { boolean displayEnabled); /** - * Called when the system is mostly done booting to dentermine whether + * Called when the system is mostly done booting to determine whether * the system should go into safe mode. */ public boolean detectSafeMode(); @@ -791,9 +856,21 @@ public interface WindowManagerPolicy { */ public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always); + /** + * A special function that is called from the very low-level input queue + * to provide feedback to the user. Currently only called for virtual + * keys. + */ + public void keyFeedbackFromInput(KeyEvent event); + /** * Called when we have stopped keeping the screen on because a window * requesting this is no longer visible. */ public void screenOnStoppedLw(); + + /** + * Return false to disable key repeat events from being generated. + */ + public boolean allowKeyRepeat(); } diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index a6627609e3be9d929b073ad9983ddab8eefba748..c8396c4d49799cae86e29204bf75426f55a17263 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -175,6 +175,11 @@ public abstract class Animation implements Cloneable { */ private int mZAdjustment; + /** + * Don't animate the wallpaper. + */ + private boolean mDetachWallpaper = false; + private boolean mMore = true; private boolean mOneMoreTime = true; @@ -218,6 +223,8 @@ public abstract class Animation implements Cloneable { setZAdjustment(a.getInt(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL)); + setDetachWallpaper(a.getBoolean(com.android.internal.R.styleable.Animation_detachWallpaper, false)); + ensureInterpolator(); a.recycle(); @@ -319,7 +326,7 @@ public abstract class Animation implements Cloneable { * * @param durationMillis Duration in milliseconds * - * @throw java.lang.IllegalArgumentException if the duration is < 0 + * @throws java.lang.IllegalArgumentException if the duration is < 0 * * @attr ref android.R.styleable#Animation_duration */ @@ -514,6 +521,19 @@ public abstract class Animation implements Cloneable { mZAdjustment = zAdjustment; } + /** + * If detachWallpaper is true, and this is a window animation of a window + * that has a wallpaper background, then the window will be detached from + * the wallpaper while it runs. That is, the animation will only be applied + * to the window, and the wallpaper behind it will remain static. + * + * @param detachWallpaper true if the wallpaper should be detached from the animation + * @attr ref android.R.styleable#Animation_detachWallpaper + */ + public void setDetachWallpaper(boolean detachWallpaper) { + mDetachWallpaper = detachWallpaper; + } + /** * Gets the acceleration curve type for this animation. * @@ -610,6 +630,14 @@ public abstract class Animation implements Cloneable { return mZAdjustment; } + /** + * Return value of {@link #setDetachWallpaper(boolean)}. + * @attr ref android.R.styleable#Animation_detachWallpaper + */ + public boolean getDetachWallpaper() { + return mDetachWallpaper; + } + /** *

    Indicates whether or not this animation will affect the transformation * matrix. For instance, a fade animation will not affect the matrix whereas diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index b4c5b72f33eb563d0956913c3f700fc5452760b1..316bcd614c46f498a76391ba6bc3c8bb68c448f0 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -40,7 +40,7 @@ import java.io.IOException; * This class is used to specify meta information of an input method. */ public final class InputMethodInfo implements Parcelable { - static final String TAG = "InputMethodMetaInfo"; + static final String TAG = "InputMethodInfo"; /** * The Service that implements this input method component. @@ -244,7 +244,7 @@ public final class InputMethodInfo implements Parcelable { @Override public String toString() { - return "InputMethodMetaInfo{" + mId + return "InputMethodInfo{" + mId + ", settings: " + mSettingsActivityName + "}"; } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index d79789059ca89be8e9bd697896fbffe881a6ebbb..e30687f3327ba7ba5bf79d12f2bfcea1e0eb1fd3 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -446,13 +446,22 @@ public final class InputMethodManager { * @hide */ static public InputMethodManager getInstance(Context context) { + return getInstance(context.getMainLooper()); + } + + /** + * Internally, the input method manager can't be context-dependent, so + * we have this here for the places that need it. + * @hide + */ + static public InputMethodManager getInstance(Looper mainLooper) { synchronized (mInstanceSync) { if (mInstance != null) { return mInstance; } IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE); IInputMethodManager service = IInputMethodManager.Stub.asInterface(b); - mInstance = new InputMethodManager(service, context.getMainLooper()); + mInstance = new InputMethodManager(service, mainLooper); } return mInstance; } diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index dbd268291aa22a19fc1cb074dfddaa468a1af126..9456ae14ca31c6167fe19d55eb26e43aece72199 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -16,6 +16,7 @@ package android.webkit; +import android.app.ActivityManager; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; @@ -31,6 +32,7 @@ import junit.framework.Assert; import java.net.URLEncoder; import java.util.HashMap; +import java.util.Map; import java.util.Iterator; class BrowserFrame extends Handler { @@ -59,7 +61,7 @@ class BrowserFrame extends Handler { private boolean mIsMainFrame; // Attached Javascript interfaces - private HashMap mJSInterfaceMap; + private Map mJSInterfaceMap; // message ids // a message posted when a frame loading is completed @@ -98,20 +100,27 @@ class BrowserFrame extends Handler { * XXX: Called by WebCore thread. */ public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy, - WebSettings settings) { + WebSettings settings, Map javascriptInterfaces) { // Create a global JWebCoreJavaBridge to handle timers and // cookies in the WebCore thread. if (sJavaBridge == null) { - sJavaBridge = new JWebCoreJavaBridge(); + sJavaBridge = new JWebCoreJavaBridge(context); // set WebCore native cache size - sJavaBridge.setCacheSize(4 * 1024 * 1024); + ActivityManager am = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + if (am.getMemoryClass() > 16) { + sJavaBridge.setCacheSize(8 * 1024 * 1024); + } else { + sJavaBridge.setCacheSize(4 * 1024 * 1024); + } // initialize CacheManager CacheManager.init(context); // create CookieSyncManager with current Context CookieSyncManager.createInstance(context); + // create PluginManager with current Context + PluginManager.getInstance(context); } - AssetManager am = context.getAssets(); - nativeCreateFrame(w, am, proxy.getBackForwardList()); + mJSInterfaceMap = javascriptInterfaces; mSettings = settings; mContext = context; @@ -119,7 +128,10 @@ class BrowserFrame extends Handler { mDatabase = WebViewDatabase.getInstance(context); mWebViewCore = w; - if (WebView.LOGV_ENABLED) { + AssetManager am = context.getAssets(); + nativeCreateFrame(w, am, proxy.getBackForwardList()); + + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); } } @@ -217,7 +229,6 @@ class BrowserFrame extends Handler { private void resetLoadingStates() { mCommitted = true; - mWebViewCore.mEndScaleZoom = mFirstLayoutDone == false; mFirstLayoutDone = true; } @@ -240,7 +251,6 @@ class BrowserFrame extends Handler { // blocking the update in {@link #loadStarted} mWebViewCore.contentDraw(); } - mWebViewCore.mEndScaleZoom = true; } /** @@ -341,17 +351,16 @@ class BrowserFrame extends Handler { switch (msg.what) { case FRAME_COMPLETED: { if (mSettings.getSavePassword() && hasPasswordField()) { - if (WebView.DEBUG) { - Assert.assertNotNull(mCallbackProxy.getBackForwardList() - .getCurrentItem()); - } - WebAddress uri = new WebAddress( - mCallbackProxy.getBackForwardList().getCurrentItem() - .getUrl()); - String schemePlusHost = uri.mScheme + uri.mHost; - String[] up = mDatabase.getUsernamePassword(schemePlusHost); - if (up != null && up[0] != null) { - setUsernamePassword(up[0], up[1]); + WebHistoryItem item = mCallbackProxy.getBackForwardList() + .getCurrentItem(); + if (item != null) { + WebAddress uri = new WebAddress(item.getUrl()); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] up = + mDatabase.getUsernamePassword(schemePlusHost); + if (up != null && up[0] != null) { + setUsernamePassword(up[0], up[1]); + } } } CacheManager.trimCacheIfNeeded(); @@ -463,8 +472,6 @@ class BrowserFrame extends Handler { * @param postData If the method is "POST" postData is sent as the request * body. Is null when empty. * @param cacheMode The cache mode to use when loading this resource. - * @param isHighPriority True if this resource needs to be put at the front - * of the network queue. * @param synchronous True if the load is synchronous. * @return A newly created LoadListener object. */ @@ -474,7 +481,6 @@ class BrowserFrame extends Handler { HashMap headers, byte[] postData, int cacheMode, - boolean isHighPriority, boolean synchronous) { PerfChecker checker = new PerfChecker(); @@ -490,7 +496,7 @@ class BrowserFrame extends Handler { } if (mSettings.getSavePassword() && hasPasswordField()) { try { - if (WebView.DEBUG) { + if (DebugFlags.BROWSER_FRAME) { Assert.assertNotNull(mCallbackProxy.getBackForwardList() .getCurrentItem()); } @@ -538,10 +544,10 @@ class BrowserFrame extends Handler { // is this resource the main-frame top-level page? boolean isMainFramePage = mIsMainFrame; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" - + method + ", postData=" + postData + ", isHighPriority=" - + isHighPriority + ", isMainFramePage=" + isMainFramePage); + + method + ", postData=" + postData + ", isMainFramePage=" + + isMainFramePage); } // Create a LoadListener @@ -551,23 +557,17 @@ class BrowserFrame extends Handler { mCallbackProxy.onLoadResource(url); if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) { + // send an error message, so that loadListener can be deleted + // after this is returned. This is important as LoadListener's + // nativeError will remove the request from its DocLoader's request + // list. But the set up is not done until this method is returned. loadListener.error( android.net.http.EventHandler.ERROR, mContext.getString( com.android.internal.R.string.httpErrorTooManyRequests)); - loadListener.notifyError(); - loadListener.tearDown(); - return null; + return loadListener; } - // during synchronous load, the WebViewCore thread is blocked, so we - // need to endCacheTransaction first so that http thread won't be - // blocked in setupFile() when createCacheFile. - if (synchronous) { - CacheManager.endCacheTransaction(); - } - - FrameLoader loader = new FrameLoader(loadListener, mSettings, - method, isHighPriority); + FrameLoader loader = new FrameLoader(loadListener, mSettings, method); loader.setHeaders(headers); loader.setPostData(postData); // Set the load mode to the mode used for the current page. @@ -581,10 +581,6 @@ class BrowserFrame extends Handler { } checker.responseAlert("startLoadingResource succeed"); - if (synchronous) { - CacheManager.startCacheTransaction(); - } - return !synchronous ? loadListener : null; } @@ -615,6 +611,11 @@ class BrowserFrame extends Handler { mCallbackProxy.onReceivedIcon(icon); } + // Called by JNI when an apple-touch-icon attribute was found. + private void didReceiveTouchIconUrl(String url, boolean precomposed) { + mCallbackProxy.onReceivedTouchIconUrl(url, precomposed); + } + /** * Request a new window from the client. * @return The BrowserFrame object stored in the new WebView. @@ -677,6 +678,7 @@ class BrowserFrame extends Handler { // these ids need to be in sync with enum RAW_RES_ID in WebFrame private static final int NODOMAIN = 1; private static final int LOADERROR = 2; + private static final int DRAWABLEDIR = 3; String getRawResFilename(int id) { int resid; @@ -689,15 +691,33 @@ class BrowserFrame extends Handler { resid = com.android.internal.R.raw.loaderror; break; + case DRAWABLEDIR: + // use one known resource to find the drawable directory + resid = com.android.internal.R.drawable.btn_check_off; + break; + default: Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); - return new String(); + return ""; } TypedValue value = new TypedValue(); mContext.getResources().getValue(resid, value, true); + if (id == DRAWABLEDIR) { + String path = value.string.toString(); + int index = path.lastIndexOf('/'); + if (index < 0) { + Log.e(LOGTAG, "Can't find drawable directory."); + return ""; + } + return path.substring(0, index + 1); + } return value.string.toString(); } + private float density() { + return mContext.getResources().getDisplayMetrics().density; + } + //========================================================================== // native functions //========================================================================== diff --git a/core/java/android/webkit/CacheLoader.java b/core/java/android/webkit/CacheLoader.java index 3e1b602221c8cf93100129cd819c3c42ac3a10a8..de8f888e329fa371dd100820eea87abfec56714d 100644 --- a/core/java/android/webkit/CacheLoader.java +++ b/core/java/android/webkit/CacheLoader.java @@ -17,6 +17,7 @@ package android.webkit; import android.net.http.Headers; +import android.text.TextUtils; /** * This class is a concrete implementation of StreamLoader that uses a @@ -49,17 +50,22 @@ class CacheLoader extends StreamLoader { @Override protected void buildHeaders(Headers headers) { StringBuilder sb = new StringBuilder(mCacheResult.mimeType); - if (mCacheResult.encoding != null && - mCacheResult.encoding.length() > 0) { + if (!TextUtils.isEmpty(mCacheResult.encoding)) { sb.append(';'); sb.append(mCacheResult.encoding); } headers.setContentType(sb.toString()); - if (mCacheResult.location != null && - mCacheResult.location.length() > 0) { + if (!TextUtils.isEmpty(mCacheResult.location)) { headers.setLocation(mCacheResult.location); } - } + if (!TextUtils.isEmpty(mCacheResult.expiresString)) { + headers.setExpires(mCacheResult.expiresString); + } + + if (!TextUtils.isEmpty(mCacheResult.contentdisposition)) { + headers.setContentDisposition(mCacheResult.contentdisposition); + } + } } diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 7897435df2fc2e1c233b4c0f25122d12338375aa..75028deac943e1bd43885588f2ef5ed62e4b8302 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -51,7 +51,6 @@ public final class CacheManager { private static final String NO_STORE = "no-store"; private static final String NO_CACHE = "no-cache"; - private static final String PRIVATE = "private"; private static final String MAX_AGE = "max-age"; private static long CACHE_THRESHOLD = 6 * 1024 * 1024; @@ -80,12 +79,14 @@ public final class CacheManager { int httpStatusCode; long contentLength; long expires; + String expiresString; String localPath; String lastModified; String etag; String mimeType; String location; String encoding; + String contentdisposition; // these fields are NOT saved to the database InputStream inStream; @@ -108,6 +109,10 @@ public final class CacheManager { return expires; } + public String getExpiresString() { + return expiresString; + } + public String getLastModified() { return lastModified; } @@ -128,6 +133,10 @@ public final class CacheManager { return encoding; } + public String getContentDisposition() { + return contentdisposition; + } + // For out-of-package access to the underlying streams. public InputStream getInputStream() { return inStream; @@ -321,7 +330,7 @@ public final class CacheManager { } } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "getCacheFile for url " + url); } @@ -340,7 +349,7 @@ public final class CacheManager { * @hide - hide createCacheFile since it has a parameter of type headers, which is * in a hidden package. */ - // can be called from any thread + // only called from WebCore thread public static CacheResult createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache) { if (!forceCache && mDisabled) { @@ -349,17 +358,25 @@ public final class CacheManager { // according to the rfc 2616, the 303 response MUST NOT be cached. if (statusCode == 303) { + // remove the saved cache if there is any + mDataBase.removeCache(url); return null; } // like the other browsers, do not cache redirects containing a cookie // header. if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) { + // remove the saved cache if there is any + mDataBase.removeCache(url); return null; } CacheResult ret = parseHeaders(statusCode, headers, mimeType); - if (ret != null) { + if (ret == null) { + // this should only happen if the headers has "no-store" in the + // cache-control. remove the saved cache if there is any + mDataBase.removeCache(url); + } else { setupFiles(url, ret); try { ret.outStream = new FileOutputStream(ret.outFile); @@ -403,19 +420,23 @@ public final class CacheManager { } cacheRet.contentLength = cacheRet.outFile.length(); - if (checkCacheRedirect(cacheRet.httpStatusCode)) { + boolean redirect = checkCacheRedirect(cacheRet.httpStatusCode); + if (redirect) { // location is in database, no need to keep the file cacheRet.contentLength = 0; - cacheRet.localPath = new String(); - cacheRet.outFile.delete(); - } else if (cacheRet.contentLength == 0) { - cacheRet.outFile.delete(); + cacheRet.localPath = ""; + } + if ((redirect || cacheRet.contentLength == 0) + && !cacheRet.outFile.delete()) { + Log.e(LOGTAG, cacheRet.outFile.getPath() + " delete failed."); + } + if (cacheRet.contentLength == 0) { return; } mDataBase.addCache(url, cacheRet); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "saveCacheFile for url " + url); } } @@ -444,7 +465,10 @@ public final class CacheManager { // if mBaseDir doesn't exist, files can be null. if (files != null) { for (int i = 0; i < files.length; i++) { - new File(mBaseDir, files[i]).delete(); + File f = new File(mBaseDir, files[i]); + if (!f.delete()) { + Log.e(LOGTAG, f.getPath() + " delete failed."); + } } } } catch (SecurityException e) { @@ -472,7 +496,10 @@ public final class CacheManager { ArrayList pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT); int size = pathList.size(); for (int i = 0; i < size; i++) { - new File(mBaseDir, pathList.get(i)).delete(); + File f = new File(mBaseDir, pathList.get(i)); + if (!f.delete()) { + Log.e(LOGTAG, f.getPath() + " delete failed."); + } } } } @@ -511,12 +538,7 @@ public final class CacheManager { // cache file. If it is not, resolve the collision. while (file.exists()) { if (checkOldPath) { - // as this is called from http thread through - // createCacheFile, we need endCacheTransaction before - // database access. - WebViewCore.endCacheTransaction(); CacheResult oldResult = mDataBase.getCache(url); - WebViewCore.startCacheTransaction(); if (oldResult != null && oldResult.contentLength > 0) { if (path.equals(oldResult.localPath)) { path = oldResult.localPath; @@ -596,21 +618,27 @@ public final class CacheManager { if (location != null) ret.location = location; ret.expires = -1; - String expires = headers.getExpires(); - if (expires != null) { + ret.expiresString = headers.getExpires(); + if (ret.expiresString != null) { try { - ret.expires = HttpDateTime.parse(expires); + ret.expires = HttpDateTime.parse(ret.expiresString); } catch (IllegalArgumentException ex) { // Take care of the special "-1" and "0" cases - if ("-1".equals(expires) || "0".equals(expires)) { + if ("-1".equals(ret.expiresString) + || "0".equals(ret.expiresString)) { // make it expired, but can be used for history navigation ret.expires = 0; } else { - Log.e(LOGTAG, "illegal expires: " + expires); + Log.e(LOGTAG, "illegal expires: " + ret.expiresString); } } } + String contentDisposition = headers.getContentDisposition(); + if (contentDisposition != null) { + ret.contentdisposition = contentDisposition; + } + String lastModified = headers.getLastModified(); if (lastModified != null) ret.lastModified = lastModified; @@ -628,7 +656,7 @@ public final class CacheManager { // must be re-validated on every load. It does not mean that // the content can not be cached. set to expire 0 means it // can only be used in CACHE_MODE_CACHE_ONLY case - if (NO_CACHE.equals(controls[i]) || PRIVATE.equals(controls[i])) { + if (NO_CACHE.equals(controls[i])) { ret.expires = 0; } else if (controls[i].startsWith(MAX_AGE)) { int separator = controls[i].indexOf('='); diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 17d3f9407f337ecb3a1e3b35d523d5b2c2b118d4..f760b61f4715c2ee1900f829b8b70e34b99d704f 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -65,50 +65,62 @@ class CallbackProxy extends Handler { // Keep track of multiple progress updates. private boolean mProgressUpdatePending; // Keep track of the last progress amount. - private volatile int mLatestProgress; + // Start with 100 to indicate it is not in load for the empty page. + private volatile int mLatestProgress = 100; // Back/Forward list private final WebBackForwardList mBackForwardList; // Used to call startActivity during url override. private final Context mContext; // Message Ids - private static final int PAGE_STARTED = 100; - private static final int RECEIVED_ICON = 101; - private static final int RECEIVED_TITLE = 102; - private static final int OVERRIDE_URL = 103; - private static final int AUTH_REQUEST = 104; - private static final int SSL_ERROR = 105; - private static final int PROGRESS = 106; - private static final int UPDATE_VISITED = 107; - private static final int LOAD_RESOURCE = 108; - private static final int CREATE_WINDOW = 109; - private static final int CLOSE_WINDOW = 110; - private static final int SAVE_PASSWORD = 111; - private static final int JS_ALERT = 112; - private static final int JS_CONFIRM = 113; - private static final int JS_PROMPT = 114; - private static final int JS_UNLOAD = 115; - private static final int ASYNC_KEYEVENTS = 116; - private static final int TOO_MANY_REDIRECTS = 117; - private static final int DOWNLOAD_FILE = 118; - private static final int REPORT_ERROR = 119; - private static final int RESEND_POST_DATA = 120; - private static final int PAGE_FINISHED = 121; - private static final int REQUEST_FOCUS = 122; - private static final int SCALE_CHANGED = 123; - private static final int RECEIVED_CERTIFICATE = 124; - private static final int SWITCH_OUT_HISTORY = 125; - private static final int JS_TIMEOUT = 126; + private static final int PAGE_STARTED = 100; + private static final int RECEIVED_ICON = 101; + private static final int RECEIVED_TITLE = 102; + private static final int OVERRIDE_URL = 103; + private static final int AUTH_REQUEST = 104; + private static final int SSL_ERROR = 105; + private static final int PROGRESS = 106; + private static final int UPDATE_VISITED = 107; + private static final int LOAD_RESOURCE = 108; + private static final int CREATE_WINDOW = 109; + private static final int CLOSE_WINDOW = 110; + private static final int SAVE_PASSWORD = 111; + private static final int JS_ALERT = 112; + private static final int JS_CONFIRM = 113; + private static final int JS_PROMPT = 114; + private static final int JS_UNLOAD = 115; + private static final int ASYNC_KEYEVENTS = 116; + private static final int TOO_MANY_REDIRECTS = 117; + private static final int DOWNLOAD_FILE = 118; + private static final int REPORT_ERROR = 119; + private static final int RESEND_POST_DATA = 120; + private static final int PAGE_FINISHED = 121; + private static final int REQUEST_FOCUS = 122; + private static final int SCALE_CHANGED = 123; + private static final int RECEIVED_CERTIFICATE = 124; + private static final int SWITCH_OUT_HISTORY = 125; + private static final int EXCEEDED_DATABASE_QUOTA = 126; + private static final int REACHED_APPCACHE_MAXSIZE = 127; + private static final int JS_TIMEOUT = 128; + private static final int ADD_MESSAGE_TO_CONSOLE = 129; + private static final int GEOLOCATION_PERMISSIONS_SHOW_PROMPT = 130; + private static final int GEOLOCATION_PERMISSIONS_HIDE_PROMPT = 131; + private static final int RECEIVED_TOUCH_ICON_URL = 132; + private static final int GET_VISITED_HISTORY = 133; // Message triggered by the client to resume execution - private static final int NOTIFY = 200; + private static final int NOTIFY = 200; // Result transportation object for returning results across thread // boundaries. - private class ResultTransport { + private static class ResultTransport { // Private result object private E mResult; + public ResultTransport(E defaultResult) { + mResult = defaultResult; + } + public synchronized void setResult(E result) { mResult = result; } @@ -144,6 +156,14 @@ class CallbackProxy extends Handler { mWebChromeClient = client; } + /** + * Get the WebChromeClient. + * @return the current WebChromeClient instance. + */ + public WebChromeClient getWebChromeClient() { + return mWebChromeClient; + } + /** * Set the client DownloadListener. * @param client An implementation of DownloadListener. @@ -229,6 +249,13 @@ class CallbackProxy extends Handler { } break; + case RECEIVED_TOUCH_ICON_URL: + if (mWebChromeClient != null) { + mWebChromeClient.onReceivedTouchIconUrl(mWebView, + (String) msg.obj, msg.arg1 == 1); + } + break; + case RECEIVED_TITLE: if (mWebChromeClient != null) { mWebChromeClient.onReceivedTitle(mWebView, @@ -389,6 +416,63 @@ class CallbackProxy extends Handler { } break; + case EXCEEDED_DATABASE_QUOTA: + if (mWebChromeClient != null) { + HashMap map = + (HashMap) msg.obj; + String databaseIdentifier = + (String) map.get("databaseIdentifier"); + String url = (String) map.get("url"); + long currentQuota = + ((Long) map.get("currentQuota")).longValue(); + long totalUsedQuota = + ((Long) map.get("totalUsedQuota")).longValue(); + long estimatedSize = + ((Long) map.get("estimatedSize")).longValue(); + WebStorage.QuotaUpdater quotaUpdater = + (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + + mWebChromeClient.onExceededDatabaseQuota(url, + databaseIdentifier, currentQuota, estimatedSize, + totalUsedQuota, quotaUpdater); + } + break; + + case REACHED_APPCACHE_MAXSIZE: + if (mWebChromeClient != null) { + HashMap map = + (HashMap) msg.obj; + long spaceNeeded = + ((Long) map.get("spaceNeeded")).longValue(); + long totalUsedQuota = + ((Long) map.get("totalUsedQuota")).longValue(); + WebStorage.QuotaUpdater quotaUpdater = + (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + + mWebChromeClient.onReachedMaxAppCacheSize(spaceNeeded, + totalUsedQuota, quotaUpdater); + } + break; + + case GEOLOCATION_PERMISSIONS_SHOW_PROMPT: + if (mWebChromeClient != null) { + HashMap map = + (HashMap) msg.obj; + String origin = (String) map.get("origin"); + GeolocationPermissions.Callback callback = + (GeolocationPermissions.Callback) + map.get("callback"); + mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, + callback); + } + break; + + case GEOLOCATION_PERMISSIONS_HIDE_PROMPT: + if (mWebChromeClient != null) { + mWebChromeClient.onGeolocationPermissionsHidePrompt(); + } + break; + case JS_ALERT: if (mWebChromeClient != null) { final JsResult res = (JsResult) msg.obj; @@ -563,6 +647,19 @@ class CallbackProxy extends Handler { case SWITCH_OUT_HISTORY: mWebView.switchOutDrawHistory(); break; + + case ADD_MESSAGE_TO_CONSOLE: + String message = msg.getData().getString("message"); + String sourceID = msg.getData().getString("sourceID"); + int lineNumber = msg.getData().getInt("lineNumber"); + mWebChromeClient.addMessageToConsole(message, lineNumber, sourceID); + break; + + case GET_VISITED_HISTORY: + if (mWebChromeClient != null) { + mWebChromeClient.getVisitedHistory((ValueCallback)msg.obj); + } + break; } } @@ -605,7 +702,40 @@ class CallbackProxy extends Handler { //-------------------------------------------------------------------------- // Performance probe + private static final boolean PERF_PROBE = false; private long mWebCoreThreadTime; + private long mWebCoreIdleTime; + + /* + * If PERF_PROBE is true, this block needs to be added to MessageQueue.java. + * startWait() and finishWait() should be called before and after wait(). + + private WaitCallback mWaitCallback = null; + public static interface WaitCallback { + void startWait(); + void finishWait(); + } + public final void setWaitCallback(WaitCallback callback) { + mWaitCallback = callback; + } + */ + + // un-comment this block if PERF_PROBE is true + /* + private IdleCallback mIdleCallback = new IdleCallback(); + + private final class IdleCallback implements MessageQueue.WaitCallback { + private long mStartTime = 0; + + public void finishWait() { + mWebCoreIdleTime += SystemClock.uptimeMillis() - mStartTime; + } + + public void startWait() { + mStartTime = SystemClock.uptimeMillis(); + } + } + */ public void onPageStarted(String url, Bitmap favicon) { // Do an unsynchronized quick check to avoid posting if no callback has @@ -614,9 +744,12 @@ class CallbackProxy extends Handler { return; } // Performance probe - if (false) { + if (PERF_PROBE) { mWebCoreThreadTime = SystemClock.currentThreadTimeMillis(); + mWebCoreIdleTime = 0; Network.getInstance(mContext).startTiming(); + // un-comment this if PERF_PROBE is true +// Looper.myQueue().setWaitCallback(mIdleCallback); } Message msg = obtainMessage(PAGE_STARTED); msg.obj = favicon; @@ -631,10 +764,12 @@ class CallbackProxy extends Handler { return; } // Performance probe - if (false) { + if (PERF_PROBE) { + // un-comment this if PERF_PROBE is true +// Looper.myQueue().setWaitCallback(null); Log.d("WebCore", "WebCore thread used " + (SystemClock.currentThreadTimeMillis() - mWebCoreThreadTime) - + " ms"); + + " ms and idled " + mWebCoreIdleTime + " ms"); Network.getInstance(mContext).stopTiming(); } Message msg = obtainMessage(PAGE_FINISHED, url); @@ -693,7 +828,7 @@ class CallbackProxy extends Handler { public boolean shouldOverrideUrlLoading(String url) { // We have a default behavior if no client exists so always send the // message. - ResultTransport res = new ResultTransport(); + ResultTransport res = new ResultTransport(false); Message msg = obtainMessage(OVERRIDE_URL); msg.getData().putString("url", url); msg.obj = res; @@ -834,7 +969,7 @@ class CallbackProxy extends Handler { String password, Message resumeMsg) { // resumeMsg should be null at this point because we want to create it // within the CallbackProxy. - if (WebView.DEBUG) { + if (DebugFlags.CALLBACK_PROXY) { junit.framework.Assert.assertNull(resumeMsg); } resumeMsg = obtainMessage(NOTIFY); @@ -939,6 +1074,24 @@ class CallbackProxy extends Handler { sendMessage(obtainMessage(RECEIVED_ICON, icon)); } + /* package */ void onReceivedTouchIconUrl(String url, boolean precomposed) { + // We should have a current item but we do not want to crash so check + // for null. + WebHistoryItem i = mBackForwardList.getCurrentItem(); + if (i != null) { + if (precomposed || i.getTouchIconUrl() != null) { + i.setTouchIconUrl(url); + } + } + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(RECEIVED_TOUCH_ICON_URL, + precomposed ? 1 : 0, 0, url)); + } + public void onReceivedTitle(String title) { // Do an unsynchronized quick check to avoid posting if no callback has // been set. @@ -1037,8 +1190,124 @@ class CallbackProxy extends Handler { } /** - * @hide pending API council approval + * Called by WebViewCore to inform the Java side that the current origin + * has overflowed it's database quota. Called in the WebCore thread so + * posts a message to the UI thread that will prompt the WebChromeClient + * for what to do. On return back to C++ side, the WebCore thread will + * sleep pending a new quota value. + * @param url The URL that caused the quota overflow. + * @param databaseIdentifier The identifier of the database that the + * transaction that caused the overflow was running on. + * @param currentQuota The current quota the origin is allowed. + * @param estimatedSize The estimated size of the database. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater An instance of a class encapsulating a callback + * to WebViewCore to run when the decision to allow or deny more + * quota has been made. + */ + public void onExceededDatabaseQuota( + String url, String databaseIdentifier, long currentQuota, + long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + if (mWebChromeClient == null) { + quotaUpdater.updateQuota(currentQuota); + return; + } + + Message exceededQuota = obtainMessage(EXCEEDED_DATABASE_QUOTA); + HashMap map = new HashMap(); + map.put("databaseIdentifier", databaseIdentifier); + map.put("url", url); + map.put("currentQuota", currentQuota); + map.put("estimatedSize", estimatedSize); + map.put("totalUsedQuota", totalUsedQuota); + map.put("quotaUpdater", quotaUpdater); + exceededQuota.obj = map; + sendMessage(exceededQuota); + } + + /** + * Called by WebViewCore to inform the Java side that the appcache has + * exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater An instance of a class encapsulating a callback + * to WebViewCore to run when the decision to allow or deny a bigger + * app cache size has been made. + */ + public void onReachedMaxAppCacheSize(long spaceNeeded, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { + if (mWebChromeClient == null) { + quotaUpdater.updateQuota(0); + return; + } + + Message msg = obtainMessage(REACHED_APPCACHE_MAXSIZE); + HashMap map = new HashMap(); + map.put("spaceNeeded", spaceNeeded); + map.put("totalUsedQuota", totalUsedQuota); + map.put("quotaUpdater", quotaUpdater); + msg.obj = map; + sendMessage(msg); + } + + /** + * Called by WebViewCore to instruct the browser to display a prompt to ask + * the user to set the Geolocation permission state for the given origin. + * @param origin The origin requesting Geolocation permsissions. + * @param callback The callback to call once a permission state has been + * obtained. + */ + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) { + if (mWebChromeClient == null) { + return; + } + + Message showMessage = + obtainMessage(GEOLOCATION_PERMISSIONS_SHOW_PROMPT); + HashMap map = new HashMap(); + map.put("origin", origin); + map.put("callback", callback); + showMessage.obj = map; + sendMessage(showMessage); + } + + /** + * Called by WebViewCore to instruct the browser to hide the Geolocation + * permissions prompt. + */ + public void onGeolocationPermissionsHidePrompt() { + if (mWebChromeClient == null) { + return; + } + + Message hideMessage = obtainMessage(GEOLOCATION_PERMISSIONS_HIDE_PROMPT); + sendMessage(hideMessage); + } + + /** + * Called by WebViewCore when we have a message to be added to the JavaScript + * error console. Sends a message to the Java side with the details. + * @param message The message to add to the console. + * @param lineNumber The lineNumber of the source file on which the error + * occurred. + * @param sourceID The filename of the source file in which the error + * occurred. */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) { + if (mWebChromeClient == null) { + return; + } + + Message msg = obtainMessage(ADD_MESSAGE_TO_CONSOLE); + msg.getData().putString("message", message); + msg.getData().putString("sourceID", sourceID); + msg.getData().putInt("lineNumber", lineNumber); + sendMessage(msg); + } + public boolean onJsTimeout() { //always interrupt timedout JS by default if (mWebChromeClient == null) { @@ -1057,4 +1326,13 @@ class CallbackProxy extends Handler { } return result.getResult(); } + + public void getVisitedHistory(ValueCallback callback) { + if (mWebChromeClient == null) { + return; + } + Message msg = obtainMessage(GET_VISITED_HISTORY); + msg.obj = callback; + sendMessage(msg); + } } diff --git a/core/java/android/webkit/CertTool.java b/core/java/android/webkit/CertTool.java new file mode 100644 index 0000000000000000000000000000000000000000..99757d24f00ad3d5a9e1d0f9011cbf7f42541ab8 --- /dev/null +++ b/core/java/android/webkit/CertTool.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.netscape.NetscapeCertRequest; +import org.bouncycastle.util.encoders.Base64; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.security.Credentials; +import android.util.Log; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +class CertTool { + private static final String LOGTAG = "CertTool"; + + private static final AlgorithmIdentifier MD5_WITH_RSA = + new AlgorithmIdentifier(PKCSObjectIdentifiers.md5WithRSAEncryption); + + static final String CERT = Credentials.CERTIFICATE; + static final String PKCS12 = Credentials.PKCS12; + + static String[] getKeyStrengthList() { + return new String[] {"High Grade", "Medium Grade"}; + } + + static String getSignedPublicKey(Context context, int index, String challenge) { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize((index == 0) ? 2048 : 1024); + KeyPair pair = generator.genKeyPair(); + + NetscapeCertRequest request = new NetscapeCertRequest(challenge, + MD5_WITH_RSA, pair.getPublic()); + request.sign(pair.getPrivate()); + byte[] signed = request.toASN1Object().getDEREncoded(); + + Credentials.getInstance().install(context, pair); + return new String(Base64.encode(signed)); + } catch (Exception e) { + Log.w(LOGTAG, e); + } + return null; + } + + static void addCertificate(Context context, String type, byte[] value) { + Credentials.getInstance().install(context, type, value); + } + + private CertTool() {} +} diff --git a/core/java/android/webkit/ContentLoader.java b/core/java/android/webkit/ContentLoader.java index f6d7f6990a45a32931235e88eeb594b83e8a989b..19aa087c49e6169f5342674fc91fbb81a2aabf3a 100644 --- a/core/java/android/webkit/ContentLoader.java +++ b/core/java/android/webkit/ContentLoader.java @@ -57,6 +57,16 @@ class ContentLoader extends StreamLoader { } + private String errString(Exception ex) { + String exMessage = ex.getMessage(); + String errString = mContext.getString( + com.android.internal.R.string.httpErrorFileNotFound); + if (exMessage != null) { + errString += " " + exMessage; + } + return errString; + } + @Override protected boolean setupStreamAndSendStatus() { Uri uri = Uri.parse(mUrl); @@ -73,28 +83,16 @@ class ContentLoader extends StreamLoader { mDataStream = mContext.getContentResolver().openInputStream(uri); mHandler.status(1, 1, 0, "OK"); } catch (java.io.FileNotFoundException ex) { - mHandler.error( - EventHandler.FILE_NOT_FOUND_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_NOT_FOUND_ERROR, errString(ex)); return false; } catch (java.io.IOException ex) { - mHandler.error( - EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } catch (RuntimeException ex) { // readExceptionWithFileNotFoundExceptionFromParcel in DatabaseUtils // can throw a serial of RuntimeException. Catch them all here. - mHandler.error( - EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } return true; @@ -105,8 +103,7 @@ class ContentLoader extends StreamLoader { if (mContentType != null) { headers.setContentType("text/html"); } - // override the cache-control header set by StreamLoader as content can - // change, we don't want WebKit to cache it + // content can change, we don't want WebKit to cache it headers.setCacheControl("no-store, no-cache"); } diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index e8c22798bd55bfa1030f8f9a8c665dcf4349b69b..fca591f4fb7ac016605a3ffd3f4af3c28d450780 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -23,9 +23,12 @@ import android.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; /** * CookieManager manages cookies according to RFC2109 spec. @@ -190,6 +193,31 @@ public final class CookieManager { } } + private static final CookieComparator COMPARATOR = new CookieComparator(); + + private static final class CookieComparator implements Comparator { + public int compare(Cookie cookie1, Cookie cookie2) { + // According to RFC 2109, multiple cookies are ordered in a way such + // that those with more specific Path attributes precede those with + // less specific. Ordering with respect to other attributes (e.g., + // Domain) is unspecified. + // As Set is not modified if the two objects are same, we do want to + // assign different value for each cookie. + int diff = cookie2.path.length() - cookie1.path.length(); + if (diff == 0) { + diff = cookie2.domain.length() - cookie1.domain.length(); + if (diff == 0) { + diff = cookie2.name.hashCode() - cookie1.name.hashCode(); + if (diff == 0) { + Log.w(LOGTAG, "Found two cookies with the same value." + + "cookie1=" + cookie1 + " , cookie2=" + cookie2); + } + } + } + return diff; + } + } + private CookieManager() { } @@ -262,7 +290,7 @@ public final class CookieManager { if (!mAcceptCookie || uri == null) { return; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value); } @@ -401,8 +429,8 @@ public final class CookieManager { long now = System.currentTimeMillis(); boolean secure = HTTPS.equals(uri.mScheme); Iterator iter = cookieList.iterator(); - StringBuilder ret = new StringBuilder(256); + SortedSet cookieSet = new TreeSet(COMPARATOR); while (iter.hasNext()) { Cookie cookie = iter.next(); if (cookie.domainMatch(hostAndPath[0]) && @@ -413,26 +441,33 @@ public final class CookieManager { && (!cookie.secure || secure) && cookie.mode != Cookie.MODE_DELETED) { cookie.lastAcessTime = now; + cookieSet.add(cookie); + } + } - if (ret.length() > 0) { - ret.append(SEMICOLON); - // according to RC2109, SEMICOLON is office separator, - // but when log in yahoo.com, it needs WHITE_SPACE too. - ret.append(WHITE_SPACE); - } - - ret.append(cookie.name); - ret.append(EQUAL); - ret.append(cookie.value); + StringBuilder ret = new StringBuilder(256); + Iterator setIter = cookieSet.iterator(); + while (setIter.hasNext()) { + Cookie cookie = setIter.next(); + if (ret.length() > 0) { + ret.append(SEMICOLON); + // according to RC2109, SEMICOLON is official separator, + // but when log in yahoo.com, it needs WHITE_SPACE too. + ret.append(WHITE_SPACE); } + + ret.append(cookie.name); + ret.append(EQUAL); + ret.append(cookie.value); } + if (ret.length() > 0) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); } return ret.toString(); } else { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " But can't find cookie."); } @@ -588,7 +623,7 @@ public final class CookieManager { Iterator> listIter = cookieLists.iterator(); while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { ArrayList list = listIter.next(); - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Iterator iter = list.iterator(); while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { Cookie cookie = iter.next(); @@ -608,7 +643,7 @@ public final class CookieManager { ArrayList retlist = new ArrayList(); if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, count + " cookies used " + byteCount + " bytes with " + mapSize + " domains"); } @@ -616,7 +651,7 @@ public final class CookieManager { int toGo = mapSize / 10 + 1; while (toGo-- > 0){ String domain = domains[toGo].toString(); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "delete domain: " + domain + " from RAM cache"); } @@ -798,22 +833,24 @@ public final class CookieManager { // "secure" is a known attribute doesn't use "="; // while sites like live.com uses "secure=" - if (length - index > SECURE_LENGTH + if (length - index >= SECURE_LENGTH && cookieString.substring(index, index + SECURE_LENGTH). equalsIgnoreCase(SECURE)) { index += SECURE_LENGTH; cookie.secure = true; + if (index == length) break; if (cookieString.charAt(index) == EQUAL) index++; continue; } // "httponly" is a known attribute doesn't use "="; // while sites like live.com uses "httponly=" - if (length - index > HTTP_ONLY_LENGTH + if (length - index >= HTTP_ONLY_LENGTH && cookieString.substring(index, index + HTTP_ONLY_LENGTH). equalsIgnoreCase(HTTP_ONLY)) { index += HTTP_ONLY_LENGTH; + if (index == length) break; if (cookieString.charAt(index) == EQUAL) index++; // FIXME: currently only parse the attribute continue; diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java index 8d66529f24b93e8203aa30cfe50dd22de3d10487..14375d2b13228e03f11238ac7e3d17ba42c80215 100644 --- a/core/java/android/webkit/CookieSyncManager.java +++ b/core/java/android/webkit/CookieSyncManager.java @@ -24,30 +24,39 @@ import java.util.ArrayList; import java.util.Iterator; /** - * The class CookieSyncManager is used to synchronize the browser cookies - * between RAM and FLASH. To get the best performance, browser cookie is saved - * in RAM. We use a separate thread to sync the cookies between RAM and FLASH on - * a timer base. + * The CookieSyncManager is used to synchronize the browser cookie store + * between RAM and permanent storage. To get the best performance, browser cookies are + * saved in RAM. A separate thread saves the cookies between, driven by a timer. *

    + * * To use the CookieSyncManager, the host application has to call the following - * when the application starts. - *

    - * CookieSyncManager.createInstance(context) - *

    - * To set up for sync, the host application has to call - *

    - * CookieSyncManager.getInstance().startSync() + * when the application starts: *

    - * in its Activity.onResume(), and call + * + *

    CookieSyncManager.createInstance(context)

    + * + * To set up for sync, the host application has to call

    + *

    CookieSyncManager.getInstance().startSync()

    + * + * in Activity.onResume(), and call *

    + * + *

      * CookieSyncManager.getInstance().stopSync()
    - * 

    - * in its Activity.onStop(). - *

    + *

    + * + * in Activity.onPause().

    + * * To get instant sync instead of waiting for the timer to trigger, the host can * call *

    - * CookieSyncManager.getInstance().sync() + *

    CookieSyncManager.getInstance().sync()

    + * + * The sync interval is 5 minutes, so you will want to force syncs + * manually anyway, for instance in {@link + * WebViewClient#onPageFinished}. Note that even sync() happens + * asynchronously, so don't do it just as your activity is shutting + * down. */ public final class CookieSyncManager extends WebSyncManager { @@ -90,7 +99,7 @@ public final class CookieSyncManager extends WebSyncManager { } /** - * Package level api, called from CookieManager Get all the cookies which + * Package level api, called from CookieManager. Get all the cookies which * matches a given base domain. * @param domain * @return A list of Cookie @@ -161,7 +170,7 @@ public final class CookieSyncManager extends WebSyncManager { } protected void syncFromRamToFlash() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash STARTS"); } @@ -178,7 +187,7 @@ public final class CookieSyncManager extends WebSyncManager { CookieManager.getInstance().deleteLRUDomain(); syncFromRamToFlash(lruList); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash DONE"); } } diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java index dcdc949c551b5d08aac69ca6d69bc313476ede57..6c5d10dc7049b61c0c32a5d2a63eb6101b5df196 100644 --- a/core/java/android/webkit/DataLoader.java +++ b/core/java/android/webkit/DataLoader.java @@ -16,12 +16,10 @@ package android.webkit; -import org.apache.http.protocol.HTTP; - -import android.net.http.Headers; - import java.io.ByteArrayInputStream; +import org.apache.harmony.luni.util.Base64; + /** * This class is a concrete implementation of StreamLoader that uses the * content supplied as a URL as the source for the stream. The mimetype @@ -30,8 +28,6 @@ import java.io.ByteArrayInputStream; */ class DataLoader extends StreamLoader { - private String mContentType; // Content mimetype, if supplied in URL - /** * Constructor uses the dataURL as the source for an InputStream * @param dataUrl data: URL string optionally containing a mimetype @@ -41,16 +37,20 @@ class DataLoader extends StreamLoader { super(loadListener); String url = dataUrl.substring("data:".length()); - String content; + byte[] data = null; int commaIndex = url.indexOf(','); if (commaIndex != -1) { - mContentType = url.substring(0, commaIndex); - content = url.substring(commaIndex + 1); + String contentType = url.substring(0, commaIndex); + data = url.substring(commaIndex + 1).getBytes(); + loadListener.parseContentTypeHeader(contentType); + if ("base64".equals(loadListener.transferEncoding())) { + data = Base64.decode(data); + } } else { - content = url; + data = url.getBytes(); } - mDataStream = new ByteArrayInputStream(content.getBytes()); - mContentLength = content.length(); + mDataStream = new ByteArrayInputStream(data); + mContentLength = data.length; } @Override @@ -60,10 +60,7 @@ class DataLoader extends StreamLoader { } @Override - protected void buildHeaders(Headers headers) { - if (mContentType != null) { - headers.setContentType(mContentType); - } + protected void buildHeaders(android.net.http.Headers h) { } /** diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 750403b49817304a0ea76bf214e52f2e9692707c..c46702e7050921ea7d3969a948a27d27f3f2ce20 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -43,9 +43,6 @@ public class DateSorter { private static final int NUM_DAYS_AGO = 5; - Date mDate = new Date(); - Calendar mCal = Calendar.getInstance(); - /** * @param context Application context */ diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java new file mode 100644 index 0000000000000000000000000000000000000000..8e25395f12010d2e64cec9e7e68578b7e1db3535 --- /dev/null +++ b/core/java/android/webkit/DebugFlags.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * This class is a container for all of the debug flags used in the Java + * components of webkit. These flags must be final in order to ensure that + * the compiler optimizes the code that uses them out of the final executable. + * + * The name of each flags maps directly to the name of the class in which that + * flag is used. + * + */ +class DebugFlags { + + public static final boolean BROWSER_FRAME = false; + public static final boolean CACHE_MANAGER = false; + public static final boolean CALLBACK_PROXY = false; + public static final boolean COOKIE_MANAGER = false; + public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean FRAME_LOADER = false; + public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE + public static final boolean LOAD_LISTENER = false; + public static final boolean NETWORK = false; + public static final boolean SSL_ERROR_HANDLER = false; + public static final boolean STREAM_LOADER = false; + public static final boolean URL_UTIL = false; + public static final boolean WEB_BACK_FORWARD_LIST = false; + public static final boolean WEB_SETTINGS = false; + public static final boolean WEB_SYNC_MANAGER = false; + public static final boolean WEB_TEXT_VIEW = false; + public static final boolean WEB_VIEW = false; + public static final boolean WEB_VIEW_CORE = false; + +} diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java index 54a4c1d12468c77f2f77a1b198de24c969083974..085f1f405c72a75768327bfa3ef71ce861e5ae10 100644 --- a/core/java/android/webkit/FileLoader.java +++ b/core/java/android/webkit/FileLoader.java @@ -72,6 +72,15 @@ class FileLoader extends StreamLoader { } } + private String errString(Exception ex) { + String exMessage = ex.getMessage(); + String errString = mContext.getString(R.string.httpErrorFileNotFound); + if (exMessage != null) { + errString += " " + exMessage; + } + return errString; + } + @Override protected boolean setupStreamAndSendStatus() { try { @@ -95,16 +104,11 @@ class FileLoader extends StreamLoader { mHandler.status(1, 1, 0, "OK"); } catch (java.io.FileNotFoundException ex) { - mHandler.error( - EventHandler.FILE_NOT_FOUND_ERROR, - mContext.getString(R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_NOT_FOUND_ERROR, errString(ex)); return false; } catch (java.io.IOException ex) { - mHandler.error(EventHandler.FILE_ERROR, - mContext.getString(R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } return true; diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index 66ab0213543cc0c043d9f04b4b118359656ace0e..c1eeb3b2a0b70a2c6cea3b1aadaf2b38927f5c16 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -28,7 +28,6 @@ class FrameLoader { private final LoadListener mListener; private final String mMethod; - private final boolean mIsHighPriority; private final WebSettings mSettings; private Map mHeaders; private byte[] mPostData; @@ -52,11 +51,10 @@ class FrameLoader { private static final String LOGTAG = "webkit"; FrameLoader(LoadListener listener, WebSettings settings, - String method, boolean highPriority) { + String method) { mListener = listener; mHeaders = null; mMethod = method; - mIsHighPriority = highPriority; mCacheMode = WebSettings.LOAD_NORMAL; mSettings = settings; } @@ -97,17 +95,6 @@ class FrameLoader { public boolean executeLoad() { String url = mListener.url(); - // Attempt to decode the percent-encoded url. - try { - url = new String(URLUtil.decode(url.getBytes())); - } catch (IllegalArgumentException e) { - // Fail with a bad url error if the decode fails. - mListener.error(EventHandler.ERROR_BAD_URL, - mListener.getContext().getString( - com.android.internal.R.string.httpErrorBadUrl)); - return false; - } - if (URLUtil.isNetworkUrl(url)){ if (mSettings.getBlockNetworkLoads()) { mListener.error(EventHandler.ERROR_BAD_URL, @@ -115,12 +102,19 @@ class FrameLoader { com.android.internal.R.string.httpErrorBadUrl)); return false; } + // Make sure it is correctly URL encoded before sending the request + if (!URLUtil.verifyURLEncoding(url)) { + mListener.error(EventHandler.ERROR_BAD_URL, + mListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + return false; + } mNetwork = Network.getInstance(mListener.getContext()); return handleHTTPLoad(); } else if (handleLocalFile(url, mListener, mSettings)) { return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" + mListener.url()); } @@ -134,6 +128,18 @@ class FrameLoader { /* package */ static boolean handleLocalFile(String url, LoadListener loadListener, WebSettings settings) { + // Attempt to decode the percent-encoded url before passing to the + // local loaders. + try { + url = new String(URLUtil.decode(url.getBytes())); + } catch (IllegalArgumentException e) { + loadListener.error(EventHandler.ERROR_BAD_URL, + loadListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + // Return true here so we do not trigger an unsupported scheme + // error. + return true; + } if (URLUtil.isAssetUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), true, settings.getAllowFileAccess()); @@ -166,21 +172,17 @@ class FrameLoader { populateStaticHeaders(); populateHeaders(); - // response was handled by UrlIntercept, don't issue HTTP request - if (handleUrlIntercept()) return true; - // response was handled by Cache, don't issue HTTP request if (handleCache()) { // push the request data down to the LoadListener // as response from the cache could be a redirect // and we may need to initiate a network request if the cache // can't satisfy redirect URL - mListener.setRequestData(mMethod, mHeaders, mPostData, - mIsHighPriority); + mListener.setRequestData(mMethod, mHeaders, mPostData); return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " + mListener.url()); } @@ -190,7 +192,7 @@ class FrameLoader { try { ret = mNetwork.requestURL(mMethod, mHeaders, - mPostData, mListener, mIsHighPriority); + mPostData, mListener); } catch (android.net.ParseException ex) { error = EventHandler.ERROR_BAD_URL; } catch (java.lang.RuntimeException ex) { @@ -207,11 +209,11 @@ class FrameLoader { } /* - * This function is used by handleUrlInterecpt and handleCache to + * This function is used by handleCache to * setup a load from the byte stream in a CacheResult. */ private void startCacheLoad(CacheResult result) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: loading from cache: " + mListener.url()); } @@ -222,30 +224,6 @@ class FrameLoader { cacheLoader.load(); } - /* - * This function is used by handleHTTPLoad to allow URL - * interception. This can be used to provide alternative load - * methods such as locally stored versions or for debugging. - * - * Returns true if the response was handled by UrlIntercept. - */ - private boolean handleUrlIntercept() { - // Check if the URL can be served from UrlIntercept. If - // successful, return the data just like a cache hit. - - PluginData data = UrlInterceptRegistry.getPluginData( - mListener.url(), mHeaders); - - if(data != null) { - PluginContentLoader loader = - new PluginContentLoader(mListener, data); - loader.load(); - return true; - } - // Not intercepted. Carry on as normal. - return false; - } - /* * This function is used by the handleHTTPLoad to setup the cache headers * correctly. @@ -285,7 +263,7 @@ class FrameLoader { // of it's state. If it is not in the cache, then go to the // network. case WebSettings.LOAD_CACHE_ELSE_NETWORK: { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: checking cache: " + mListener.url()); } diff --git a/core/java/android/webkit/GearsPermissionsManager.java b/core/java/android/webkit/GearsPermissionsManager.java deleted file mode 100644 index 6549cb8fb0aa349e6411071a171bb042b81a48e2..0000000000000000000000000000000000000000 --- a/core/java/android/webkit/GearsPermissionsManager.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.webkit; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.database.ContentObserver; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.Log; - -import java.io.File; -import java.util.HashSet; - -/** - * Donut-specific hack to keep Gears permissions in sync with the - * system location setting. - */ -class GearsPermissionsManager { - // The application context. - Context mContext; - // The path to gears.so. - private String mGearsPath; - - // The Gears permissions database directory. - private final static String GEARS_DATABASE_DIR = "gears"; - // The Gears permissions database file name. - private final static String GEARS_DATABASE_FILE = "permissions.db"; - // The Gears location permissions table. - private final static String GEARS_LOCATION_ACCESS_TABLE_NAME = - "LocationAccess"; - // The Gears storage access permissions table. - private final static String GEARS_STORAGE_ACCESS_TABLE_NAME = "Access"; - // The Gears permissions db schema version table. - private final static String GEARS_SCHEMA_VERSION_TABLE_NAME = - "VersionInfo"; - // The Gears permission value that denotes "allow access to location". - private static final int GEARS_ALLOW_LOCATION_ACCESS = 1; - // The shared pref name. - private static final String LAST_KNOWN_LOCATION_SETTING = - "lastKnownLocationSystemSetting"; - // The Browser package name. - private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; - // The Secure Settings observer that will be notified when the system - // location setting changes. - private SecureSettingsObserver mSettingsObserver; - // The Google URLs whitelisted for Gears location access. - private static HashSet sGearsWhiteList; - - static { - sGearsWhiteList = new HashSet(); - // NOTE: DO NOT ADD A "/" AT THE END! - sGearsWhiteList.add("http://www.google.com"); - sGearsWhiteList.add("http://www.google.co.uk"); - } - - private static final String LOGTAG = "webcore"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; - - GearsPermissionsManager(Context context, String gearsPath) { - mContext = context; - mGearsPath = gearsPath; - } - - public void doCheckAndStartObserver() { - // Are we running in the browser? - if (!BROWSER_PACKAGE_NAME.equals(mContext.getPackageName())) { - return; - } - // Do the check. - checkGearsPermissions(); - // Install the observer. - mSettingsObserver = new SecureSettingsObserver(); - mSettingsObserver.observe(); - } - - private void checkGearsPermissions() { - // Get the current system settings. - int setting = Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.USE_LOCATION_FOR_SERVICES, -1); - // Check if we need to set the Gears permissions. - if (setting != -1 && locationSystemSettingChanged(setting)) { - setGearsPermissionForGoogleDomains(setting); - } - } - - private boolean locationSystemSettingChanged(int newSetting) { - SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(mContext); - int oldSetting = 0; - oldSetting = prefs.getInt(LAST_KNOWN_LOCATION_SETTING, oldSetting); - if (oldSetting == newSetting) { - return false; - } - Editor ed = prefs.edit(); - ed.putInt(LAST_KNOWN_LOCATION_SETTING, newSetting); - ed.commit(); - return true; - } - - private void setGearsPermissionForGoogleDomains(int systemPermission) { - // Transform the system permission into a boolean flag. When this - // flag is true, it means the origins in gGearsWhiteList are added - // to the Gears location permission table with permission 1 (allowed). - // When the flag is false, the origins in gGearsWhiteList are removed - // from the Gears location permission table. Next time the user - // navigates to one of these origins, she will see the normal Gears - // permission prompt. - boolean addToGearsLocationTable = (systemPermission == 1 ? true : false); - // Build the path to the Gears library. - - File file = new File(mGearsPath).getParentFile(); - if (file == null) { - return; - } - // Build the Gears database file name. - file = new File(file.getAbsolutePath() + File.separator - + GEARS_DATABASE_DIR + File.separator + GEARS_DATABASE_FILE); - // Remember whether or not we need to create the LocationAccess table. - boolean needToCreateTables = false; - if (!file.exists()) { - needToCreateTables = true; - // Create the path or else SQLiteDatabase.openOrCreateDatabase() - // may throw on the device. - file.getParentFile().mkdirs(); - } - // If the database file does not yet exist and the system location - // setting says that the Gears origins need to be removed from the - // location permission table, it means that we don't actually need - // to do anything at all. - if (needToCreateTables && !addToGearsLocationTable) { - return; - } - // Try opening the Gears database. - SQLiteDatabase permissions; - try { - permissions = SQLiteDatabase.openOrCreateDatabase(file, null); - } catch (SQLiteException e) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "Could not open Gears permission DB: " - + e.getMessage()); - } - // Just bail out. - return; - } - // We now have a database open. Begin a transaction. - permissions.beginTransaction(); - try { - if (needToCreateTables) { - // Create the tables. Note that this creates the - // Gears tables for the permissions DB schema version 2. - // The Gears schema upgrade process will take care of the rest. - // First, the storage access table. - SQLiteStatement statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_STORAGE_ACCESS_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Next the location access table. - statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_LOCATION_ACCESS_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Finally, the schema version table. - statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_SCHEMA_VERSION_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Set the schema version to 2. - ContentValues schema = new ContentValues(); - schema.put("Name", "Version"); - schema.put("Value", 2); - permissions.insert(GEARS_SCHEMA_VERSION_TABLE_NAME, null, - schema); - } - - if (addToGearsLocationTable) { - ContentValues permissionValues = new ContentValues(); - - for (String url : sGearsWhiteList) { - permissionValues.put("Name", url); - permissionValues.put("Value", GEARS_ALLOW_LOCATION_ACCESS); - permissions.replace(GEARS_LOCATION_ACCESS_TABLE_NAME, null, - permissionValues); - permissionValues.clear(); - } - } else { - for (String url : sGearsWhiteList) { - permissions.delete(GEARS_LOCATION_ACCESS_TABLE_NAME, "Name=?", - new String[] { url }); - } - } - // Commit the transaction. - permissions.setTransactionSuccessful(); - } catch (SQLiteException e) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "Could not set the Gears permissions: " - + e.getMessage()); - } - } finally { - permissions.endTransaction(); - permissions.close(); - } - } - - class SecureSettingsObserver extends ContentObserver { - SecureSettingsObserver() { - super(new Handler()); - } - - void observe() { - ContentResolver resolver = mContext.getContentResolver(); - resolver.registerContentObserver(Settings.Secure.getUriFor( - Settings.Secure.USE_LOCATION_FOR_SERVICES), false, this); - } - - @Override - public void onChange(boolean selfChange) { - checkGearsPermissions(); - } - } -} diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java new file mode 100755 index 0000000000000000000000000000000000000000..64a9d9b5ee2a2f9a311ec98580d7508b8769eee8 --- /dev/null +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + + +/** + * Implements the Java side of GeolocationPermissions. Simply marshalls calls + * from the UI thread to the WebKit thread. + */ +public final class GeolocationPermissions { + /** + * Callback interface used by the browser to report a Geolocation permission + * state set by the user in response to a permissions prompt. + */ + public interface Callback { + public void invoke(String origin, boolean allow, boolean remember); + }; + + // Log tag + private static final String TAG = "geolocationPermissions"; + + // Global instance + private static GeolocationPermissions sInstance; + + private Handler mHandler; + private Handler mUIHandler; + + // Members used to transfer the origins and permissions between threads. + private Set mOrigins; + private boolean mAllowed; + private Set mOriginsToClear; + private Set mOriginsToAllow; + + // Message ids + static final int GET_ORIGINS = 0; + static final int GET_ALLOWED = 1; + static final int CLEAR = 2; + static final int ALLOW = 3; + static final int CLEAR_ALL = 4; + + // Message ids on the UI thread + static final int RETURN_ORIGINS = 0; + static final int RETURN_ALLOWED = 1; + + private static final String ORIGINS = "origins"; + private static final String ORIGIN = "origin"; + private static final String CALLBACK = "callback"; + private static final String ALLOWED = "allowed"; + + /** + * Gets the singleton instance of the class. + */ + public static GeolocationPermissions getInstance() { + if (sInstance == null) { + sInstance = new GeolocationPermissions(); + } + return sInstance; + } + + /** + * Creates the UI message handler. Must be called on the UI thread. + * @hide + */ + public void createUIHandler() { + if (mUIHandler == null) { + mUIHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // Runs on the UI thread. + switch (msg.what) { + case RETURN_ORIGINS: { + Map values = (Map) msg.obj; + Set origins = (Set) values.get(ORIGINS); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + callback.onReceiveValue(origins); + } break; + case RETURN_ALLOWED: { + Map values = (Map) msg.obj; + Boolean allowed = (Boolean) values.get(ALLOWED); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + callback.onReceiveValue(allowed); + } break; + } + } + }; + } + } + + /** + * Creates the message handler. Must be called on the WebKit thread. + * @hide + */ + public void createHandler() { + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // Runs on the WebKit thread. + switch (msg.what) { + case GET_ORIGINS: { + getOriginsImpl(); + ValueCallback callback = (ValueCallback) msg.obj; + Set origins = new HashSet(mOrigins); + Map values = new HashMap(); + values.put(CALLBACK, callback); + values.put(ORIGINS, origins); + postUIMessage(Message.obtain(null, RETURN_ORIGINS, values)); + } break; + case GET_ALLOWED: { + Map values = (Map) msg.obj; + String origin = (String) values.get(ORIGIN); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + getAllowedImpl(origin); + Map retValues = new HashMap(); + retValues.put(CALLBACK, callback); + retValues.put(ALLOWED, new Boolean(mAllowed)); + postUIMessage(Message.obtain(null, RETURN_ALLOWED, retValues)); + } break; + case CLEAR: + nativeClear((String) msg.obj); + break; + case ALLOW: + nativeAllow((String) msg.obj); + break; + case CLEAR_ALL: + nativeClearAll(); + break; + } + } + }; + + if (mOriginsToClear != null) { + for (String origin : mOriginsToClear) { + nativeClear(origin); + } + } + if (mOriginsToAllow != null) { + for (String origin : mOriginsToAllow) { + nativeAllow(origin); + } + } + } + } + + /** + * Utility function to send a message to our handler. + */ + private void postMessage(Message msg) { + assert(mHandler != null); + mHandler.sendMessage(msg); + } + + /** + * Utility function to send a message to the handler on the UI thread + */ + private void postUIMessage(Message msg) { + if (mUIHandler != null) { + mUIHandler.sendMessage(msg); + } + } + + /** + * Gets the set of origins for which Geolocation permissions are stored. + * Note that we represent the origins as strings. These are created using + * WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' + * (Database, Geolocation etc) do so, it's safe to match up origins for the + * purposes of displaying UI. + */ + public void getOrigins(ValueCallback callback) { + if (callback != null) { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + getOriginsImpl(); + Set origins = new HashSet(mOrigins); + callback.onReceiveValue(origins); + } else { + postMessage(Message.obtain(null, GET_ORIGINS, callback)); + } + } + } + + /** + * Helper method to get the set of origins. + */ + private void getOriginsImpl() { + // Called on the WebKit thread. + mOrigins = nativeGetOrigins(); + } + + /** + * Gets the permission state for the specified origin. + */ + public void getAllowed(String origin, ValueCallback callback) { + if (callback == null) { + return; + } + if (origin == null) { + callback.onReceiveValue(null); + return; + } + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + getAllowedImpl(origin); + callback.onReceiveValue(new Boolean(mAllowed)); + } else { + Map values = new HashMap(); + values.put(ORIGIN, origin); + values.put(CALLBACK, callback); + postMessage(Message.obtain(null, GET_ALLOWED, values)); + } + } + + /** + * Helper method to get the permission state. + */ + private void getAllowedImpl(String origin) { + // Called on the WebKit thread. + mAllowed = nativeGetAllowed(origin); + } + + /** + * Clears the permission state for the specified origin. This method may be + * called before the WebKit thread has intialized the message handler. + * Messages will be queued until this time. + */ + public void clear(String origin) { + // Called on the UI thread. + if (mHandler == null) { + if (mOriginsToClear == null) { + mOriginsToClear = new HashSet(); + } + mOriginsToClear.add(origin); + if (mOriginsToAllow != null) { + mOriginsToAllow.remove(origin); + } + } else { + postMessage(Message.obtain(null, CLEAR, origin)); + } + } + + /** + * Allows the specified origin. This method may be called before the WebKit + * thread has intialized the message handler. Messages will be queued until + * this time. + */ + public void allow(String origin) { + // Called on the UI thread. + if (mHandler == null) { + if (mOriginsToAllow == null) { + mOriginsToAllow = new HashSet(); + } + mOriginsToAllow.add(origin); + if (mOriginsToClear != null) { + mOriginsToClear.remove(origin); + } + } else { + postMessage(Message.obtain(null, ALLOW, origin)); + } + } + + /** + * Clears the permission state for all origins. + */ + public void clearAll() { + // Called on the UI thread. + postMessage(Message.obtain(null, CLEAR_ALL)); + } + + // Native functions, run on the WebKit thread. + private static native Set nativeGetOrigins(); + private static native boolean nativeGetAllowed(String origin); + private static native void nativeClear(String origin); + private static native void nativeAllow(String origin); + private static native void nativeClearAll(); +} diff --git a/core/java/android/webkit/GeolocationService.java b/core/java/android/webkit/GeolocationService.java new file mode 100755 index 0000000000000000000000000000000000000000..24306f407b90abce2976b8aa0d9083afcbbe724c --- /dev/null +++ b/core/java/android/webkit/GeolocationService.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.app.ActivityThread; +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; +import android.webkit.WebViewCore; + + +/** + * Implements the Java side of GeolocationServiceAndroid. + */ +final class GeolocationService implements LocationListener { + + // Log tag + private static final String TAG = "geolocationService"; + + private long mNativeObject; + private LocationManager mLocationManager; + private boolean mIsGpsEnabled; + private boolean mIsRunning; + private boolean mIsNetworkProviderAvailable; + private boolean mIsGpsProviderAvailable; + + /** + * Constructor + * @param nativeObject The native object to which this object will report position updates and + * errors. + */ + public GeolocationService(long nativeObject) { + mNativeObject = nativeObject; + // Register newLocationAvailable with platform service. + ActivityThread thread = ActivityThread.systemMain(); + Context context = thread.getApplication(); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (mLocationManager == null) { + Log.e(TAG, "Could not get location manager."); + } + } + + /** + * Start listening for location updates. + */ + public void start() { + registerForLocationUpdates(); + mIsRunning = true; + } + + /** + * Stop listening for location updates. + */ + public void stop() { + unregisterFromLocationUpdates(); + mIsRunning = false; + } + + /** + * Sets whether to use the GPS. + * @param enable Whether to use the GPS. + */ + public void setEnableGps(boolean enable) { + if (mIsGpsEnabled != enable) { + mIsGpsEnabled = enable; + if (mIsRunning) { + // There's no way to unregister from a single provider, so we can + // only unregister from all, then reregister with all but the GPS. + unregisterFromLocationUpdates(); + registerForLocationUpdates(); + } + } + } + + /** + * LocationListener implementation. + * Called when the location has changed. + * @param location The new location, as a Location object. + */ + public void onLocationChanged(Location location) { + // Callbacks from the system location sevice are queued to this thread, so it's possible + // that we receive callbacks after unregistering. At this point, the native object will no + // longer exist. + if (mIsRunning) { + nativeNewLocationAvailable(mNativeObject, location); + } + } + + /** + * LocationListener implementation. + * Called when the provider status changes. + * @param provider The name of the provider. + * @param status The new status of the provider. + * @param extras an optional Bundle with provider specific data. + */ + public void onStatusChanged(String providerName, int status, Bundle extras) { + boolean isAvailable = (status == LocationProvider.AVAILABLE); + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = isAvailable; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = isAvailable; + } + maybeReportError("The last location provider is no longer available"); + } + + /** + * LocationListener implementation. + * Called when the provider is enabled. + * @param provider The name of the location provider that is now enabled. + */ + public void onProviderEnabled(String providerName) { + // No need to notify the native side. It's enough to start sending + // valid position fixes again. + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = true; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = true; + } + } + + /** + * LocationListener implementation. + * Called when the provider is disabled. + * @param provider The name of the location provider that is now disabled. + */ + public void onProviderDisabled(String providerName) { + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = false; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = false; + } + maybeReportError("The last location provider was disabled"); + } + + /** + * Registers this object with the location service. + */ + private void registerForLocationUpdates() { + try { + mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); + mIsNetworkProviderAvailable = true; + if (mIsGpsEnabled) { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + mIsGpsProviderAvailable = true; + } + } catch(SecurityException e) { + Log.e(TAG, "Caught security exception registering for location updates from system. " + + "This should only happen in DumpRenderTree."); + } + } + + /** + * Unregisters this object from the location service. + */ + private void unregisterFromLocationUpdates() { + mLocationManager.removeUpdates(this); + } + + /** + * Reports an error if neither the network nor the GPS provider is available. + */ + private void maybeReportError(String message) { + // Callbacks from the system location sevice are queued to this thread, so it's possible + // that we receive callbacks after unregistering. At this point, the native object will no + // longer exist. + if (mIsRunning && !mIsNetworkProviderAvailable && !mIsGpsProviderAvailable) { + nativeNewErrorAvailable(mNativeObject, message); + } + } + + // Native functions + private static native void nativeNewLocationAvailable(long nativeObject, Location location); + private static native void nativeNewErrorAvailable(long nativeObject, String message); +} diff --git a/core/java/android/webkit/GoogleLocationSettingManager.java b/core/java/android/webkit/GoogleLocationSettingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ecac70a127767032b873421bb6a7bfeab6d30da6 --- /dev/null +++ b/core/java/android/webkit/GoogleLocationSettingManager.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.database.ContentObserver; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; + +import java.util.HashSet; + +/** + * A class to manage the interaction between the system setting 'Location & + * Security - Share with Google' and the browser. When this setting is set + * to true, we allow Geolocation for Google origins. When this setting is + * set to false, we clear Geolocation permissions for Google origins. + */ +class GoogleLocationSettingManager { + // The observer used to listen to the system setting. + private GoogleLocationSettingObserver mSettingObserver; + + // The value of the system setting that indicates true. + private final static int sSystemSettingTrue = 1; + // The value of the system setting that indicates false. + private final static int sSystemSettingFalse = 0; + // The value of the USE_LOCATION_FOR_SERVICES system setting last read + // by the browser. + private final static String LAST_READ_USE_LOCATION_FOR_SERVICES = + "lastReadUseLocationForServices"; + // The Browser package name. + private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; + // The Google origins we consider. + private static HashSet sGoogleOrigins; + static { + sGoogleOrigins = new HashSet(); + // NOTE: DO NOT ADD A "/" AT THE END! + sGoogleOrigins.add("http://www.google.com"); + sGoogleOrigins.add("http://www.google.co.uk"); + } + + private static GoogleLocationSettingManager sGoogleLocationSettingManager = null; + private static int sRefCount = 0; + + static GoogleLocationSettingManager getInstance() { + if (sGoogleLocationSettingManager == null) { + sGoogleLocationSettingManager = new GoogleLocationSettingManager(); + } + return sGoogleLocationSettingManager; + } + + private GoogleLocationSettingManager() {} + + /** + * Starts the manager. Checks whether the setting has changed and + * installs an observer to listen for future changes. + */ + public void start(Context context) { + // Are we running in the browser? + if (context == null || !BROWSER_PACKAGE_NAME.equals(context.getPackageName())) { + return; + } + // Increase the refCount + sRefCount++; + // Are we already registered? + if (mSettingObserver != null) { + return; + } + // Read and apply the settings if needed. + maybeApplySetting(context); + // Register to receive notifications when the system settings change. + mSettingObserver = new GoogleLocationSettingObserver(); + mSettingObserver.observe(context); + } + + /** + * Stops the manager. + */ + public void stop() { + // Are we already registered? + if (mSettingObserver == null) { + return; + } + if (--sRefCount == 0) { + mSettingObserver.doNotObserve(); + mSettingObserver = null; + } + } + /** + * Checks to see if the system setting has changed and if so, + * updates the Geolocation permissions accordingly. + * @param the Application context + */ + private void maybeApplySetting(Context context) { + int setting = getSystemSetting(context); + if (settingChanged(setting, context)) { + applySetting(setting); + } + } + + /** + * Gets the current system setting for 'Use location for Google services'. + * @param the Application context + * @return The system setting. + */ + private int getSystemSetting(Context context) { + return Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.USE_LOCATION_FOR_SERVICES, + sSystemSettingFalse); + } + + /** + * Determines whether the supplied setting has changed from the last + * value read by the browser. + * @param setting The setting. + * @param the Application context + * @return Whether the setting has changed from the last value read + * by the browser. + */ + private boolean settingChanged(int setting, Context context) { + SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + // Default to false. If the system setting is false the first time it is ever read by the + // browser, there's nothing to do. + int lastReadSetting = sSystemSettingFalse; + lastReadSetting = preferences.getInt(LAST_READ_USE_LOCATION_FOR_SERVICES, + lastReadSetting); + + if (lastReadSetting == setting) { + return false; + } + + Editor editor = preferences.edit(); + editor.putInt(LAST_READ_USE_LOCATION_FOR_SERVICES, setting); + editor.commit(); + return true; + } + + /** + * Applies the supplied setting to the Geolocation permissions. + * @param setting The setting. + */ + private void applySetting(int setting) { + for (String origin : sGoogleOrigins) { + if (setting == sSystemSettingTrue) { + GeolocationPermissions.getInstance().allow(origin); + } else { + GeolocationPermissions.getInstance().clear(origin); + } + } + } + + /** + * This class implements an observer to listen for changes to the + * system setting. + */ + private class GoogleLocationSettingObserver extends ContentObserver { + private Context mContext; + + GoogleLocationSettingObserver() { + super(new Handler()); + } + + void observe(Context context) { + if (mContext != null) { + return; + } + ContentResolver resolver = context.getContentResolver(); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.USE_LOCATION_FOR_SERVICES), false, this); + mContext = context; + } + + void doNotObserve() { + if (mContext == null) { + return; + } + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + mContext = null; + } + + @Override + public void onChange(boolean selfChange) { + // This may come after the call to doNotObserve() above, + // so mContext may be null. + if (mContext != null) { + maybeApplySetting(mContext); + } + } + } +} diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..b7a9065e627577ab1669c1372ac1f4b77086734c --- /dev/null +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.net.http.RequestHandle; +import android.net.http.RequestQueue; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsoluteLayout; +import android.widget.FrameLayout; +import android.widget.MediaController; +import android.widget.VideoView; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + *

    Proxy for HTML5 video views. + */ +class HTML5VideoViewProxy extends Handler + implements MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener { + // Logging tag. + private static final String LOGTAG = "HTML5VideoViewProxy"; + + // Message Ids for WebCore thread -> UI thread communication. + private static final int PLAY = 100; + private static final int SEEK = 101; + private static final int PAUSE = 102; + private static final int ERROR = 103; + private static final int LOAD_DEFAULT_POSTER = 104; + + // Message Ids to be handled on the WebCore thread + private static final int PREPARED = 200; + private static final int ENDED = 201; + private static final int POSTER_FETCHED = 202; + + // The C++ MediaPlayerPrivateAndroid object. + int mNativePointer; + // The handler for WebCore thread messages; + private Handler mWebCoreHandler; + // The WebView instance that created this view. + private WebView mWebView; + // The poster image to be shown when the video is not playing. + // This ref prevents the bitmap from being GC'ed. + private Bitmap mPoster; + // The poster downloader. + private PosterDownloader mPosterDownloader; + // The seek position. + private int mSeekPosition; + // A helper class to control the playback. This executes on the UI thread! + private static final class VideoPlayer { + // The proxy that is currently playing (if any). + private static HTML5VideoViewProxy mCurrentProxy; + // The VideoView instance. This is a singleton for now, at least until + // http://b/issue?id=1973663 is fixed. + private static VideoView mVideoView; + // The progress view. + private static View mProgressView; + // The container for the progress view and video view + private static FrameLayout mLayout; + + private static final WebChromeClient.CustomViewCallback mCallback = + new WebChromeClient.CustomViewCallback() { + public void onCustomViewHidden() { + // At this point the videoview is pretty much destroyed. + // It listens to SurfaceHolder.Callback.SurfaceDestroyed event + // which happens when the video view is detached from its parent + // view. This happens in the WebChromeClient before this method + // is invoked. + mCurrentProxy.playbackEnded(); + mCurrentProxy = null; + mLayout.removeView(mVideoView); + mVideoView = null; + if (mProgressView != null) { + mLayout.removeView(mProgressView); + mProgressView = null; + } + mLayout = null; + } + }; + + public static void play(String url, int time, HTML5VideoViewProxy proxy, + WebChromeClient client) { + if (mCurrentProxy != null) { + // Some other video is already playing. Notify the caller that its playback ended. + proxy.playbackEnded(); + return; + } + mCurrentProxy = proxy; + // Create a FrameLayout that will contain the VideoView and the + // progress view (if any). + mLayout = new FrameLayout(proxy.getContext()); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER); + mVideoView = new VideoView(proxy.getContext()); + mVideoView.setWillNotDraw(false); + mVideoView.setMediaController(new MediaController(proxy.getContext())); + mVideoView.setVideoURI(Uri.parse(url)); + mVideoView.setOnCompletionListener(proxy); + mVideoView.setOnPreparedListener(proxy); + mVideoView.setOnErrorListener(proxy); + mVideoView.seekTo(time); + mLayout.addView(mVideoView, layoutParams); + mProgressView = client.getVideoLoadingProgressView(); + if (mProgressView != null) { + mLayout.addView(mProgressView, layoutParams); + mProgressView.setVisibility(View.VISIBLE); + } + mLayout.setVisibility(View.VISIBLE); + mVideoView.start(); + client.onShowCustomView(mLayout, mCallback); + } + + public static void seek(int time, HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { + mVideoView.seekTo(time); + } + } + + public static void pause(HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && mVideoView != null) { + mVideoView.pause(); + } + } + + public static void onPrepared() { + if (mProgressView == null || mLayout == null) { + return; + } + mProgressView.setVisibility(View.GONE); + mLayout.removeView(mProgressView); + mProgressView = null; + } + } + + // A bunch event listeners for our VideoView + // MediaPlayer.OnPreparedListener + public void onPrepared(MediaPlayer mp) { + VideoPlayer.onPrepared(); + Message msg = Message.obtain(mWebCoreHandler, PREPARED); + Map map = new HashMap(); + map.put("dur", new Integer(mp.getDuration())); + map.put("width", new Integer(mp.getVideoWidth())); + map.put("height", new Integer(mp.getVideoHeight())); + msg.obj = map; + mWebCoreHandler.sendMessage(msg); + } + + // MediaPlayer.OnCompletionListener; + public void onCompletion(MediaPlayer mp) { + playbackEnded(); + } + + // MediaPlayer.OnErrorListener + public boolean onError(MediaPlayer mp, int what, int extra) { + sendMessage(obtainMessage(ERROR)); + return false; + } + + public void playbackEnded() { + Message msg = Message.obtain(mWebCoreHandler, ENDED); + mWebCoreHandler.sendMessage(msg); + } + + // Handler for the messages from WebCore thread to the UI thread. + @Override + public void handleMessage(Message msg) { + // This executes on the UI thread. + switch (msg.what) { + case PLAY: { + String url = (String) msg.obj; + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + VideoPlayer.play(url, mSeekPosition, this, client); + } + break; + } + case SEEK: { + Integer time = (Integer) msg.obj; + mSeekPosition = time; + VideoPlayer.seek(mSeekPosition, this); + break; + } + case PAUSE: { + VideoPlayer.pause(this); + break; + } + case ERROR: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + client.onHideCustomView(); + } + break; + } + case LOAD_DEFAULT_POSTER: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + doSetPoster(client.getDefaultVideoPoster()); + } + break; + } + } + } + + // Everything below this comment executes on the WebCore thread, except for + // the EventHandler methods, which are called on the network thread. + + // A helper class that knows how to download posters + private static final class PosterDownloader implements EventHandler { + // The request queue. This is static as we have one queue for all posters. + private static RequestQueue mRequestQueue; + private static int mQueueRefCount = 0; + // The poster URL + private String mUrl; + // The proxy we're doing this for. + private final HTML5VideoViewProxy mProxy; + // The poster bytes. We only touch this on the network thread. + private ByteArrayOutputStream mPosterBytes; + // The request handle. We only touch this on the WebCore thread. + private RequestHandle mRequestHandle; + // The response status code. + private int mStatusCode; + // The response headers. + private Headers mHeaders; + // The handler to handle messages on the WebCore thread. + private Handler mHandler; + + public PosterDownloader(String url, HTML5VideoViewProxy proxy) { + mUrl = url; + mProxy = proxy; + mHandler = new Handler(); + } + // Start the download. Called on WebCore thread. + public void start() { + retainQueue(); + mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); + } + // Cancel the download if active and release the queue. Called on WebCore thread. + public void cancelAndReleaseQueue() { + if (mRequestHandle != null) { + mRequestHandle.cancel(); + mRequestHandle = null; + } + releaseQueue(); + } + // EventHandler methods. Executed on the network thread. + public void status(int major_version, + int minor_version, + int code, + String reason_phrase) { + mStatusCode = code; + } + + public void headers(Headers headers) { + mHeaders = headers; + } + + public void data(byte[] data, int len) { + if (mPosterBytes == null) { + mPosterBytes = new ByteArrayOutputStream(); + } + mPosterBytes.write(data, 0, len); + } + + public void endData() { + if (mStatusCode == 200) { + if (mPosterBytes.size() > 0) { + Bitmap poster = BitmapFactory.decodeByteArray( + mPosterBytes.toByteArray(), 0, mPosterBytes.size()); + mProxy.doSetPoster(poster); + } + cleanup(); + } else if (mStatusCode >= 300 && mStatusCode < 400) { + // We have a redirect. + mUrl = mHeaders.getLocation(); + if (mUrl != null) { + mHandler.post(new Runnable() { + public void run() { + if (mRequestHandle != null) { + mRequestHandle.setupRedirect(mUrl, mStatusCode, + new HashMap()); + } + } + }); + } + } + } + + public void certificate(SslCertificate certificate) { + // Don't care. + } + + public void error(int id, String description) { + cleanup(); + } + + public boolean handleSslErrorRequest(SslError error) { + // Don't care. If this happens, data() will never be called so + // mPosterBytes will never be created, so no need to call cleanup. + return false; + } + // Tears down the poster bytes stream. Called on network thread. + private void cleanup() { + if (mPosterBytes != null) { + try { + mPosterBytes.close(); + } catch (IOException ignored) { + // Ignored. + } finally { + mPosterBytes = null; + } + } + } + + // Queue management methods. Called on WebCore thread. + private void retainQueue() { + if (mRequestQueue == null) { + mRequestQueue = new RequestQueue(mProxy.getContext()); + } + mQueueRefCount++; + } + + private void releaseQueue() { + if (mQueueRefCount == 0) { + return; + } + if (--mQueueRefCount == 0) { + mRequestQueue.shutdown(); + mRequestQueue = null; + } + } + } + + /** + * Private constructor. + * @param webView is the WebView that hosts the video. + * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. + */ + private HTML5VideoViewProxy(WebView webView, int nativePtr) { + // This handler is for the main (UI) thread. + super(Looper.getMainLooper()); + // Save the WebView object. + mWebView = webView; + // Save the native ptr + mNativePointer = nativePtr; + // create the message handler for this thread + createWebCoreHandler(); + } + + private void createWebCoreHandler() { + mWebCoreHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PREPARED: { + Map map = (Map) msg.obj; + Integer duration = (Integer) map.get("dur"); + Integer width = (Integer) map.get("width"); + Integer height = (Integer) map.get("height"); + nativeOnPrepared(duration.intValue(), width.intValue(), + height.intValue(), mNativePointer); + break; + } + case ENDED: + nativeOnEnded(mNativePointer); + break; + case POSTER_FETCHED: + Bitmap poster = (Bitmap) msg.obj; + nativeOnPosterFetched(poster, mNativePointer); + break; + } + } + }; + } + + private void doSetPoster(Bitmap poster) { + if (poster == null) { + return; + } + // Save a ref to the bitmap and send it over to the WebCore thread. + mPoster = poster; + Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); + msg.obj = poster; + mWebCoreHandler.sendMessage(msg); + } + + public Context getContext() { + return mWebView.getContext(); + } + + // The public methods below are all called from WebKit only. + /** + * Play a video stream. + * @param url is the URL of the video stream. + */ + public void play(String url) { + if (url == null) { + return; + } + Message message = obtainMessage(PLAY); + message.obj = url; + sendMessage(message); + } + + /** + * Seek into the video stream. + * @param time is the position in the video stream. + */ + public void seek(int time) { + Message message = obtainMessage(SEEK); + message.obj = new Integer(time); + sendMessage(message); + } + + /** + * Pause the playback. + */ + public void pause() { + Message message = obtainMessage(PAUSE); + sendMessage(message); + } + + /** + * Tear down this proxy object. + */ + public void teardown() { + // This is called by the C++ MediaPlayerPrivate dtor. + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + mNativePointer = 0; + } + + /** + * Load the poster image. + * @param url is the URL of the poster image. + */ + public void loadPoster(String url) { + if (url == null) { + Message message = obtainMessage(LOAD_DEFAULT_POSTER); + sendMessage(message); + return; + } + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + // Load the poster asynchronously + mPosterDownloader = new PosterDownloader(url, this); + mPosterDownloader.start(); + } + + /** + * The factory for HTML5VideoViewProxy instances. + * @param webViewCore is the WebViewCore that is requesting the proxy. + * + * @return a new HTML5VideoViewProxy object. + */ + public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { + return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); + } + + private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); + private native void nativeOnEnded(int nativePointer); + private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); +} diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java index 84dc9f0ae0e81af6e2a32bed8f13abe70b168c9e..1c17575033fe60b7e7fe622a4fc8832e8145c180 100644 --- a/core/java/android/webkit/HttpAuthHandler.java +++ b/core/java/android/webkit/HttpAuthHandler.java @@ -49,8 +49,8 @@ public class HttpAuthHandler extends Handler { // Message id for handling the user response - private final int AUTH_PROCEED = 100; - private final int AUTH_CANCEL = 200; + private static final int AUTH_PROCEED = 100; + private static final int AUTH_CANCEL = 200; /** * Creates a new HTTP authentication handler with an empty diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index c6ec2d27cfee4ed3520915d262ccbfef44ad0991..2f46f2b974d839d6eadbb3e56f6f7e809343800b 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -23,7 +23,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -class HttpDateTime { +/** {@hide} */ +public final class HttpDateTime { /* * Regular expression for parsing HTTP-date. @@ -47,14 +48,16 @@ class HttpDateTime { * Wdy, DD Mon YYYY HH:MM:SS * Wdy Mon (SP)D HH:MM:SS YYYY * Wdy Mon DD HH:MM:SS YYYY GMT + * + * HH can be H if the first digit is zero. */ private static final String HTTP_DATE_RFC_REGEXP = "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])"; + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; private static final String HTTP_DATE_ANSIC_REGEXP = "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; /** * The compiled version of the HTTP-date regular expressions. @@ -65,6 +68,12 @@ class HttpDateTime { Pattern.compile(HTTP_DATE_ANSIC_REGEXP); private static class TimeOfDay { + TimeOfDay(int h, int m, int s) { + this.hour = h; + this.minute = m; + this.second = s; + } + int hour; int minute; int second; @@ -76,7 +85,7 @@ class HttpDateTime { int date = 1; int month = Calendar.JANUARY; int year = 1970; - TimeOfDay timeOfDay = new TimeOfDay(); + TimeOfDay timeOfDay; Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); if (rfcMatcher.find()) { @@ -175,21 +184,39 @@ class HttpDateTime { } else { return year + 2000; } - } else - return (yearString.charAt(0) - '0') * 1000 + } else if (yearString.length() == 3) { + // According to RFC 2822, three digit years should be added to 1900. + int year = (yearString.charAt(0) - '0') * 100 + + (yearString.charAt(1) - '0') * 10 + + (yearString.charAt(2) - '0'); + return year + 1900; + } else if (yearString.length() == 4) { + return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0'); + } else { + return 1970; + } } private static TimeOfDay getTime(String timeString) { - TimeOfDay time = new TimeOfDay(); - time.hour = (timeString.charAt(0) - '0') * 10 - + (timeString.charAt(1) - '0'); - time.minute = (timeString.charAt(3) - '0') * 10 - + (timeString.charAt(4) - '0'); - time.second = (timeString.charAt(6) - '0') * 10 - + (timeString.charAt(7) - '0'); - return time; + // HH might be H + int i = 0; + int hour = timeString.charAt(i++) - '0'; + if (timeString.charAt(i) != ':') + hour = hour * 10 + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int minute = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int second = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + + return new TimeOfDay(hour, minute, second); } } diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 1dbd007886f13ae8b9322c3aed1b0a91d6283417..f350d139edcc4e4d18cc46bef857840fd1414aad 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -16,9 +16,9 @@ package android.webkit; +import android.content.Context; import android.os.Handler; import android.os.Message; -import android.security.CertTool; import android.util.Log; final class JWebCoreJavaBridge extends Handler { @@ -34,14 +34,24 @@ final class JWebCoreJavaBridge extends Handler { // Instant timer is used to implement a timer that needs to fire almost // immediately. private boolean mHasInstantTimer; + // Reference count the pause/resume of timers private int mPauseTimerRefCount; + private boolean mTimerPaused; + private boolean mHasDeferredTimers; + + private Context mContext; + + /* package */ + static final int REFRESH_PLUGINS = 100; + /** * Construct a new JWebCoreJavaBridge to interface with * WebCore timers and cookies. */ - public JWebCoreJavaBridge() { + public JWebCoreJavaBridge(Context context) { + mContext = context; nativeConstructor(); } @@ -50,6 +60,17 @@ final class JWebCoreJavaBridge extends Handler { nativeFinalize(); } + /** + * Call native timer callbacks. + */ + private void fireSharedTimer() { + PerfChecker checker = new PerfChecker(); + // clear the flag so that sharedTimerFired() can set a new timer + mHasInstantTimer = false; + sharedTimerFired(); + checker.responseAlert("sharedTimer"); + } + /** * handleMessage * @param msg The dispatched message. @@ -60,16 +81,21 @@ final class JWebCoreJavaBridge extends Handler { public void handleMessage(Message msg) { switch (msg.what) { case TIMER_MESSAGE: { - PerfChecker checker = new PerfChecker(); - // clear the flag so that sharedTimerFired() can set a new timer - mHasInstantTimer = false; - sharedTimerFired(); - checker.responseAlert("sharedTimer"); + if (mTimerPaused) { + mHasDeferredTimers = true; + } else { + fireSharedTimer(); + } break; } case FUNCPTR_MESSAGE: nativeServiceFuncPtrQueue(); break; + case REFRESH_PLUGINS: + nativeUpdatePluginDirectories(PluginManager.getInstance(null) + .getPluginDirectories(), ((Boolean) msg.obj) + .booleanValue()); + break; } } @@ -86,7 +112,8 @@ final class JWebCoreJavaBridge extends Handler { */ public void pause() { if (--mPauseTimerRefCount == 0) { - setDeferringTimers(true); + mTimerPaused = true; + mHasDeferredTimers = false; } } @@ -95,7 +122,11 @@ final class JWebCoreJavaBridge extends Handler { */ public void resume() { if (++mPauseTimerRefCount == 1) { - setDeferringTimers(false); + mTimerPaused = false; + if (mHasDeferredTimers) { + mHasDeferredTimers = false; + fireSharedTimer(); + } } } @@ -108,10 +139,9 @@ final class JWebCoreJavaBridge extends Handler { /** * Store a cookie string associated with a url. * @param url The url to be used as a key for the cookie. - * @param docUrl The policy base url used by WebCore. * @param value The cookie string to be stored. */ - private void setCookies(String url, String docUrl, String value) { + private void setCookies(String url, String value) { if (value.contains("\r") || value.contains("\n")) { // for security reason, filter out '\r' and '\n' from the cookie int size = value.length(); @@ -151,12 +181,26 @@ final class JWebCoreJavaBridge extends Handler { return CookieManager.getInstance().acceptCookie(); } + /** + * Returns an array of plugin directoies + */ + private String[] getPluginDirectories() { + return PluginManager.getInstance(null).getPluginDirectories(); + } + + /** + * Returns the path of the plugin data directory + */ + private String getPluginSharedDataDirectory() { + return PluginManager.getInstance(null).getPluginSharedDataDirectory(); + } + /** * setSharedTimer * @param timemillis The relative time when the timer should fire */ private void setSharedTimer(long timemillis) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "setSharedTimer " + timemillis); + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) Log.v(LOGTAG, "setSharedTimer " + timemillis); if (timemillis <= 0) { // we don't accumulate the sharedTimer unless it is a delayed @@ -180,25 +224,27 @@ final class JWebCoreJavaBridge extends Handler { * Stop the shared timer. */ private void stopSharedTimer() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) { Log.v(LOGTAG, "stopSharedTimer removing all timers"); } removeMessages(TIMER_MESSAGE); mHasInstantTimer = false; + mHasDeferredTimers = false; } private String[] getKeyStrengthList() { - return CertTool.getInstance().getSupportedKeyStrenghs(); + return CertTool.getKeyStrengthList(); } private String getSignedPublicKey(int index, String challenge, String url) { // generateKeyPair expects organizations which we don't have. Ignore url. - return CertTool.getInstance().generateKeyPair(index, challenge, null); + return CertTool.getSignedPublicKey(mContext, index, challenge); } private native void nativeConstructor(); private native void nativeFinalize(); private native void sharedTimerFired(); - private native void setDeferringTimers(boolean defer); + private native void nativeUpdatePluginDirectories(String[] directories, + boolean reload); public native void setNetworkOnLine(boolean online); } diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index c3f3594957b787b77a2414588a8dd91f79dcce0e..4c17f997c61bf654449c3cf8c4ffef3ad2329beb 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -28,7 +28,6 @@ import android.net.http.SslError; import android.os.Handler; import android.os.Message; -import android.security.CertTool; import android.util.Log; import android.webkit.CacheManager.CacheResult; @@ -37,14 +36,11 @@ import com.android.internal.R; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Vector; import java.util.regex.Pattern; import java.util.regex.Matcher; -import org.apache.commons.codec.binary.Base64; - class LoadListener extends Handler implements EventHandler { private static final String LOGTAG = "webkit"; @@ -72,12 +68,12 @@ class LoadListener extends Handler implements EventHandler { private static final int HTTP_NOT_FOUND = 404; private static final int HTTP_PROXY_AUTH = 407; - private static HashSet sCertificateMimeTypeMap; + private static HashMap sCertificateTypeMap; static { - sCertificateMimeTypeMap = new HashSet(); - sCertificateMimeTypeMap.add("application/x-x509-ca-cert"); - sCertificateMimeTypeMap.add("application/x-x509-user-cert"); - sCertificateMimeTypeMap.add("application/x-pkcs12"); + sCertificateTypeMap = new HashMap(); + sCertificateTypeMap.put("application/x-x509-ca-cert", CertTool.CERT); + sCertificateTypeMap.put("application/x-x509-user-cert", CertTool.CERT); + sCertificateTypeMap.put("application/x-pkcs12", CertTool.PKCS12); } private static int sNativeLoaderCount; @@ -101,6 +97,7 @@ class LoadListener extends Handler implements EventHandler { private boolean mAuthFailed; // indicates that the prev. auth failed private CacheLoader mCacheLoader; private CacheManager.CacheResult mCacheResult; + private boolean mFromCache = false; private HttpAuthHeader mAuthHeader; private int mErrorID = OK; private String mErrorDescription; @@ -113,7 +110,6 @@ class LoadListener extends Handler implements EventHandler { private String mMethod; private Map mRequestHeaders; private byte[] mPostData; - private boolean mIsHighPriority; // Flag to indicate that this load is synchronous. private boolean mSynchronous; private Vector mMessageQueue; @@ -142,15 +138,13 @@ class LoadListener extends Handler implements EventHandler { LoadListener(Context context, BrowserFrame frame, String url, int nativeLoader, boolean synchronous, boolean isMainPageLoader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } mContext = context; mBrowserFrame = frame; setUrl(url); mNativeLoader = nativeLoader; - mMimeType = ""; - mEncoding = ""; mSynchronous = synchronous; if (synchronous) { mMessageQueue = new Vector(); @@ -293,7 +287,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void headers(Headers headers) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "LoadListener.headers"); + if (DebugFlags.LOAD_LISTENER) Log.v(LOGTAG, "LoadListener.headers"); sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); } @@ -301,8 +295,6 @@ class LoadListener extends Handler implements EventHandler { private void handleHeaders(Headers headers) { if (mCancelled) return; mHeaders = headers; - mMimeType = ""; - mEncoding = ""; ArrayList cookies = headers.getSetCookie(); for (int i = 0; i < cookies.size(); ++i) { @@ -322,8 +314,8 @@ class LoadListener extends Handler implements EventHandler { // If we have one of "generic" MIME types, try to deduce // the right MIME type from the file extension (if any): - if (mMimeType.equalsIgnoreCase("text/plain") || - mMimeType.equalsIgnoreCase("application/octet-stream")) { + if (mMimeType.equals("text/plain") || + mMimeType.equals("application/octet-stream")) { // for attachment, use the filename in the Content-Disposition // to guess the mimetype @@ -339,17 +331,14 @@ class LoadListener extends Handler implements EventHandler { if (newMimeType != null) { mMimeType = newMimeType; } - } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml")) { + } else if (mMimeType.equals("text/vnd.wap.wml")) { // As we don't support wml, render it as plain text mMimeType = "text/plain"; } else { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. // It seems that xhtml+xml and vnd.wap.xhtml+xml mime // subtypes are used interchangeably. So treat them the same. - if (mMimeType.equalsIgnoreCase("application/xhtml+xml") || - mMimeType.equals("application/vnd.wap.xhtml+xml")) { - mMimeType = "text/html"; + if (mMimeType.equals("application/vnd.wap.xhtml+xml")) { + mMimeType = "application/xhtml+xml"; } } } else { @@ -419,11 +408,10 @@ class LoadListener extends Handler implements EventHandler { mStatusCode == HTTP_MOVED_PERMANENTLY || mStatusCode == HTTP_TEMPORARY_REDIRECT) && mNativeLoader != 0) { - // Content arriving from a StreamLoader (eg File, Cache or Data) - // will not be cached as they have the header: - // cache-control: no-store - mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, - headers, mMimeType, false); + if (!mFromCache && mRequestHandle != null) { + mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, + headers, mMimeType, false); + } if (mCacheResult != null) { mCacheResult.encoding = mEncoding; } @@ -450,7 +438,7 @@ class LoadListener extends Handler implements EventHandler { */ public void status(int majorVersion, int minorVersion, int code, /* Status-Code value */ String reasonPhrase) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener: from: " + mUrl + " major: " + majorVersion + " minor: " + minorVersion @@ -464,6 +452,9 @@ class LoadListener extends Handler implements EventHandler { status.put("reason", reasonPhrase); // New status means new data. Clear the old. mDataBuilder.clear(); + mMimeType = ""; + mEncoding = ""; + mTransferEncoding = ""; sendMessageInternal(obtainMessage(MSG_STATUS, status)); } @@ -507,7 +498,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void error(int id, String description) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.error url:" + url() + " id:" + id + " description:" + description); } @@ -535,23 +526,10 @@ class LoadListener extends Handler implements EventHandler { * mDataBuilder is a thread-safe structure. */ public void data(byte[] data, int length) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.data(): url: " + url()); } - // Decode base64 data - // Note: It's fine that we only decode base64 here and not in the other - // data call because the only caller of the stream version is not - // base64 encoded. - if ("base64".equalsIgnoreCase(mTransferEncoding)) { - if (length < data.length) { - byte[] trimmedData = new byte[length]; - System.arraycopy(data, 0, trimmedData, 0, length); - data = trimmedData; - } - data = Base64.decodeBase64(data); - length = data.length; - } // Synchronize on mData because commitLoad may write mData to WebCore // and we don't want to replace mData or mDataLength at the same time // as a write. @@ -573,7 +551,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void endData() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); } sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); @@ -626,7 +604,8 @@ class LoadListener extends Handler implements EventHandler { // before calling it. if (mCacheLoader != null) { mCacheLoader.load(); - if (WebView.LOGV_ENABLED) { + mFromCache = true; + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener cache load url=" + url()); } return; @@ -646,6 +625,7 @@ class LoadListener extends Handler implements EventHandler { * serviced by the Cache. */ /* package */ void setCacheLoader(CacheLoader c) { mCacheLoader = c; + mFromCache = true; } /** @@ -662,6 +642,8 @@ class LoadListener extends Handler implements EventHandler { // Go ahead and set the cache loader to null in case the result is // null. mCacheLoader = null; + // reset the flag + mFromCache = false; if (result != null) { // The contents of the cache may need to be revalidated so just @@ -676,12 +658,13 @@ class LoadListener extends Handler implements EventHandler { CacheManager.HEADER_KEY_IFNONEMATCH) && !headers.containsKey( CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + "and usable: " + url()); } // Load the cached file mCacheLoader.load(); + mFromCache = true; return true; } } @@ -695,12 +678,23 @@ class LoadListener extends Handler implements EventHandler { * directly */ public boolean handleSslErrorRequest(SslError error) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleSslErrorRequest(): url:" + url() + " primary error: " + error.getPrimaryError() + " certificate: " + error.getCertificate()); } + // Check the cached preference table before sending a message. This + // will prevent waiting for an already available answer. + if (Network.getInstance(mContext).checkSslPrefTable(this, error)) { + return true; + } + // Do not post a message for a synchronous request. This will cause a + // deadlock. Just bail on the request. + if (isSynchronous()) { + mRequestHandle.handleSslErrorResponse(false); + return true; + } sendMessageInternal(obtainMessage(MSG_SSL_ERROR, error)); // if it has been canceled, return false so that the network thread // won't be blocked. If it is not canceled, save the mRequestHandle @@ -773,7 +767,7 @@ class LoadListener extends Handler implements EventHandler { * are null, cancel the request. */ void handleAuthResponse(String username, String password) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl + " username: " + username + " password: " + password); @@ -823,14 +817,12 @@ class LoadListener extends Handler implements EventHandler { * @param method * @param headers * @param postData - * @param isHighPriority */ void setRequestData(String method, Map headers, - byte[] postData, boolean isHighPriority) { + byte[] postData) { mMethod = method; mRequestHeaders = headers; mPostData = postData; - mIsHighPriority = isHighPriority; } /** @@ -870,7 +862,7 @@ class LoadListener extends Handler implements EventHandler { } void attachRequestHandle(RequestHandle requestHandle) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + "requestHandle: " + requestHandle); } @@ -878,7 +870,7 @@ class LoadListener extends Handler implements EventHandler { } void detachRequestHandle() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + "requestHandle: " + mRequestHandle); } @@ -917,7 +909,7 @@ class LoadListener extends Handler implements EventHandler { */ static boolean willLoadFromCache(String url) { boolean inCache = CacheManager.getCacheFile(url, null) != null; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + inCache); } @@ -938,6 +930,10 @@ class LoadListener extends Handler implements EventHandler { return mMimeType; } + String transferEncoding() { + return mTransferEncoding; + } + /* * Return the size of the content being downloaded. This represents the * full content size, even under the situation where the download has been @@ -965,9 +961,9 @@ class LoadListener extends Handler implements EventHandler { // This commits the headers without checking the response status code. private void commitHeaders() { - if (mIsMainPageLoader && sCertificateMimeTypeMap.contains(mMimeType)) { + if (mIsMainPageLoader && sCertificateTypeMap.containsKey(mMimeType)) { // In the case of downloading certificate, we will save it to the - // Keystore in commitLoad. Do not call webcore. + // KeyStore in commitLoad. Do not call webcore. return; } @@ -992,8 +988,7 @@ class LoadListener extends Handler implements EventHandler { // pass content-type content-length and content-encoding final int nativeResponse = nativeCreateResponse( mUrl, statusCode, mStatusText, - mMimeType, mContentLength, mEncoding, - mCacheResult == null ? 0 : mCacheResult.expires / 1000); + mMimeType, mContentLength, mEncoding); if (mHeaders != null) { mHeaders.getHeaders(new Headers.HeaderCallback() { public void header(String name, String value) { @@ -1011,26 +1006,28 @@ class LoadListener extends Handler implements EventHandler { private void commitLoad() { if (mCancelled) return; - if (mIsMainPageLoader && sCertificateMimeTypeMap.contains(mMimeType)) { - // In the case of downloading certificate, we will save it to the - // Keystore and stop the current loading so that it will not - // generate a new history page - byte[] cert = new byte[mDataBuilder.getByteSize()]; - int position = 0; - ByteArrayBuilder.Chunk c; - while (true) { - c = mDataBuilder.getFirstChunk(); - if (c == null) break; - - if (c.mLength != 0) { - System.arraycopy(c.mArray, 0, cert, position, c.mLength); - position += c.mLength; + if (mIsMainPageLoader) { + String type = sCertificateTypeMap.get(mMimeType); + if (type != null) { + // In the case of downloading certificate, we will save it to + // the KeyStore and stop the current loading so that it will not + // generate a new history page + byte[] cert = new byte[mDataBuilder.getByteSize()]; + int offset = 0; + while (true) { + ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + System.arraycopy(c.mArray, 0, cert, offset, c.mLength); + offset += c.mLength; + } + mDataBuilder.releaseChunk(c); } - mDataBuilder.releaseChunk(c); + CertTool.addCertificate(mContext, type, cert); + mBrowserFrame.stopLoading(); + return; } - CertTool.getInstance().addCertificate(cert, mContext); - mBrowserFrame.stopLoading(); - return; } // Give the data to WebKit now @@ -1115,7 +1112,7 @@ class LoadListener extends Handler implements EventHandler { * EventHandler's method call. */ public void cancel() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { if (mRequestHandle == null) { Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); } else { @@ -1221,7 +1218,7 @@ class LoadListener extends Handler implements EventHandler { // Network.requestURL. Network network = Network.getInstance(getContext()); if (!network.requestURL(mMethod, mRequestHeaders, - mPostData, this, mIsHighPriority)) { + mPostData, this)) { // Signal a bad url error if we could not load the // redirection. handleError(EventHandler.ERROR_BAD_URL, @@ -1247,7 +1244,7 @@ class LoadListener extends Handler implements EventHandler { tearDown(); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + redirectTo); } @@ -1260,8 +1257,8 @@ class LoadListener extends Handler implements EventHandler { private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^((?:[xX]-)?[a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); - private void parseContentTypeHeader(String contentType) { - if (WebView.LOGV_ENABLED) { + /* package */ void parseContentTypeHeader(String contentType) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + "contentType: " + contentType); } @@ -1282,13 +1279,14 @@ class LoadListener extends Handler implements EventHandler { mEncoding = contentType.substring(i + 1); } // Trim excess whitespace. - mEncoding = mEncoding.trim(); + mEncoding = mEncoding.trim().toLowerCase(); if (i < contentType.length() - 1) { // for data: uri the mimeType and encoding have // the form image/jpeg;base64 or text/plain;charset=utf-8 // or text/html;charset=utf-8;base64 - mTransferEncoding = contentType.substring(i + 1).trim(); + mTransferEncoding = + contentType.substring(i + 1).trim().toLowerCase(); } } else { mMimeType = contentType; @@ -1308,6 +1306,8 @@ class LoadListener extends Handler implements EventHandler { guessMimeType(); } } + // Ensure mMimeType is lower case. + mMimeType = mMimeType.toLowerCase(); } /** @@ -1397,7 +1397,8 @@ class LoadListener extends Handler implements EventHandler { */ private boolean ignoreCallbacks() { return (mCancelled || mAuthHeader != null || - (mStatusCode > 300 && mStatusCode < 400)); + // Allow 305 (Use Proxy) to call through. + (mStatusCode > 300 && mStatusCode < 400 && mStatusCode != 305)); } /** @@ -1438,7 +1439,7 @@ class LoadListener extends Handler implements EventHandler { mMimeType = "text/html"; String newMimeType = guessMimeTypeFromExtension(mUrl); if (newMimeType != null) { - mMimeType = newMimeType; + mMimeType = newMimeType; } } } @@ -1448,23 +1449,12 @@ class LoadListener extends Handler implements EventHandler { */ private String guessMimeTypeFromExtension(String url) { // PENDING: need to normalize url - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "guessMimeTypeFromExtension: url = " + url); } - String mimeType = - MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(url)); - - if (mimeType != null) { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. - if (mimeType.equals("application/xhtml+xml")) { - mimeType = "text/html"; - } - } - - return mimeType; + return MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(url)); } /** @@ -1483,7 +1473,7 @@ class LoadListener extends Handler implements EventHandler { * Cycle through our messages for synchronous loads. */ /* package */ void loadSynchronousMessages() { - if (WebView.DEBUG && !mSynchronous) { + if (DebugFlags.LOAD_LISTENER && !mSynchronous) { throw new AssertionError(); } // Note: this can be called twice if it is a synchronous network load, @@ -1510,12 +1500,11 @@ class LoadListener extends Handler implements EventHandler { * @param expectedLength An estimate of the content length or the length * given by the server. * @param encoding HTTP encoding. - * @param expireTime HTTP expires converted to seconds since the epoch. * @return The native response pointer. */ private native int nativeCreateResponse(String url, int statusCode, String statusText, String mimeType, long expectedLength, - String encoding, long expireTime); + String encoding); /** * Add a response header to the native object. diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index 9fdde6192e672aa50a0c9b2dc99d0e364e163aca..fffba1bdfc050ed1b15b1d8855292c1f2f0ca040 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; /** * Two-way map that maps MIME-types to file extensions and vice versa. */ -public /* package */ class MimeTypeMap { +public class MimeTypeMap { /** * Singleton MIME-type map instance: @@ -39,7 +39,6 @@ public /* package */ class MimeTypeMap { */ private HashMap mExtensionToMimeTypeMap; - /** * Creates a new MIME-type map. */ @@ -50,7 +49,10 @@ public /* package */ class MimeTypeMap { /** * Returns the file extension or an empty string iff there is no - * extension. + * extension. This method is a convenience method for obtaining the + * extension of a url and has undefined results for other Strings. + * @param url + * @return The file extension of the given url. */ public static String getFileExtensionFromUrl(String url) { if (url != null && url.length() > 0) { @@ -80,8 +82,7 @@ public /* package */ class MimeTypeMap { * Load an entry into the map. This does not check if the item already * exists, it trusts the caller! */ - private void loadEntry(String mimeType, String extension, - boolean textType) { + private void loadEntry(String mimeType, String extension) { // // if we have an existing x --> y mapping, we do not want to // override it with another mapping x --> ? @@ -94,18 +95,12 @@ public /* package */ class MimeTypeMap { mMimeTypeToExtensionMap.put(mimeType, extension); } - // - // here, we don't want to map extensions to text MIME types; - // otherwise, we will start replacing generic text/plain and - // text/html with text MIME types that our platform does not - // understand. - // - if (!textType) { - mExtensionToMimeTypeMap.put(extension, mimeType); - } + mExtensionToMimeTypeMap.put(extension, mimeType); } /** + * Return true if the given MIME type has an entry in the map. + * @param mimeType A MIME type (i.e. text/plain) * @return True iff there is a mimeType entry in the map. */ public boolean hasMimeType(String mimeType) { @@ -117,7 +112,9 @@ public /* package */ class MimeTypeMap { } /** - * @return The extension for the MIME type or null iff there is none. + * Return the MIME type for the given extension. + * @param extension A file extension without the leading '.' + * @return The MIME type for the given extension or null iff there is none. */ public String getMimeTypeFromExtension(String extension) { if (extension != null && extension.length() > 0) { @@ -128,18 +125,23 @@ public /* package */ class MimeTypeMap { } /** + * Return true if the given extension has a registered MIME type. + * @param extension A file extension without the leading '.' * @return True iff there is an extension entry in the map. */ public boolean hasExtension(String extension) { if (extension != null && extension.length() > 0) { return mExtensionToMimeTypeMap.containsKey(extension); } - return false; } /** - * @return The MIME type for the extension or null iff there is none. + * Return the registered extension for the given MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The extension for the given MIME type or null iff there is none. */ public String getExtensionFromMimeType(String mimeType) { if (mimeType != null && mimeType.length() > 0) { @@ -150,6 +152,7 @@ public /* package */ class MimeTypeMap { } /** + * Get the singleton instance of MimeTypeMap. * @return The singleton instance of the MIME-type map. */ public static MimeTypeMap getSingleton() { @@ -164,341 +167,341 @@ public /* package */ class MimeTypeMap { // mail.google.com/a/google.com // // and "active" MIME types (due to potential security issues). - // - // Also, notice that not all data from this table is actually - // added (see loadEntry method for more details). - sMimeTypeMap.loadEntry("application/andrew-inset", "ez", false); - sMimeTypeMap.loadEntry("application/dsptype", "tsp", false); - sMimeTypeMap.loadEntry("application/futuresplash", "spl", false); - sMimeTypeMap.loadEntry("application/hta", "hta", false); - sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx", false); - sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt", false); - sMimeTypeMap.loadEntry("application/mathematica", "nb", false); - sMimeTypeMap.loadEntry("application/msaccess", "mdb", false); - sMimeTypeMap.loadEntry("application/oda", "oda", false); - sMimeTypeMap.loadEntry("application/ogg", "ogg", false); - sMimeTypeMap.loadEntry("application/pdf", "pdf", false); - sMimeTypeMap.loadEntry("application/pgp-keys", "key", false); - sMimeTypeMap.loadEntry("application/pgp-signature", "pgp", false); - sMimeTypeMap.loadEntry("application/pics-rules", "prf", false); - sMimeTypeMap.loadEntry("application/rar", "rar", false); - sMimeTypeMap.loadEntry("application/rdf+xml", "rdf", false); - sMimeTypeMap.loadEntry("application/rss+xml", "rss", false); - sMimeTypeMap.loadEntry("application/zip", "zip", false); + sMimeTypeMap.loadEntry("application/andrew-inset", "ez"); + sMimeTypeMap.loadEntry("application/dsptype", "tsp"); + sMimeTypeMap.loadEntry("application/futuresplash", "spl"); + sMimeTypeMap.loadEntry("application/hta", "hta"); + sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx"); + sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt"); + sMimeTypeMap.loadEntry("application/mathematica", "nb"); + sMimeTypeMap.loadEntry("application/msaccess", "mdb"); + sMimeTypeMap.loadEntry("application/oda", "oda"); + sMimeTypeMap.loadEntry("application/ogg", "ogg"); + sMimeTypeMap.loadEntry("application/pdf", "pdf"); + sMimeTypeMap.loadEntry("application/pgp-keys", "key"); + sMimeTypeMap.loadEntry("application/pgp-signature", "pgp"); + sMimeTypeMap.loadEntry("application/pics-rules", "prf"); + sMimeTypeMap.loadEntry("application/rar", "rar"); + sMimeTypeMap.loadEntry("application/rdf+xml", "rdf"); + sMimeTypeMap.loadEntry("application/rss+xml", "rss"); + sMimeTypeMap.loadEntry("application/zip", "zip"); sMimeTypeMap.loadEntry("application/vnd.android.package-archive", - "apk", false); - sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy", false); - sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl", false); + "apk"); + sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy"); + sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.database", "odb", - false); + "application/vnd.oasis.opendocument.database", "odb"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.formula", "odf", - false); + "application/vnd.oasis.opendocument.formula", "odf"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.graphics", "odg", - false); + "application/vnd.oasis.opendocument.graphics", "odg"); sMimeTypeMap.loadEntry( "application/vnd.oasis.opendocument.graphics-template", - "otg", false); + "otg"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.image", "odi", false); + "application/vnd.oasis.opendocument.image", "odi"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.spreadsheet", "ods", - false); + "application/vnd.oasis.opendocument.spreadsheet", "ods"); sMimeTypeMap.loadEntry( "application/vnd.oasis.opendocument.spreadsheet-template", - "ots", false); + "ots"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text", "odt"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-master", "odm"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-template", "ott"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-web", "oth"); + sMimeTypeMap.loadEntry("application/msword", "doc"); + sMimeTypeMap.loadEntry("application/msword", "dot"); + sMimeTypeMap.loadEntry( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docx"); + sMimeTypeMap.loadEntry( + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "dotx"); + sMimeTypeMap.loadEntry("application/vnd.ms-excel", "xls"); + sMimeTypeMap.loadEntry("application/vnd.ms-excel", "xlt"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text", "odt", false); + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-master", "odm", - false); + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xltx"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "ppt"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "pot"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "pps"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-template", "ott", - false); + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pptx"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-web", "oth", - false); - sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod", false); - sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf", false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda", - false); + "application/vnd.openxmlformats-officedocument.presentationml.template", + "potx"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.impress", "sdd", false); + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppsx"); + sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod"); + sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.impress", "sdp", false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "sdw", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "vor", - false); + "application/vnd.stardivision.impress", "sdd"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.writer-global", "sgl", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc", - false); + "application/vnd.stardivision.impress", "sdp"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", + "sdw"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", + "vor"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.calc.template", "stc", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd", - false); + "application/vnd.stardivision.writer-global", "sgl"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.draw.template", "std", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi", - false); + "application/vnd.sun.xml.calc.template", "stc"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.impress.template", "sti", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm", - false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw", - false); + "application/vnd.sun.xml.draw.template", "std"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.writer.global", "sxg", false); + "application/vnd.sun.xml.impress.template", "sti"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.writer.template", "stw", false); - sMimeTypeMap.loadEntry("application/vnd.visio", "vsd", false); - sMimeTypeMap.loadEntry("application/x-abiword", "abw", false); - sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg", - false); - sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio", false); - sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent", - false); - sMimeTypeMap.loadEntry("application/x-cdf", "cdf", false); - sMimeTypeMap.loadEntry("application/x-cdlink", "vcd", false); - sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn", false); - sMimeTypeMap.loadEntry("application/x-cpio", "cpio", false); - sMimeTypeMap.loadEntry("application/x-debian-package", "deb", - false); - sMimeTypeMap.loadEntry("application/x-debian-package", "udeb", - false); - sMimeTypeMap.loadEntry("application/x-director", "dcr", false); - sMimeTypeMap.loadEntry("application/x-director", "dir", false); - sMimeTypeMap.loadEntry("application/x-director", "dxr", false); - sMimeTypeMap.loadEntry("application/x-dms", "dms", false); - sMimeTypeMap.loadEntry("application/x-doom", "wad", false); - sMimeTypeMap.loadEntry("application/x-dvi", "dvi", false); - sMimeTypeMap.loadEntry("application/x-flac", "flac", false); - sMimeTypeMap.loadEntry("application/x-font", "pfa", false); - sMimeTypeMap.loadEntry("application/x-font", "pfb", false); - sMimeTypeMap.loadEntry("application/x-font", "gsf", false); - sMimeTypeMap.loadEntry("application/x-font", "pcf", false); - sMimeTypeMap.loadEntry("application/x-font", "pcf.Z", false); - sMimeTypeMap.loadEntry("application/x-freemind", "mm", false); - sMimeTypeMap.loadEntry("application/x-futuresplash", "spl", false); - sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric", false); - sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf", false); - sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf", - false); - sMimeTypeMap.loadEntry("application/x-gtar", "gtar", false); - sMimeTypeMap.loadEntry("application/x-gtar", "tgz", false); - sMimeTypeMap.loadEntry("application/x-gtar", "taz", false); - sMimeTypeMap.loadEntry("application/x-hdf", "hdf", false); - sMimeTypeMap.loadEntry("application/x-ica", "ica", false); - sMimeTypeMap.loadEntry("application/x-internet-signup", "ins", - false); - sMimeTypeMap.loadEntry("application/x-internet-signup", "isp", - false); - sMimeTypeMap.loadEntry("application/x-iphone", "iii", false); - sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso", false); - sMimeTypeMap.loadEntry("application/x-jmol", "jmz", false); - sMimeTypeMap.loadEntry("application/x-kchart", "chrt", false); - sMimeTypeMap.loadEntry("application/x-killustrator", "kil", false); - sMimeTypeMap.loadEntry("application/x-koan", "skp", false); - sMimeTypeMap.loadEntry("application/x-koan", "skd", false); - sMimeTypeMap.loadEntry("application/x-koan", "skt", false); - sMimeTypeMap.loadEntry("application/x-koan", "skm", false); - sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr", false); - sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt", false); - sMimeTypeMap.loadEntry("application/x-kspread", "ksp", false); - sMimeTypeMap.loadEntry("application/x-kword", "kwd", false); - sMimeTypeMap.loadEntry("application/x-kword", "kwt", false); - sMimeTypeMap.loadEntry("application/x-latex", "latex", false); - sMimeTypeMap.loadEntry("application/x-lha", "lha", false); - sMimeTypeMap.loadEntry("application/x-lzh", "lzh", false); - sMimeTypeMap.loadEntry("application/x-lzx", "lzx", false); - sMimeTypeMap.loadEntry("application/x-maker", "frm", false); - sMimeTypeMap.loadEntry("application/x-maker", "maker", false); - sMimeTypeMap.loadEntry("application/x-maker", "frame", false); - sMimeTypeMap.loadEntry("application/x-maker", "fb", false); - sMimeTypeMap.loadEntry("application/x-maker", "book", false); - sMimeTypeMap.loadEntry("application/x-maker", "fbdoc", false); - sMimeTypeMap.loadEntry("application/x-mif", "mif", false); - sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd", false); - sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz", false); - sMimeTypeMap.loadEntry("application/x-msi", "msi", false); - sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac", - false); - sMimeTypeMap.loadEntry("application/x-nwc", "nwc", false); - sMimeTypeMap.loadEntry("application/x-object", "o", false); - sMimeTypeMap.loadEntry("application/x-oz-application", "oza", - false); - sMimeTypeMap.loadEntry("application/x-pkcs12", "p12", false); - sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r", - false); - sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl", false); - sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl", - false); - sMimeTypeMap.loadEntry("application/x-shar", "shar", false); - sMimeTypeMap.loadEntry("application/x-stuffit", "sit", false); - sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio", false); - sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc", false); - sMimeTypeMap.loadEntry("application/x-tar", "tar", false); - sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo", false); - sMimeTypeMap.loadEntry("application/x-texinfo", "texi", false); - sMimeTypeMap.loadEntry("application/x-troff", "t", false); - sMimeTypeMap.loadEntry("application/x-troff", "roff", false); - sMimeTypeMap.loadEntry("application/x-troff-man", "man", false); - sMimeTypeMap.loadEntry("application/x-ustar", "ustar", false); - sMimeTypeMap.loadEntry("application/x-wais-source", "src", false); - sMimeTypeMap.loadEntry("application/x-wingz", "wz", false); + "application/vnd.sun.xml.writer.global", "sxg"); sMimeTypeMap.loadEntry( - "application/x-webarchive", "webarchive", false); // added - sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt", false); - sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt", false); - sMimeTypeMap.loadEntry("application/x-xcf", "xcf", false); - sMimeTypeMap.loadEntry("application/x-xfig", "fig", false); - sMimeTypeMap.loadEntry("audio/basic", "snd", false); - sMimeTypeMap.loadEntry("audio/midi", "mid", false); - sMimeTypeMap.loadEntry("audio/midi", "midi", false); - sMimeTypeMap.loadEntry("audio/midi", "kar", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mpga", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mpega", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mp2", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mp3", false); - sMimeTypeMap.loadEntry("audio/mpeg", "m4a", false); - sMimeTypeMap.loadEntry("audio/mpegurl", "m3u", false); - sMimeTypeMap.loadEntry("audio/prs.sid", "sid", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aif", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aiff", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aifc", false); - sMimeTypeMap.loadEntry("audio/x-gsm", "gsm", false); - sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u", false); - sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma", false); - sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram", false); - sMimeTypeMap.loadEntry("audio/x-realaudio", "ra", false); - sMimeTypeMap.loadEntry("audio/x-scpls", "pls", false); - sMimeTypeMap.loadEntry("audio/x-sd2", "sd2", false); - sMimeTypeMap.loadEntry("audio/x-wav", "wav", false); - sMimeTypeMap.loadEntry("image/bmp", "bmp", false); // added - sMimeTypeMap.loadEntry("image/gif", "gif", false); - sMimeTypeMap.loadEntry("image/ico", "cur", false); // added - sMimeTypeMap.loadEntry("image/ico", "ico", false); // added - sMimeTypeMap.loadEntry("image/ief", "ief", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpeg", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpg", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpe", false); - sMimeTypeMap.loadEntry("image/pcx", "pcx", false); - sMimeTypeMap.loadEntry("image/png", "png", false); - sMimeTypeMap.loadEntry("image/svg+xml", "svg", false); - sMimeTypeMap.loadEntry("image/svg+xml", "svgz", false); - sMimeTypeMap.loadEntry("image/tiff", "tiff", false); - sMimeTypeMap.loadEntry("image/tiff", "tif", false); - sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu", false); - sMimeTypeMap.loadEntry("image/vnd.djvu", "djv", false); - sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp", false); - sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras", false); - sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr", false); - sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat", false); - sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt", false); - sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt", false); - sMimeTypeMap.loadEntry("image/x-icon", "ico", false); - sMimeTypeMap.loadEntry("image/x-jg", "art", false); - sMimeTypeMap.loadEntry("image/x-jng", "jng", false); - sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp", false); - sMimeTypeMap.loadEntry("image/x-photoshop", "psd", false); - sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm", false); - sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm", false); - sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm", false); - sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm", false); - sMimeTypeMap.loadEntry("image/x-rgb", "rgb", false); - sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm", false); - sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm", false); - sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd", false); - sMimeTypeMap.loadEntry("model/iges", "igs", false); - sMimeTypeMap.loadEntry("model/iges", "iges", false); - sMimeTypeMap.loadEntry("model/mesh", "msh", false); - sMimeTypeMap.loadEntry("model/mesh", "mesh", false); - sMimeTypeMap.loadEntry("model/mesh", "silo", false); - sMimeTypeMap.loadEntry("text/calendar", "ics", true); - sMimeTypeMap.loadEntry("text/calendar", "icz", true); - sMimeTypeMap.loadEntry("text/comma-separated-values", "csv", true); - sMimeTypeMap.loadEntry("text/css", "css", true); - sMimeTypeMap.loadEntry("text/h323", "323", true); - sMimeTypeMap.loadEntry("text/iuls", "uls", true); - sMimeTypeMap.loadEntry("text/mathml", "mml", true); + "application/vnd.sun.xml.writer.template", "stw"); + sMimeTypeMap.loadEntry("application/vnd.visio", "vsd"); + sMimeTypeMap.loadEntry("application/x-abiword", "abw"); + sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg"); + sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio"); + sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent"); + sMimeTypeMap.loadEntry("application/x-cdf", "cdf"); + sMimeTypeMap.loadEntry("application/x-cdlink", "vcd"); + sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn"); + sMimeTypeMap.loadEntry("application/x-cpio", "cpio"); + sMimeTypeMap.loadEntry("application/x-debian-package", "deb"); + sMimeTypeMap.loadEntry("application/x-debian-package", "udeb"); + sMimeTypeMap.loadEntry("application/x-director", "dcr"); + sMimeTypeMap.loadEntry("application/x-director", "dir"); + sMimeTypeMap.loadEntry("application/x-director", "dxr"); + sMimeTypeMap.loadEntry("application/x-dms", "dms"); + sMimeTypeMap.loadEntry("application/x-doom", "wad"); + sMimeTypeMap.loadEntry("application/x-dvi", "dvi"); + sMimeTypeMap.loadEntry("application/x-flac", "flac"); + sMimeTypeMap.loadEntry("application/x-font", "pfa"); + sMimeTypeMap.loadEntry("application/x-font", "pfb"); + sMimeTypeMap.loadEntry("application/x-font", "gsf"); + sMimeTypeMap.loadEntry("application/x-font", "pcf"); + sMimeTypeMap.loadEntry("application/x-font", "pcf.Z"); + sMimeTypeMap.loadEntry("application/x-freemind", "mm"); + sMimeTypeMap.loadEntry("application/x-futuresplash", "spl"); + sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric"); + sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf"); + sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf"); + sMimeTypeMap.loadEntry("application/x-gtar", "gtar"); + sMimeTypeMap.loadEntry("application/x-gtar", "tgz"); + sMimeTypeMap.loadEntry("application/x-gtar", "taz"); + sMimeTypeMap.loadEntry("application/x-hdf", "hdf"); + sMimeTypeMap.loadEntry("application/x-ica", "ica"); + sMimeTypeMap.loadEntry("application/x-internet-signup", "ins"); + sMimeTypeMap.loadEntry("application/x-internet-signup", "isp"); + sMimeTypeMap.loadEntry("application/x-iphone", "iii"); + sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso"); + sMimeTypeMap.loadEntry("application/x-jmol", "jmz"); + sMimeTypeMap.loadEntry("application/x-kchart", "chrt"); + sMimeTypeMap.loadEntry("application/x-killustrator", "kil"); + sMimeTypeMap.loadEntry("application/x-koan", "skp"); + sMimeTypeMap.loadEntry("application/x-koan", "skd"); + sMimeTypeMap.loadEntry("application/x-koan", "skt"); + sMimeTypeMap.loadEntry("application/x-koan", "skm"); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr"); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt"); + sMimeTypeMap.loadEntry("application/x-kspread", "ksp"); + sMimeTypeMap.loadEntry("application/x-kword", "kwd"); + sMimeTypeMap.loadEntry("application/x-kword", "kwt"); + sMimeTypeMap.loadEntry("application/x-latex", "latex"); + sMimeTypeMap.loadEntry("application/x-lha", "lha"); + sMimeTypeMap.loadEntry("application/x-lzh", "lzh"); + sMimeTypeMap.loadEntry("application/x-lzx", "lzx"); + sMimeTypeMap.loadEntry("application/x-maker", "frm"); + sMimeTypeMap.loadEntry("application/x-maker", "maker"); + sMimeTypeMap.loadEntry("application/x-maker", "frame"); + sMimeTypeMap.loadEntry("application/x-maker", "fb"); + sMimeTypeMap.loadEntry("application/x-maker", "book"); + sMimeTypeMap.loadEntry("application/x-maker", "fbdoc"); + sMimeTypeMap.loadEntry("application/x-mif", "mif"); + sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd"); + sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz"); + sMimeTypeMap.loadEntry("application/x-msi", "msi"); + sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac"); + sMimeTypeMap.loadEntry("application/x-nwc", "nwc"); + sMimeTypeMap.loadEntry("application/x-object", "o"); + sMimeTypeMap.loadEntry("application/x-oz-application", "oza"); + sMimeTypeMap.loadEntry("application/x-pkcs12", "p12"); + sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r"); + sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl"); + sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl"); + sMimeTypeMap.loadEntry("application/x-shar", "shar"); + sMimeTypeMap.loadEntry("application/x-stuffit", "sit"); + sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio"); + sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc"); + sMimeTypeMap.loadEntry("application/x-tar", "tar"); + sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo"); + sMimeTypeMap.loadEntry("application/x-texinfo", "texi"); + sMimeTypeMap.loadEntry("application/x-troff", "t"); + sMimeTypeMap.loadEntry("application/x-troff", "roff"); + sMimeTypeMap.loadEntry("application/x-troff-man", "man"); + sMimeTypeMap.loadEntry("application/x-ustar", "ustar"); + sMimeTypeMap.loadEntry("application/x-wais-source", "src"); + sMimeTypeMap.loadEntry("application/x-wingz", "wz"); + sMimeTypeMap.loadEntry("application/x-webarchive", "webarchive"); + sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt"); + sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt"); + sMimeTypeMap.loadEntry("application/x-xcf", "xcf"); + sMimeTypeMap.loadEntry("application/x-xfig", "fig"); + sMimeTypeMap.loadEntry("application/xhtml+xml", "xhtml"); + sMimeTypeMap.loadEntry("audio/basic", "snd"); + sMimeTypeMap.loadEntry("audio/midi", "mid"); + sMimeTypeMap.loadEntry("audio/midi", "midi"); + sMimeTypeMap.loadEntry("audio/midi", "kar"); + sMimeTypeMap.loadEntry("audio/mpeg", "mpga"); + sMimeTypeMap.loadEntry("audio/mpeg", "mpega"); + sMimeTypeMap.loadEntry("audio/mpeg", "mp2"); + sMimeTypeMap.loadEntry("audio/mpeg", "mp3"); + sMimeTypeMap.loadEntry("audio/mpeg", "m4a"); + sMimeTypeMap.loadEntry("audio/mpegurl", "m3u"); + sMimeTypeMap.loadEntry("audio/prs.sid", "sid"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aif"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aiff"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aifc"); + sMimeTypeMap.loadEntry("audio/x-gsm", "gsm"); + sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u"); + sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma"); + sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram"); + sMimeTypeMap.loadEntry("audio/x-realaudio", "ra"); + sMimeTypeMap.loadEntry("audio/x-scpls", "pls"); + sMimeTypeMap.loadEntry("audio/x-sd2", "sd2"); + sMimeTypeMap.loadEntry("audio/x-wav", "wav"); + sMimeTypeMap.loadEntry("image/bmp", "bmp"); + sMimeTypeMap.loadEntry("image/gif", "gif"); + sMimeTypeMap.loadEntry("image/ico", "cur"); + sMimeTypeMap.loadEntry("image/ico", "ico"); + sMimeTypeMap.loadEntry("image/ief", "ief"); + sMimeTypeMap.loadEntry("image/jpeg", "jpeg"); + sMimeTypeMap.loadEntry("image/jpeg", "jpg"); + sMimeTypeMap.loadEntry("image/jpeg", "jpe"); + sMimeTypeMap.loadEntry("image/pcx", "pcx"); + sMimeTypeMap.loadEntry("image/png", "png"); + sMimeTypeMap.loadEntry("image/svg+xml", "svg"); + sMimeTypeMap.loadEntry("image/svg+xml", "svgz"); + sMimeTypeMap.loadEntry("image/tiff", "tiff"); + sMimeTypeMap.loadEntry("image/tiff", "tif"); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu"); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djv"); + sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp"); + sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras"); + sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr"); + sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat"); + sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt"); + sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt"); + sMimeTypeMap.loadEntry("image/x-icon", "ico"); + sMimeTypeMap.loadEntry("image/x-jg", "art"); + sMimeTypeMap.loadEntry("image/x-jng", "jng"); + sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp"); + sMimeTypeMap.loadEntry("image/x-photoshop", "psd"); + sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm"); + sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm"); + sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm"); + sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm"); + sMimeTypeMap.loadEntry("image/x-rgb", "rgb"); + sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm"); + sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm"); + sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd"); + sMimeTypeMap.loadEntry("model/iges", "igs"); + sMimeTypeMap.loadEntry("model/iges", "iges"); + sMimeTypeMap.loadEntry("model/mesh", "msh"); + sMimeTypeMap.loadEntry("model/mesh", "mesh"); + sMimeTypeMap.loadEntry("model/mesh", "silo"); + sMimeTypeMap.loadEntry("text/calendar", "ics"); + sMimeTypeMap.loadEntry("text/calendar", "icz"); + sMimeTypeMap.loadEntry("text/comma-separated-values", "csv"); + sMimeTypeMap.loadEntry("text/css", "css"); + sMimeTypeMap.loadEntry("text/h323", "323"); + sMimeTypeMap.loadEntry("text/iuls", "uls"); + sMimeTypeMap.loadEntry("text/mathml", "mml"); // add it first so it will be the default for ExtensionFromMimeType - sMimeTypeMap.loadEntry("text/plain", "txt", true); - sMimeTypeMap.loadEntry("text/plain", "asc", true); - sMimeTypeMap.loadEntry("text/plain", "text", true); - sMimeTypeMap.loadEntry("text/plain", "diff", true); - sMimeTypeMap.loadEntry("text/plain", "pot", true); - sMimeTypeMap.loadEntry("text/richtext", "rtx", true); - sMimeTypeMap.loadEntry("text/rtf", "rtf", true); - sMimeTypeMap.loadEntry("text/texmacs", "ts", true); - sMimeTypeMap.loadEntry("text/text", "phps", true); - sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv", true); - sMimeTypeMap.loadEntry("text/x-bibtex", "bib", true); - sMimeTypeMap.loadEntry("text/x-boo", "boo", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "h++", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hh", true); - sMimeTypeMap.loadEntry("text/x-c++src", "c++", true); - sMimeTypeMap.loadEntry("text/x-c++src", "cpp", true); - sMimeTypeMap.loadEntry("text/x-c++src", "cxx", true); - sMimeTypeMap.loadEntry("text/x-chdr", "h", true); - sMimeTypeMap.loadEntry("text/x-component", "htc", true); - sMimeTypeMap.loadEntry("text/x-csh", "csh", true); - sMimeTypeMap.loadEntry("text/x-csrc", "c", true); - sMimeTypeMap.loadEntry("text/x-dsrc", "d", true); - sMimeTypeMap.loadEntry("text/x-haskell", "hs", true); - sMimeTypeMap.loadEntry("text/x-java", "java", true); - sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs", true); - sMimeTypeMap.loadEntry("text/x-moc", "moc", true); - sMimeTypeMap.loadEntry("text/x-pascal", "p", true); - sMimeTypeMap.loadEntry("text/x-pascal", "pas", true); - sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd", true); - sMimeTypeMap.loadEntry("text/x-setext", "etx", true); - sMimeTypeMap.loadEntry("text/x-tcl", "tcl", true); - sMimeTypeMap.loadEntry("text/x-tex", "tex", true); - sMimeTypeMap.loadEntry("text/x-tex", "ltx", true); - sMimeTypeMap.loadEntry("text/x-tex", "sty", true); - sMimeTypeMap.loadEntry("text/x-tex", "cls", true); - sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs", true); - sMimeTypeMap.loadEntry("text/x-vcard", "vcf", true); - sMimeTypeMap.loadEntry("video/3gpp", "3gp", false); - sMimeTypeMap.loadEntry("video/3gpp", "3g2", false); - sMimeTypeMap.loadEntry("video/dl", "dl", false); - sMimeTypeMap.loadEntry("video/dv", "dif", false); - sMimeTypeMap.loadEntry("video/dv", "dv", false); - sMimeTypeMap.loadEntry("video/fli", "fli", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpeg", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpg", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpe", false); - sMimeTypeMap.loadEntry("video/mp4", "mp4", false); - sMimeTypeMap.loadEntry("video/mpeg", "VOB", false); - sMimeTypeMap.loadEntry("video/quicktime", "qt", false); - sMimeTypeMap.loadEntry("video/quicktime", "mov", false); - sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu", false); - sMimeTypeMap.loadEntry("video/x-la-asf", "lsf", false); - sMimeTypeMap.loadEntry("video/x-la-asf", "lsx", false); - sMimeTypeMap.loadEntry("video/x-mng", "mng", false); - sMimeTypeMap.loadEntry("video/x-ms-asf", "asf", false); - sMimeTypeMap.loadEntry("video/x-ms-asf", "asx", false); - sMimeTypeMap.loadEntry("video/x-ms-wm", "wm", false); - sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv", false); - sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx", false); - sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx", false); - sMimeTypeMap.loadEntry("video/x-msvideo", "avi", false); - sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie", false); - sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice", false); - sMimeTypeMap.loadEntry("x-epoc/x-sisx-app", "sisx", false); + sMimeTypeMap.loadEntry("text/plain", "txt"); + sMimeTypeMap.loadEntry("text/plain", "asc"); + sMimeTypeMap.loadEntry("text/plain", "text"); + sMimeTypeMap.loadEntry("text/plain", "diff"); + sMimeTypeMap.loadEntry("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint + sMimeTypeMap.loadEntry("text/richtext", "rtx"); + sMimeTypeMap.loadEntry("text/rtf", "rtf"); + sMimeTypeMap.loadEntry("text/texmacs", "ts"); + sMimeTypeMap.loadEntry("text/text", "phps"); + sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv"); + sMimeTypeMap.loadEntry("text/xml", "xml"); + sMimeTypeMap.loadEntry("text/x-bibtex", "bib"); + sMimeTypeMap.loadEntry("text/x-boo", "boo"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "h++"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hh"); + sMimeTypeMap.loadEntry("text/x-c++src", "c++"); + sMimeTypeMap.loadEntry("text/x-c++src", "cpp"); + sMimeTypeMap.loadEntry("text/x-c++src", "cxx"); + sMimeTypeMap.loadEntry("text/x-chdr", "h"); + sMimeTypeMap.loadEntry("text/x-component", "htc"); + sMimeTypeMap.loadEntry("text/x-csh", "csh"); + sMimeTypeMap.loadEntry("text/x-csrc", "c"); + sMimeTypeMap.loadEntry("text/x-dsrc", "d"); + sMimeTypeMap.loadEntry("text/x-haskell", "hs"); + sMimeTypeMap.loadEntry("text/x-java", "java"); + sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs"); + sMimeTypeMap.loadEntry("text/x-moc", "moc"); + sMimeTypeMap.loadEntry("text/x-pascal", "p"); + sMimeTypeMap.loadEntry("text/x-pascal", "pas"); + sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd"); + sMimeTypeMap.loadEntry("text/x-setext", "etx"); + sMimeTypeMap.loadEntry("text/x-tcl", "tcl"); + sMimeTypeMap.loadEntry("text/x-tex", "tex"); + sMimeTypeMap.loadEntry("text/x-tex", "ltx"); + sMimeTypeMap.loadEntry("text/x-tex", "sty"); + sMimeTypeMap.loadEntry("text/x-tex", "cls"); + sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs"); + sMimeTypeMap.loadEntry("text/x-vcard", "vcf"); + sMimeTypeMap.loadEntry("video/3gpp", "3gp"); + sMimeTypeMap.loadEntry("video/3gpp", "3g2"); + sMimeTypeMap.loadEntry("video/dl", "dl"); + sMimeTypeMap.loadEntry("video/dv", "dif"); + sMimeTypeMap.loadEntry("video/dv", "dv"); + sMimeTypeMap.loadEntry("video/fli", "fli"); + sMimeTypeMap.loadEntry("video/mpeg", "mpeg"); + sMimeTypeMap.loadEntry("video/mpeg", "mpg"); + sMimeTypeMap.loadEntry("video/mpeg", "mpe"); + sMimeTypeMap.loadEntry("video/mp4", "mp4"); + sMimeTypeMap.loadEntry("video/mpeg", "VOB"); + sMimeTypeMap.loadEntry("video/quicktime", "qt"); + sMimeTypeMap.loadEntry("video/quicktime", "mov"); + sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu"); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsf"); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsx"); + sMimeTypeMap.loadEntry("video/x-mng", "mng"); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asf"); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asx"); + sMimeTypeMap.loadEntry("video/x-ms-wm", "wm"); + sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv"); + sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx"); + sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx"); + sMimeTypeMap.loadEntry("video/x-msvideo", "avi"); + sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie"); + sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice"); + sMimeTypeMap.loadEntry("x-epoc/x-sisx-app", "sisx"); } return sMimeTypeMap; diff --git a/core/java/android/webkit/MockGeolocation.java b/core/java/android/webkit/MockGeolocation.java new file mode 100644 index 0000000000000000000000000000000000000000..fbda4924da0e8be62d5b1b88a167b12f7f44fcc0 --- /dev/null +++ b/core/java/android/webkit/MockGeolocation.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * This class is simply a container for the methods used to configure WebKit's + * mock Geolocation service for use in LayoutTests. + * @hide + */ +public final class MockGeolocation { + + // Global instance of a MockGeolocation + private static MockGeolocation sMockGeolocation; + + /** + * Set the position for the mock Geolocation service. + */ + public void setPosition(double latitude, double longitude, double accuracy) { + // This should only ever be called on the WebKit thread. + nativeSetPosition(latitude, longitude, accuracy); + } + + /** + * Set the error for the mock Geolocation service. + */ + public void setError(int code, String message) { + // This should only ever be called on the WebKit thread. + nativeSetError(code, message); + } + + /** + * Get the global instance of MockGeolocation. + * @return The global MockGeolocation instance. + */ + public static MockGeolocation getInstance() { + if (sMockGeolocation == null) { + sMockGeolocation = new MockGeolocation(); + } + return sMockGeolocation; + } + + // Native functions + private static native void nativeSetPosition(double latitude, double longitude, double accuracy); + private static native void nativeSetError(int code, String message); +} diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index c9b80ce77df60d6d83e35df83f78e023a9eafc2b..af0cb1e0e598c00e8a221fc48e1c6319267e9062 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -132,11 +132,11 @@ class Network { * XXX: Must be created in the same thread as WebCore!!!!! */ private Network(Context context) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(Thread.currentThread(). getName().equals(WebViewCore.THREAD_NAME)); } - mSslErrorHandler = new SslErrorHandler(this); + mSslErrorHandler = new SslErrorHandler(); mHttpAuthHandler = new HttpAuthHandler(this); mRequestQueue = new RequestQueue(context); @@ -149,14 +149,12 @@ class Network { * @param headers The http headers. * @param postData The body of the request. * @param loader A LoadListener for receiving the results of the request. - * @param isHighPriority True if this is high priority request. * @return True if the request was successfully queued. */ public boolean requestURL(String method, Map headers, byte [] postData, - LoadListener loader, - boolean isHighPriority) { + LoadListener loader) { String url = loader.url(); @@ -188,7 +186,7 @@ class Network { RequestHandle handle = q.queueRequest( url, loader.getWebAddress(), method, headers, loader, - bodyProvider, bodyLength, isHighPriority); + bodyProvider, bodyLength); loader.attachRequestHandle(handle); if (loader.isSynchronous()) { @@ -232,7 +230,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyUsername(String proxyUsername) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -252,7 +250,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyPassword(String proxyPassword) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -266,7 +264,7 @@ class Network { * @return True iff succeeds. */ public boolean saveState(Bundle outState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.saveState()"); } @@ -280,7 +278,7 @@ class Network { * @return True iff succeeds. */ public boolean restoreState(Bundle inState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.restoreState()"); } @@ -300,12 +298,20 @@ class Network { * @param loader The loader that resulted in SSL errors. */ public void handleSslErrorRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mSslErrorHandler.handleSslErrorRequest(loader); } } + /* package */ boolean checkSslPrefTable(LoadListener loader, + SslError error) { + if (loader != null && error != null) { + return mSslErrorHandler.checkSslPrefTable(loader, error); + } + return false; + } + /** * Handles authentication requests on their way up to the user (the user * must provide credentials). @@ -313,7 +319,7 @@ class Network { * authentication request. */ public void handleAuthRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mHttpAuthHandler.handleAuthRequest(loader); } diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java index f83da993b0930618246b1e8be169616102c395fe..34a30a94a731bfd90bd9bb0460d1298b75b1af5a 100644 --- a/core/java/android/webkit/Plugin.java +++ b/core/java/android/webkit/Plugin.java @@ -26,7 +26,11 @@ import android.webkit.WebView; /** * Represents a plugin (Java equivalent of the PluginPackageAndroid * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/) + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public class Plugin { public interface PreferencesClickHandler { public void handleClickEvent(Context context); @@ -38,6 +42,11 @@ public class Plugin { private String mDescription; private PreferencesClickHandler mHandler; + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public Plugin(String name, String path, String fileName, @@ -49,49 +58,103 @@ public class Plugin { mHandler = new DefaultClickHandler(); } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String toString() { return mName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getName() { return mName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getPath() { return mPath; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getFileName() { return mFileName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getDescription() { return mDescription; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setName(String name) { mName = name; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setPath(String path) { mPath = path; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setFileName(String fileName) { mFileName = fileName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setDescription(String description) { mDescription = description; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setClickHandler(PreferencesClickHandler handler) { mHandler = handler; } /** * Invokes the click handler for this plugin. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public void dispatchClickEvent(Context context) { if (mHandler != null) { mHandler.handleClickEvent(context); @@ -100,11 +163,15 @@ public class Plugin { /** * Default click handler. The plugins should implement their own. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated private class DefaultClickHandler implements PreferencesClickHandler, DialogInterface.OnClickListener { private AlertDialog mDialog; - + @Deprecated public void handleClickEvent(Context context) { // Show a simple popup dialog containing the description // string of the plugin. @@ -117,7 +184,11 @@ public class Plugin { .show(); } } - + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void onClick(DialogInterface dialog, int which) { mDialog.dismiss(); mDialog = null; diff --git a/core/java/android/webkit/PluginActivity.java b/core/java/android/webkit/PluginActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..cda7b59dfc06acaf58fdf5339f9d6e93ba2c0a8f --- /dev/null +++ b/core/java/android/webkit/PluginActivity.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +/** + * This activity is invoked when a plugin elects to go into full screen mode. + * @hide + */ +public class PluginActivity extends Activity { + + /* package */ static final String INTENT_EXTRA_PACKAGE_NAME = + "android.webkit.plugin.PACKAGE_NAME"; + /* package */ static final String INTENT_EXTRA_CLASS_NAME = + "android.webkit.plugin.CLASS_NAME"; + /* package */ static final String INTENT_EXTRA_NPP_INSTANCE = + "android.webkit.plugin.NPP_INSTANCE"; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + if (intent == null) { + // No intent means no class to lookup. + finish(); + } + final String packageName = + intent.getStringExtra(INTENT_EXTRA_PACKAGE_NAME); + final String className = intent.getStringExtra(INTENT_EXTRA_CLASS_NAME); + final int npp = intent.getIntExtra(INTENT_EXTRA_NPP_INSTANCE, -1); + // Retrieve the PluginStub implemented in packageName.className + PluginStub stub = + PluginUtil.getPluginStub(this, packageName, className); + + if (stub != null) { + View pluginView = stub.getFullScreenView(npp, this); + if (pluginView != null) { + setContentView(pluginView); + } else { + // No custom full-sreen view returned by the plugin, odd but + // just in case, finish the activity. + finish(); + } + } else { + finish(); + } + } +} diff --git a/core/java/android/webkit/PluginContentLoader.java b/core/java/android/webkit/PluginContentLoader.java deleted file mode 100644 index 206959980355771186eea7d14d35ec5d4c41a4c3..0000000000000000000000000000000000000000 --- a/core/java/android/webkit/PluginContentLoader.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.webkit; - -import android.net.http.Headers; - -import java.io.InputStream; -import java.util.*; - -import org.apache.http.util.CharArrayBuffer; - -/** - * This class is a concrete implementation of StreamLoader that uses a - * PluginData object as the source for the stream. - */ -class PluginContentLoader extends StreamLoader { - - private PluginData mData; // Content source - - /** - * Constructs a PluginDataLoader for use when loading content from - * a plugin. - * - * @param loadListener LoadListener to pass the content to - * @param data PluginData used as the source for the content. - */ - PluginContentLoader(LoadListener loadListener, PluginData data) { - super(loadListener); - mData = data; - } - - @Override - protected boolean setupStreamAndSendStatus() { - mDataStream = mData.getInputStream(); - mContentLength = mData.getContentLength(); - mHandler.status(1, 1, mData.getStatusCode(), "OK"); - return true; - } - - @Override - protected void buildHeaders(Headers headers) { - // Crate a CharArrayBuffer with an arbitrary initial capacity. - CharArrayBuffer buffer = new CharArrayBuffer(100); - Iterator> responseHeadersIt = - mData.getHeaders().entrySet().iterator(); - while (responseHeadersIt.hasNext()) { - Map.Entry entry = responseHeadersIt.next(); - // Headers.parseHeader() expects lowercase keys, so keys - // such as "Accept-Ranges" will fail to parse. - // - // UrlInterceptHandler instances supply a mapping of - // lowercase key to [ unmodified key, value ], so for - // Headers.parseHeader() to succeed, we need to construct - // a string using the key (i.e. entry.getKey()) and the - // element denoting the header value in the - // [ unmodified key, value ] pair (i.e. entry.getValue()[1). - // - // The reason why UrlInterceptHandler instances supply such a - // mapping in the first place is historical. Early versions of - // the Gears plugin used java.net.HttpURLConnection, which always - // returned headers names as capitalized strings. When these were - // fed back into webkit, they failed to parse. - // - // Mewanwhile, Gears was modified to use Apache HTTP library - // instead, so this design is now obsolete. Changing it however, - // would require changes to the Gears C++ codebase and QA-ing and - // submitting a new binary to the Android tree. Given the - // timelines for the next Android release, we will not do this - // for now. - // - // TODO: fix C++ Gears to remove the need for this - // design. - String keyValue = entry.getKey() + ": " + entry.getValue()[1]; - buffer.ensureCapacity(keyValue.length()); - buffer.append(keyValue); - // Parse it into the header container. - headers.parseHeader(buffer); - // Clear the buffer - buffer.clear(); - } - } -} diff --git a/core/java/android/webkit/PluginData.java b/core/java/android/webkit/PluginData.java index 2b539fe0a76ef9c1f28cfb22a7e2a7b29247ab92..2dd445e3b1e853132c45cf5cd60040675686ebfa 100644 --- a/core/java/android/webkit/PluginData.java +++ b/core/java/android/webkit/PluginData.java @@ -28,7 +28,10 @@ import java.util.Map; * status code. The PluginData class is the container for all these * parts. * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public final class PluginData { /** * The content stream. @@ -46,10 +49,6 @@ public final class PluginData { */ private Map mHeaders; - /** - * The index of the header value in the above mapping. - */ - private int mHeaderValueIndex; /** * The associated HTTP response code. */ @@ -63,7 +62,11 @@ public final class PluginData { * @param headers The response headers. Map of * lowercase header name to [ unmodified header name, header value] * @param length The HTTP response status code. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public PluginData( InputStream stream, long length, @@ -79,7 +82,11 @@ public final class PluginData { * Returns the input stream that contains the plugin content. * * @return An InputStream instance with the plugin content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public InputStream getInputStream() { return mStream; } @@ -88,7 +95,11 @@ public final class PluginData { * Returns the length of the plugin content. * * @return the length of the plugin content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public long getContentLength() { return mContentLength; } @@ -100,7 +111,11 @@ public final class PluginData { * @return A Map containing all headers. The * mapping is 'lowercase header name' to ['unmodified header * name', header value]. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public Map getHeaders() { return mHeaders; } @@ -109,7 +124,11 @@ public final class PluginData { * Returns the HTTP status code for the response. * * @return The HTTP statue code, e.g 200. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public int getStatusCode() { return mStatusCode; } diff --git a/core/java/android/webkit/PluginList.java b/core/java/android/webkit/PluginList.java index a9d3d8c126650bcf04e380bccd445a1ee9555d5c..a61b07b6f3384cc6e016c17832c3aca02f36a6b7 100644 --- a/core/java/android/webkit/PluginList.java +++ b/core/java/android/webkit/PluginList.java @@ -24,27 +24,43 @@ import java.util.List; * A simple list of initialized plugins. This list gets * populated when the plugins are initialized (at * browser startup, at the moment). + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public class PluginList { private ArrayList mPlugins; /** * Public constructor. Initializes the list of plugins. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public PluginList() { mPlugins = new ArrayList(); } /** * Returns the list of plugins as a java.util.List. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized List getList() { return mPlugins; } /** * Adds a plugin to the list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void addPlugin(Plugin plugin) { if (!mPlugins.contains(plugin)) { mPlugins.add(plugin); @@ -53,7 +69,11 @@ public class PluginList { /** * Removes a plugin from the list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void removePlugin(Plugin plugin) { int location = mPlugins.indexOf(plugin); if (location != -1) { @@ -63,14 +83,22 @@ public class PluginList { /** * Clears the plugin list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void clear() { mPlugins.clear(); } /** * Dispatches the click event to the appropriate plugin. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void pluginClicked(Context context, int position) { try { Plugin plugin = mPlugins.get(position); diff --git a/core/java/android/webkit/PluginManager.java b/core/java/android/webkit/PluginManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4588f46e57714c86da3095ea67647aeaca55eb0a --- /dev/null +++ b/core/java/android/webkit/PluginManager.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.content.Intent; +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.os.SystemProperties; +import android.util.Log; + +/** + * Class for managing the relationship between the {@link WebView} and installed + * plugins in the system. You can find this class through + * {@link PluginManager#getInstance}. + * + * @hide pending API solidification + */ +public class PluginManager { + + /** + * Service Action: A plugin wishes to be loaded in the WebView must provide + * {@link android.content.IntentFilter IntentFilter} that accepts this + * action in their AndroidManifest.xml. + *

    + * TODO: we may change this to a new PLUGIN_ACTION if this is going to be + * public. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String PLUGIN_ACTION = "android.webkit.PLUGIN"; + + /** + * A plugin wishes to be loaded in the WebView must provide this permission + * in their AndroidManifest.xml. + */ + public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN"; + + private static final String LOGTAG = "webkit"; + + private static PluginManager mInstance = null; + + private final Context mContext; + + private ArrayList mPackageInfoCache; + + // Only plugin matches one of the signatures in the list can be loaded + // inside the WebView process + private static final String SIGNATURE_1 = "308204c5308203ada003020102020900d7cb412f75f4887e300d06092a864886f70d010105050030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564301e170d3039313030313030323331345a170d3337303231363030323331345a30819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f726174656430820120300d06092a864886f70d01010105000382010d0030820108028201010099724f3e05bbd78843794f357776e04b340e13cb1c9ccb3044865180d7d8fec8166c5bbd876da8b80aa71eb6ba3d4d3455c9a8de162d24a25c4c1cd04c9523affd06a279fc8f0d018f242486bdbb2dbfbf6fcb21ed567879091928b876f7ccebc7bccef157366ebe74e33ae1d7e9373091adab8327482154afc0693a549522f8c796dd84d16e24bb221f5dbb809ca56dd2b6e799c5fa06b6d9c5c09ada54ea4c5db1523a9794ed22a3889e5e05b29f8ee0a8d61efe07ae28f65dece2ff7edc5b1416d7c7aad7f0d35e8f4a4b964dbf50ae9aa6d620157770d974131b3e7e3abd6d163d65758e2f0822db9c88598b9db6263d963d13942c91fc5efe34fc1e06e3020103a382010630820102301d0603551d0e041604145af418e419a639e1657db960996364a37ef20d403081d20603551d230481ca3081c780145af418e419a639e1657db960996364a37ef20d40a181a3a481a030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564820900d7cb412f75f4887e300c0603551d13040530030101ff300d06092a864886f70d0101050500038201010076c2a11fe303359689c2ebc7b2c398eff8c3f9ad545cdbac75df63bf7b5395b6988d1842d6aa1556d595b5692e08224d667a4c9c438f05e74906c53dd8016dde7004068866f01846365efd146e9bfaa48c9ecf657f87b97c757da11f225c4a24177bf2d7188e6cce2a70a1e8a841a14471eb51457398b8a0addd8b6c8c1538ca8f1e40b4d8b960009ea22c188d28924813d2c0b4a4d334b7cf05507e1fcf0a06fe946c7ffc435e173af6fc3e3400643710acc806f830a14788291d46f2feed9fb5c70423ca747ed1572d752894ac1f19f93989766308579393fabb43649aa8806a313b1ab9a50922a44c2467b9062037f2da0d484d9ffd8fe628eeea629ba637"; + + private static final Signature[] SIGNATURES = new Signature[] { + new Signature(SIGNATURE_1) + }; + + private PluginManager(Context context) { + mContext = context; + mPackageInfoCache = new ArrayList(); + } + + public static synchronized PluginManager getInstance(Context context) { + if (mInstance == null) { + if (context == null) { + throw new IllegalStateException( + "First call to PluginManager need a valid context."); + } + mInstance = new PluginManager(context); + } + return mInstance; + } + + /** + * Signal the WebCore thread to refresh its list of plugins. Use this if the + * directory contents of one of the plugin directories has been modified and + * needs its changes reflecting. May cause plugin load and/or unload. + * + * @param reloadOpenPages Set to true to reload all open pages. + */ + public void refreshPlugins(boolean reloadOpenPages) { + BrowserFrame.sJavaBridge.obtainMessage( + JWebCoreJavaBridge.REFRESH_PLUGINS, reloadOpenPages) + .sendToTarget(); + } + + String[] getPluginDirectories() { + + ArrayList directories = new ArrayList(); + PackageManager pm = mContext.getPackageManager(); + List plugins = pm.queryIntentServices(new Intent( + PLUGIN_ACTION), PackageManager.GET_SERVICES); + + synchronized(mPackageInfoCache) { + + // clear the list of existing packageInfo objects + mPackageInfoCache.clear(); + + for (ResolveInfo info : plugins) { + ServiceInfo serviceInfo = info.serviceInfo; + if (serviceInfo == null) { + Log.w(LOGTAG, "Ignore bad plugin"); + continue; + } + PackageInfo pkgInfo; + try { + pkgInfo = pm.getPackageInfo(serviceInfo.packageName, + PackageManager.GET_PERMISSIONS + | PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Log.w(LOGTAG, "Cant find plugin: " + serviceInfo.packageName); + continue; + } + if (pkgInfo == null) { + continue; + } + String directory = pkgInfo.applicationInfo.dataDir + "/lib"; + if (directories.contains(directory)) { + continue; + } + String permissions[] = pkgInfo.requestedPermissions; + if (permissions == null) { + continue; + } + boolean permissionOk = false; + for (String permit : permissions) { + if (PLUGIN_PERMISSION.equals(permit)) { + permissionOk = true; + break; + } + } + if (!permissionOk) { + continue; + } + Signature signatures[] = pkgInfo.signatures; + if (signatures == null) { + continue; + } + if (SystemProperties.getBoolean("ro.secure", false)) { + boolean signatureMatch = false; + for (Signature signature : signatures) { + for (int i = 0; i < SIGNATURES.length; i++) { + if (SIGNATURES[i].equals(signature)) { + signatureMatch = true; + break; + } + } + } + if (!signatureMatch) { + continue; + } + } + mPackageInfoCache.add(pkgInfo); + directories.add(directory); + } + } + + return directories.toArray(new String[directories.size()]); + } + + String getPluginsAPKName(String pluginLib) { + + // basic error checking on input params + if (pluginLib == null || pluginLib.length() == 0) { + return null; + } + + // must be synchronized to ensure the consistency of the cache + synchronized(mPackageInfoCache) { + for (PackageInfo pkgInfo : mPackageInfoCache) { + if (pluginLib.startsWith(pkgInfo.applicationInfo.dataDir)) { + return pkgInfo.packageName; + } + } + } + + // if no apk was found then return null + return null; + } + + String getPluginSharedDataDirectory() { + return mContext.getDir("plugins", 0).getPath(); + } +} diff --git a/core/java/android/webkit/PluginStub.java b/core/java/android/webkit/PluginStub.java new file mode 100644 index 0000000000000000000000000000000000000000..3887d44f352a4313f6eee8d4c7e52cf9d7ff227b --- /dev/null +++ b/core/java/android/webkit/PluginStub.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.view.View; + +/** + * This interface is used to implement plugins in a WebView. A plugin + * package may extend this class and implement the abstract functions to create + * embedded or fullscreeen views displayed in a WebView. The PluginStub + * implementation will be provided the same NPP instance that is created + * through the native interface. + */ +public interface PluginStub { + + /** + * Return a custom embedded view to draw the plugin. + * @param NPP The native NPP instance. + * @param context The current application's Context. + * @return A custom View that will be managed by WebView. + */ + public abstract View getEmbeddedView(int NPP, Context context); + + /** + * Return a custom full-screen view to be displayed when the user requests + * a plugin display as full-screen. Note that the application may choose not + * to display this View as completely full-screen. + * @param NPP The native NPP instance. + * @param context The current application's Context. + * @return A custom View that will be managed by the application. + */ + public abstract View getFullScreenView(int NPP, Context context); +} diff --git a/core/java/android/webkit/PluginUtil.java b/core/java/android/webkit/PluginUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..8fdbd672b544ab3ecd4d2bdd965f4d768176e5b8 --- /dev/null +++ b/core/java/android/webkit/PluginUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +class PluginUtil { + + private static final String LOGTAG = "PluginUtil"; + + /** + * + * @param packageName the name of the apk where the class can be found + * @param className the fully qualified name of a subclass of PluginStub + */ + /* package */ + static PluginStub getPluginStub(Context context, String packageName, + String className) { + try { + Context pluginContext = context.createPackageContext(packageName, + Context.CONTEXT_INCLUDE_CODE | + Context.CONTEXT_IGNORE_SECURITY); + ClassLoader pluginCL = pluginContext.getClassLoader(); + + Class stubClass = pluginCL.loadClass(className); + Object stubObject = stubClass.newInstance(); + + if (stubObject instanceof PluginStub) { + return (PluginStub) stubObject; + } else { + Log.e(LOGTAG, "The plugin class is not of type PluginStub"); + } + } catch (Exception e) { + // Any number of things could have happened. Log the exception and + // return null. Careful not to use Log.e(LOGTAG, "String", e) + // because that reports the exception to the checkin service. + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + return null; + } +} diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java index 5f84bbe04dd3cdcfa19f5d77f14353073f376781..90ed65d59ebc36df9261359d5cd0f6aa5ce0c1da 100644 --- a/core/java/android/webkit/SslErrorHandler.java +++ b/core/java/android/webkit/SslErrorHandler.java @@ -41,11 +41,6 @@ public class SslErrorHandler extends Handler { private static final String LOGTAG = "network"; - /** - * Network. - */ - private Network mNetwork; - /** * Queue of loaders that experience SSL-related problems. */ @@ -57,13 +52,15 @@ public class SslErrorHandler extends Handler { private Bundle mSslPrefTable; // Message id for handling the response - private final int HANDLE_RESPONSE = 100; + private static final int HANDLE_RESPONSE = 100; @Override public void handleMessage(Message msg) { switch (msg.what) { case HANDLE_RESPONSE: - handleSslErrorResponse(msg.arg1 == 1); + LoadListener loader = (LoadListener) msg.obj; + handleSslErrorResponse(loader, loader.sslError(), + msg.arg1 == 1); fastProcessQueuedSslErrors(); break; } @@ -72,9 +69,7 @@ public class SslErrorHandler extends Handler { /** * Creates a new error handler with an empty loader queue. */ - /* package */ SslErrorHandler(Network network) { - mNetwork = network; - + /* package */ SslErrorHandler() { mLoaderQueue = new LinkedList(); mSslPrefTable = new Bundle(); } @@ -83,7 +78,7 @@ public class SslErrorHandler extends Handler { * Saves this handler's state into a map. * @return True iff succeeds. */ - /* package */ boolean saveState(Bundle outState) { + /* package */ synchronized boolean saveState(Bundle outState) { boolean success = (outState != null); if (success) { // TODO? @@ -97,7 +92,7 @@ public class SslErrorHandler extends Handler { * Restores this handler's state from a map. * @return True iff succeeds. */ - /* package */ boolean restoreState(Bundle inState) { + /* package */ synchronized boolean restoreState(Bundle inState) { boolean success = (inState != null); if (success) { success = inState.containsKey("ssl-error-handler"); @@ -120,7 +115,7 @@ public class SslErrorHandler extends Handler { * Handles SSL error(s) on the way up to the user. */ /* package */ synchronized void handleSslErrorRequest(LoadListener loader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorRequest(): " + "url=" + loader.url()); } @@ -133,6 +128,28 @@ public class SslErrorHandler extends Handler { } } + /** + * Check the preference table for a ssl error that has already been shown + * to the user. + */ + /* package */ synchronized boolean checkSslPrefTable(LoadListener loader, + SslError error) { + final String host = loader.host(); + final int primary = error.getPrimaryError(); + + if (DebugFlags.SSL_ERROR_HANDLER) { + Assert.assertTrue(host != null && primary != 0); + } + + if (mSslPrefTable.containsKey(host)) { + if (primary <= mSslPrefTable.getInt(host)) { + handleSslErrorResponse(loader, error, true); + return true; + } + } + return false; + } + /** * Processes queued SSL-error confirmation requests in * a tight loop while there is no need to ask the user. @@ -151,28 +168,24 @@ public class SslErrorHandler extends Handler { if (loader != null) { // if this loader has been cancelled if (loader.cancelled()) { - // go to the following loader in the queue + // go to the following loader in the queue. Make sure this + // loader has been removed from the queue. + mLoaderQueue.remove(loader); return true; } SslError error = loader.sslError(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(error); } - int primary = error.getPrimaryError(); - String host = loader.host(); - - if (WebView.DEBUG) { - Assert.assertTrue(host != null && primary != 0); - } - - if (mSslPrefTable.containsKey(host)) { - if (primary <= mSslPrefTable.getInt(host)) { - handleSslErrorResponse(true); - return true; - } + // checkSslPrefTable will handle the ssl error response if the + // answer is available. It does not remove the loader from the + // queue. + if (checkSslPrefTable(loader, error)) { + mLoaderQueue.remove(loader); + return true; } // if we do not have information on record, ask @@ -189,7 +202,7 @@ public class SslErrorHandler extends Handler { * Proceed with the SSL certificate. */ public void proceed() { - sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0)); + sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0, mLoaderQueue.poll())); } /** @@ -197,19 +210,20 @@ public class SslErrorHandler extends Handler { * the error. */ public void cancel() { - sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0)); + sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0, mLoaderQueue.poll())); } /** * Handles SSL error(s) on the way down from the user. */ - /* package */ synchronized void handleSslErrorResponse(boolean proceed) { - LoadListener loader = mLoaderQueue.poll(); - if (WebView.DEBUG) { + /* package */ synchronized void handleSslErrorResponse(LoadListener loader, + SslError error, boolean proceed) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(loader); + Assert.assertNotNull(error); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorResponse():" + " proceed: " + proceed + " url:" + loader.url()); @@ -218,16 +232,16 @@ public class SslErrorHandler extends Handler { if (!loader.cancelled()) { if (proceed) { // update the user's SSL error preference table - int primary = loader.sslError().getPrimaryError(); + int primary = error.getPrimaryError(); String host = loader.host(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertTrue(host != null && primary != 0); } boolean hasKey = mSslPrefTable.containsKey(host); if (!hasKey || primary > mSslPrefTable.getInt(host)) { - mSslPrefTable.putInt(host, new Integer(primary)); + mSslPrefTable.putInt(host, primary); } } loader.handleSslErrorResponse(proceed); diff --git a/core/java/android/webkit/StreamLoader.java b/core/java/android/webkit/StreamLoader.java index 705157c639f29c918ffb0cde27a45ff0e0ef58e3..623ff29587142b66d420503a88352d936619f185 100644 --- a/core/java/android/webkit/StreamLoader.java +++ b/core/java/android/webkit/StreamLoader.java @@ -102,7 +102,7 @@ abstract class StreamLoader extends Handler { // to pass data to the loader mData = new byte[8192]; sendHeaders(); - while (!sendData()); + while (!sendData() && !mHandler.cancelled()); closeStreamAndSendEndData(); mHandler.loadSynchronousMessages(); } @@ -113,9 +113,13 @@ abstract class StreamLoader extends Handler { * @see android.os.Handler#handleMessage(android.os.Message) */ public void handleMessage(Message msg) { - if (WebView.DEBUG && mHandler.isSynchronous()) { + if (DebugFlags.STREAM_LOADER && mHandler.isSynchronous()) { throw new AssertionError(); } + if (mHandler.cancelled()) { + closeStreamAndSendEndData(); + return; + } switch(msg.what) { case MSG_STATUS: if (setupStreamAndSendStatus()) { @@ -153,7 +157,6 @@ abstract class StreamLoader extends Handler { if (mContentLength > 0) { headers.setContentLength(mContentLength); } - headers.setCacheControl(NO_STORE); buildHeaders(headers); mHandler.headers(headers); } diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 9889fe9e8de00f543e5e1a6039a83c325b038da0..232ed363391655e54b8a0dde0ad27e6a31cda77d 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -61,7 +61,7 @@ public final class URLUtil { webAddress = new WebAddress(inUrl); } catch (ParseException ex) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.URL_UTIL) { Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); } return retVal; @@ -126,6 +126,32 @@ public final class URLUtil { return retData; } + /** + * @return True iff the url is correctly URL encoded + */ + static boolean verifyURLEncoding(String url) { + int count = url.length(); + if (count == 0) { + return false; + } + + int index = url.indexOf('%'); + while (index >= 0 && index < count) { + if (index < count - 2) { + try { + parseHex((byte) url.charAt(++index)); + parseHex((byte) url.charAt(++index)); + } catch (IllegalArgumentException e) { + return false; + } + } else { + return false; + } + index = url.indexOf('%', index + 1); + } + return true; + } + private static int parseHex(byte b) { if (b >= '0' && b <= '9') return (b - '0'); if (b >= 'A' && b <= 'F') return (b - 'A' + 10); @@ -146,6 +172,7 @@ public final class URLUtil { * requests from a file url. * @deprecated Cookieless proxy is no longer supported. */ + @Deprecated public static boolean isCookielessProxyUrl(String url) { return (null != url) && url.startsWith(PROXY_BASE); } diff --git a/core/java/android/webkit/UrlInterceptHandler.java b/core/java/android/webkit/UrlInterceptHandler.java index 9216413c4a753ff9b4c1bde8e91aa012791448aa..78bab043374697e86f809e2c977f4c3e4f051c5b 100644 --- a/core/java/android/webkit/UrlInterceptHandler.java +++ b/core/java/android/webkit/UrlInterceptHandler.java @@ -20,6 +20,11 @@ import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; import java.util.Map; +/** + * @deprecated This interface was inteded to be used by Gears. Since Gears was + * deprecated, so is this class. + */ +@Deprecated public interface UrlInterceptHandler { /** @@ -30,8 +35,8 @@ public interface UrlInterceptHandler { * @param url URL string. * @param headers The headers associated with the request. May be null. * @return The CacheResult containing the surrogate response. - * @Deprecated Use PluginData getPluginData(String url, - * Map headers); instead + * + * @deprecated Do not use, this interface is deprecated. */ @Deprecated public CacheResult service(String url, Map headers); @@ -44,6 +49,9 @@ public interface UrlInterceptHandler { * @param url URL string. * @param headers The headers associated with the request. May be null. * @return The PluginData containing the surrogate response. + * + * @deprecated Do not use, this interface is deprecated. */ + @Deprecated public PluginData getPluginData(String url, Map headers); } diff --git a/core/java/android/webkit/UrlInterceptRegistry.java b/core/java/android/webkit/UrlInterceptRegistry.java index 6051f290dffe1e53de0fda7fd6c7ddfea8e7cdff..eca5acdfec5cdaf51b83ebd43bb034a9a466a73e 100644 --- a/core/java/android/webkit/UrlInterceptRegistry.java +++ b/core/java/android/webkit/UrlInterceptRegistry.java @@ -24,6 +24,11 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.Map; +/** + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ +@Deprecated public final class UrlInterceptRegistry { private final static String LOGTAG = "intercept"; @@ -42,7 +47,11 @@ public final class UrlInterceptRegistry { * set the flag to control whether url intercept is enabled or disabled * * @param disabled true to disable the cache + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized void setUrlInterceptDisabled(boolean disabled) { mDisabled = disabled; } @@ -51,7 +60,11 @@ public final class UrlInterceptRegistry { * get the state of the url intercept, enabled or disabled * * @return return if it is disabled + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean urlInterceptDisabled() { return mDisabled; } @@ -62,7 +75,11 @@ public final class UrlInterceptRegistry { * * @param handler The new UrlInterceptHandler object * @return true if the handler was not previously registered. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean registerHandler( UrlInterceptHandler handler) { if (!getHandlers().contains(handler)) { @@ -78,7 +95,11 @@ public final class UrlInterceptRegistry { * * @param handler A previously registered UrlInterceptHandler. * @return true if the handler was found and removed from the list. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean unregisterHandler( UrlInterceptHandler handler) { return getHandlers().remove(handler); @@ -89,8 +110,9 @@ public final class UrlInterceptRegistry { * UrlInterceptHandler interested, or null if none are. * * @return A CacheResult containing surrogate content. - * @Deprecated Use PluginData getPluginData( String url, - * Map headers) instead. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ @Deprecated public static synchronized CacheResult getSurrogate( @@ -115,7 +137,11 @@ public final class UrlInterceptRegistry { * intercepts are disabled. * * @return A PluginData instance containing surrogate content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized PluginData getPluginData( String url, Map headers) { if (urlInterceptDisabled()) { diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..1a167e82e7acb0a9b3dfb9340b54ba9c0bf110d7 --- /dev/null +++ b/core/java/android/webkit/ValueCallback.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * A callback interface used to returns values asynchronously + */ +public interface ValueCallback { + /** + * Invoked when we have the result + */ + public void onReceiveValue(T value); +}; diff --git a/core/java/android/webkit/ViewManager.java b/core/java/android/webkit/ViewManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6a838c3b3a9ba1cc32468fdaf374ba5e20a68284 --- /dev/null +++ b/core/java/android/webkit/ViewManager.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.Context; +import android.view.View; +import android.widget.AbsoluteLayout; + +import java.util.ArrayList; + +class ViewManager { + private final WebView mWebView; + private final ArrayList mChildren = new ArrayList(); + private boolean mHidden; + + class ChildView { + int x; + int y; + int width; + int height; + View mView; // generic view to show + + ChildView() { + } + + void setBounds(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + void attachView(int x, int y, int width, int height) { + if (mView == null) { + return; + } + setBounds(x, y, width, height); + final AbsoluteLayout.LayoutParams lp = + new AbsoluteLayout.LayoutParams(ctvD(width), ctvD(height), + ctvX(x), ctvY(y)); + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + // This method may be called multiple times. If the view is + // already attached, just set the new LayoutParams, + // otherwise attach the view and add it to the list of + // children. + if (mView.getParent() != null) { + mView.setLayoutParams(lp); + } else { + attachViewOnUIThread(lp); + } + } + }); + } + + void attachViewOnUIThread(AbsoluteLayout.LayoutParams lp) { + mWebView.addView(mView, lp); + mChildren.add(this); + } + + void removeView() { + if (mView == null) { + return; + } + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + removeViewOnUIThread(); + } + }); + } + + void removeViewOnUIThread() { + mWebView.removeView(mView); + mChildren.remove(this); + } + } + + ViewManager(WebView w) { + mWebView = w; + } + + ChildView createView() { + return new ChildView(); + } + + /** + * Shorthand for calling mWebView.contentToViewDimension. Used when + * obtaining a view dimension from a content dimension, whether it be in x + * or y. + */ + private int ctvD(int val) { + return mWebView.contentToViewDimension(val); + } + + /** + * Shorthand for calling mWebView.contentToViewX. Used when obtaining a + * view x coordinate from a content x coordinate. + */ + private int ctvX(int val) { + return mWebView.contentToViewX(val); + } + + /** + * Shorthand for calling mWebView.contentToViewY. Used when obtaining a + * view y coordinate from a content y coordinate. + */ + private int ctvY(int val) { + return mWebView.contentToViewY(val); + } + + void scaleAll() { + for (ChildView v : mChildren) { + View view = v.mView; + AbsoluteLayout.LayoutParams lp = + (AbsoluteLayout.LayoutParams) view.getLayoutParams(); + lp.width = ctvD(v.width); + lp.height = ctvD(v.height); + lp.x = ctvX(v.x); + lp.y = ctvY(v.y); + view.setLayoutParams(lp); + } + } + + void hideAll() { + if (mHidden) { + return; + } + for (ChildView v : mChildren) { + v.mView.setVisibility(View.GONE); + } + mHidden = true; + } + + void showAll() { + if (!mHidden) { + return; + } + for (ChildView v : mChildren) { + v.mView.setVisibility(View.VISIBLE); + } + mHidden = false; + } +} diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index ffd6a118d9e63914a1bc18b9a79f5b06cfa5546e..62a55318d3065bfdbb5450f8025fc5dd2e7a0009 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -137,7 +137,7 @@ public class WebBackForwardList implements Cloneable, Serializable { // when removing the first item, we can assert that the index is 0. // This lets us change the current index without having to query the // native BackForwardList. - if (WebView.DEBUG && (index != 0)) { + if (DebugFlags.WEB_BACK_FORWARD_LIST && (index != 0)) { throw new AssertionError(); } final WebHistoryItem h = mArray.remove(index); diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 9d9763ca66737a19e32839949bc6fc2e0e595b0b..92676aac8c766cd8efbb2b13b39078b0b464cd26 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -18,6 +18,7 @@ package android.webkit; import android.graphics.Bitmap; import android.os.Message; +import android.view.View; public class WebChromeClient { @@ -43,6 +44,42 @@ public class WebChromeClient { */ public void onReceivedIcon(WebView view, Bitmap icon) {} + /** + * Notify the host application of the url for an apple-touch-icon. + * @param view The WebView that initiated the callback. + * @param url The icon url. + * @param precomposed True if the url is for a precomposed touch icon. + */ + public void onReceivedTouchIconUrl(WebView view, String url, + boolean precomposed) {} + + /** + * A callback interface used by the host application to notify + * the current page that its custom view has been dismissed. + */ + public interface CustomViewCallback { + /** + * Invoked when the host application dismisses the + * custom view. + */ + public void onCustomViewHidden(); + } + + /** + * Notify the host application that the current page would + * like to show a custom View. + * @param view is the View object to be shown. + * @param callback is the callback to be invoked if and when the view + * is dismissed. + */ + public void onShowCustomView(View view, CustomViewCallback callback) {}; + + /** + * Notify the host application that the current page would + * like to hide its custom view. + */ + public void onHideCustomView() {} + /** * Request the host application to create a new Webview. The host * application should handle placement of the new WebView in the view @@ -158,6 +195,52 @@ public class WebChromeClient { return false; } + /** + * Tell the client that the database quota for the origin has been exceeded. + * @param url The URL that triggered the notification + * @param databaseIdentifier The identifier of the database that caused the + * quota overflow. + * @param currentQuota The current quota for the origin. + * @param estimatedSize The estimated size of the database. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * quota is available. This callback must always be executed at some + * point to ensure that the sleeping WebCore thread is woken up. + */ + public void onExceededDatabaseQuota(String url, String databaseIdentifier, + long currentQuota, long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + // This default implementation passes the current quota back to WebCore. + // WebCore will interpret this that new quota was declined. + quotaUpdater.updateQuota(currentQuota); + } + + /** + * Tell the client that the Application Cache has exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * app cache size is available. This callback must always be executed at + * some point to ensure that the sleeping WebCore thread is woken up. + */ + public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + quotaUpdater.updateQuota(0); + } + + /** + * Instructs the client to show a prompt to ask the user to set the + * Geolocation permission state for the specified origin. + */ + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) {} + + /** + * Instructs the client to hide the Geolocation permissions prompt. + */ + public void onGeolocationPermissionsHidePrompt() {} + /** * Tell the client that a JavaScript execution timeout has occured. And the * client may decide whether or not to interrupt the execution. If the @@ -167,9 +250,43 @@ public class WebChromeClient { * will continue to occur if the script does not finish at the next check * point. * @return boolean Whether the JavaScript execution should be interrupted. - * @hide pending API Council approval */ public boolean onJsTimeout() { return true; } + + /** + * Add a JavaScript error message to the console. Clients should override + * this to process the log message as they see fit. + * @param message The error message to report. + * @param lineNumber The line number of the error. + * @param sourceID The name of the source file that caused the error. + */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) {} + + /** + * Ask the host application for an icon to represent a

    A View that displays web pages. This class is the basis upon which you + *

    A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. * It uses the WebKit rendering engine to display * web pages and includes methods to navigate forward and backward @@ -93,12 +92,109 @@ import java.util.List; * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} * (introduced in API version 3). *

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

    *
    <uses-permission android:name="android.permission.INTERNET" />
    + * *

    This must be a child of the <manifest> element.

    + * + *

    Basic usage

    + * + *

    By default, a WebView provides no browser-like widgets, does not + * enable JavaScript and errors will be ignored. If your goal is only + * to display some HTML as a part of your UI, this is probably fine; + * the user won't need to interact with the web page beyond reading + * it, and the web page won't need to interact with the user. If you + * actually want a fully blown web browser, then you probably want to + * invoke the Browser application with your URL rather than show it + * with a WebView. See {@link android.content.Intent} for more information.

    + * + *
    + * WebView webview = new WebView(this);
    + * setContentView(webview);
    + *
    + * // Simplest usage: note that an exception will NOT be thrown
    + * // if there is an error loading this page (see below).
    + * webview.loadUrl("http://slashdot.org/");
    + *
    + * // Of course you can also load from any string:
    + * String summary = "<html><body>You scored <b>192 points.</body></html>";
    + * webview.loadData(summary, "text/html", "utf-8");
    + * // ... although note that there are restrictions on what this HTML can do.
    + * // See the JavaDocs for loadData and loadDataWithBaseUrl for more info.
    + * 
    + * + *

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

    + * + *
      + *
    • Creating and setting a {@link android.webkit.WebChromeClient} subclass. + * This class is called when something that might impact a + * browser UI happens, for instance, progress updates and + * JavaScript alerts are sent here. + *
    • + *
    • Creating and setting a {@link android.webkit.WebViewClient} subclass. + * It will be called when things happen that impact the + * rendering of the content, eg, errors or form submissions. You + * can also intercept URL loading here.
    • + *
    • Via the {@link android.webkit.WebSettings} class, which contains + * miscellaneous configuration.
    • + *
    • With the {@link android.webkit.WebView#addJavascriptInterface} method. + * This lets you bind Java objects into the WebView so they can be + * controlled from the web pages JavaScript.
    • + *
    + * + *

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

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

    Cookie and window management

    + * + *

    For obvious security reasons, your application has its own + * cache, cookie store etc - it does not share the Browser + * applications data. Cookies are managed on a separate thread, so + * operations like index building don't block the UI + * thread. Follow the instructions in {@link android.webkit.CookieSyncManager} + * if you want to use cookies in your application. + *

    + * + *

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

    + * + *

    Standard behavior for an Activity is to be destroyed and + * recreated when the devices orientation is changed. This will cause + * the WebView to reload the current page. If you don't want that, you + * can set your Activity to handle the orientation and keyboardHidden + * changes, and then just leave the WebView alone. It'll automatically + * re-orient itself as appropriate.

    */ -public class WebView extends AbsoluteLayout +public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { @@ -108,62 +204,52 @@ public class WebView extends AbsoluteLayout // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; - // keep debugging parameters near the top of the file static final String LOGTAG = "webview"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; - private class ExtendedZoomControls extends FrameLayout { + private static class ExtendedZoomControls extends FrameLayout { public ExtendedZoomControls(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); - mZoomControls = (ZoomControls) findViewById(com.android.internal.R.id.zoomControls); - mZoomMagnify = (ImageView) findViewById(com.android.internal.R.id.zoomMagnify); + mPlusMinusZoomControls = (ZoomControls) findViewById( + com.android.internal.R.id.zoomControls); + findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( + View.GONE); } - + public void show(boolean showZoom, boolean canZoomOut) { - mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); - mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE); + mPlusMinusZoomControls.setVisibility( + showZoom ? View.VISIBLE : View.GONE); fade(View.VISIBLE, 0.0f, 1.0f); } - + public void hide() { fade(View.GONE, 1.0f, 0.0f); } - + private void fade(int visibility, float startAlpha, float endAlpha) { AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); anim.setDuration(500); startAnimation(anim); setVisibility(visibility); } - - public void setIsZoomMagnifyEnabled(boolean isEnabled) { - mZoomMagnify.setEnabled(isEnabled); - } - + public boolean hasFocus() { - return mZoomControls.hasFocus() || mZoomMagnify.hasFocus(); + return mPlusMinusZoomControls.hasFocus(); } - + public void setOnZoomInClickListener(OnClickListener listener) { - mZoomControls.setOnZoomInClickListener(listener); + mPlusMinusZoomControls.setOnZoomInClickListener(listener); } - + public void setOnZoomOutClickListener(OnClickListener listener) { - mZoomControls.setOnZoomOutClickListener(listener); - } - - public void setOnZoomMagnifyClickListener(OnClickListener listener) { - mZoomMagnify.setOnClickListener(listener); + mPlusMinusZoomControls.setOnZoomOutClickListener(listener); } - ZoomControls mZoomControls; - ImageView mZoomMagnify; + ZoomControls mPlusMinusZoomControls; } - + /** * Transportation object for returning WebView across thread boundaries. */ @@ -203,13 +289,13 @@ public class WebView extends AbsoluteLayout private WebViewCore mWebViewCore; // Handler for dispatching UI messages. /* package */ final Handler mPrivateHandler = new PrivateHandler(); - private TextDialog mTextEntry; + private WebTextView mWebTextView; // Used to ignore changes to webkit text that arrives to the UI side after // more key events. private int mTextGeneration; - // The list of loaded plugins. - private static PluginList sPluginList; + // Used by WebViewCore to create child views. + /* package */ final ViewManager mViewManager; /** * Position of the last touch event. @@ -229,15 +315,27 @@ public class WebView extends AbsoluteLayout /** * The minimum elapsed time before sending another ACTION_MOVE event to - * WebViewCore + * WebViewCore. This really should be tuned for each type of the devices. + * For example in Google Map api test case, it takes Dream device at least + * 150ms to do a full cycle in the WebViewCore by processing a touch event, + * triggering the layout and drawing the picture. While the same process + * takes 60+ms on the current high speed device. If we make + * TOUCH_SENT_INTERVAL too small, there will be multiple touch events sent + * to WebViewCore queue and the real layout and draw events will be pushed + * to further, which slows down the refresh rate. Choose 50 to favor the + * current high speed devices. For Dream like devices, 100 is a better + * choice. Maybe make this in the buildspec later. */ - private static final int TOUCH_SENT_INTERVAL = 100; + private static final int TOUCH_SENT_INTERVAL = 50; /** * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; private int mMaximumFling; + private float mLastVelocity; + private float mLastVelX; + private float mLastVelY; /** * Touch mode @@ -248,16 +346,9 @@ public class WebView extends AbsoluteLayout private static final int TOUCH_DRAG_MODE = 3; private static final int TOUCH_SHORTPRESS_START_MODE = 4; private static final int TOUCH_SHORTPRESS_MODE = 5; - private static final int TOUCH_DOUBLECLICK_MODE = 6; + private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; private static final int TOUCH_SELECT_MODE = 8; - // touch mode values specific to scale+scroll - private static final int FIRST_SCROLL_ZOOM = 9; - private static final int SCROLL_ZOOM_ANIMATION_IN = 9; - private static final int SCROLL_ZOOM_ANIMATION_OUT = 10; - private static final int SCROLL_ZOOM_OUT = 11; - private static final int LAST_SCROLL_ZOOM = 11; - // end of touch mode values specific to scale+scroll // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; @@ -265,20 +356,28 @@ public class WebView extends AbsoluteLayout // Whether to prevent drag during touch. The initial value depends on // mForwardTouchEvents. If WebCore wants touch events, we assume it will // take control of touch events unless it says no for touch down event. - private boolean mPreventDrag; + private static final int PREVENT_DRAG_NO = 0; + private static final int PREVENT_DRAG_MAYBE_YES = 1; + private static final int PREVENT_DRAG_YES = 2; + private int mPreventDrag = PREVENT_DRAG_NO; + + // To keep track of whether the current drag was initiated by a WebTextView, + // so that we know not to hide the cursor + boolean mDragFromTextInput; + + // Whether or not to draw the cursor ring. + private boolean mDrawCursorRing = true; - // If updateTextEntry gets called while we are out of focus, use this - // variable to remember to do it next time we gain focus. - private boolean mNeedsUpdateTextEntry = false; - - // Whether or not to draw the focus ring. - private boolean mDrawFocusRing = true; + // true if onPause has been called (and not onResume) + private boolean mIsPaused; /** * Customizable constant */ // pre-computed square of ViewConfiguration.getScaledTouchSlop() private int mTouchSlopSquare; + // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() + private int mDoubleTapSlopSquare; // pre-computed density adjusted navigation slop private int mNavSlop; // This should be ViewConfiguration.getTapTimeout() @@ -292,7 +391,7 @@ public class WebView extends AbsoluteLayout // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; // The time that the Zoom Controls are visible before fading away - private static final long ZOOM_CONTROLS_TIMEOUT = + private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. @@ -313,7 +412,7 @@ public class WebView extends AbsoluteLayout private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore - // Need to have the separate control for horizontal and vertical scrollbar + // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; private boolean mOverlayVerticalScrollbar = false; @@ -324,55 +423,53 @@ public class WebView extends AbsoluteLayout private static final int STD_SPEED = 480; // pixels per second // time for the longest scroll animation private static final int MAX_DURATION = 750; // milliseconds + private static final int SLIDE_TITLE_DURATION = 500; // milliseconds private Scroller mScroller; private boolean mWrapContent; - // true if we should call webcore to draw the content, false means we have - // requested something but it isn't ready to draw yet. - private WebViewCore.FocusData mFocusData; /** * Private message ids */ - private static final int REMEMBER_PASSWORD = 1; - private static final int NEVER_REMEMBER_PASSWORD = 2; - private static final int SWITCH_TO_SHORTPRESS = 3; - private static final int SWITCH_TO_LONGPRESS = 4; - private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6; - private static final int SWITCH_TO_ENTER = 7; - private static final int RESUME_WEBCORE_UPDATE = 8; + private static final int REMEMBER_PASSWORD = 1; + private static final int NEVER_REMEMBER_PASSWORD = 2; + private static final int SWITCH_TO_SHORTPRESS = 3; + private static final int SWITCH_TO_LONGPRESS = 4; + private static final int RELEASE_SINGLE_TAP = 5; + private static final int REQUEST_FORM_DATA = 6; + private static final int RESUME_WEBCORE_UPDATE = 7; //! arg1=x, arg2=y - static final int SCROLL_TO_MSG_ID = 10; - static final int SCROLL_BY_MSG_ID = 11; + static final int SCROLL_TO_MSG_ID = 10; + static final int SCROLL_BY_MSG_ID = 11; //! arg1=x, arg2=y - static final int SPAWN_SCROLL_TO_MSG_ID = 12; + static final int SPAWN_SCROLL_TO_MSG_ID = 12; //! arg1=x, arg2=y - static final int SYNC_SCROLL_TO_MSG_ID = 13; - static final int NEW_PICTURE_MSG_ID = 14; - static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; - static final int WEBCORE_INITIALIZED_MSG_ID = 16; - static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; - static final int DID_FIRST_LAYOUT_MSG_ID = 18; - static final int RECOMPUTE_FOCUS_MSG_ID = 19; - static final int NOTIFY_FOCUS_SET_MSG_ID = 20; - static final int MARK_NODE_INVALID_ID = 21; - static final int UPDATE_CLIPBOARD = 22; - static final int LONG_PRESS_ENTER = 23; - static final int PREVENT_TOUCH_ID = 24; - static final int WEBCORE_NEED_TOUCH_EVENTS = 25; + static final int SYNC_SCROLL_TO_MSG_ID = 13; + static final int NEW_PICTURE_MSG_ID = 14; + static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; + static final int WEBCORE_INITIALIZED_MSG_ID = 16; + static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; + static final int MOVE_OUT_OF_PLUGIN = 19; + static final int CLEAR_TEXT_ENTRY = 20; + static final int UPDATE_TEXT_SELECTION_MSG_ID = 21; + static final int UPDATE_CLIPBOARD = 22; + static final int LONG_PRESS_CENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // obj=Rect in doc coordinates - static final int INVAL_RECT_MSG_ID = 26; - + static final int INVAL_RECT_MSG_ID = 26; + static final int REQUEST_KEYBOARD = 27; + static final String[] HandlerDebugString = { - "REMEMBER_PASSWORD", // = 1; - "NEVER_REMEMBER_PASSWORD", // = 2; - "SWITCH_TO_SHORTPRESS", // = 3; - "SWITCH_TO_LONGPRESS", // = 4; - "5", - "UPDATE_TEXT_ENTRY_ADAPTER", // = 6; - "SWITCH_TO_ENTER", // = 7; - "RESUME_WEBCORE_UPDATE", // = 8; + "REMEMBER_PASSWORD", // = 1; + "NEVER_REMEMBER_PASSWORD", // = 2; + "SWITCH_TO_SHORTPRESS", // = 3; + "SWITCH_TO_LONGPRESS", // = 4; + "RELEASE_SINGLE_TAP", // = 5; + "REQUEST_FORM_DATA", // = 6; + "SWITCH_TO_CLICK", // = 7; + "RESUME_WEBCORE_UPDATE", // = 8; "9", "SCROLL_TO_MSG_ID", // = 10; "SCROLL_BY_MSG_ID", // = 11; @@ -382,31 +479,40 @@ public class WebView extends AbsoluteLayout "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; "WEBCORE_INITIALIZED_MSG_ID", // = 16; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; - "DID_FIRST_LAYOUT_MSG_ID", // = 18; - "RECOMPUTE_FOCUS_MSG_ID", // = 19; - "NOTIFY_FOCUS_SET_MSG_ID", // = 20; - "MARK_NODE_INVALID_ID", // = 21; + "18", // = 18; + "MOVE_OUT_OF_PLUGIN", // = 19; + "CLEAR_TEXT_ENTRY", // = 20; + "UPDATE_TEXT_SELECTION_MSG_ID", // = 21; "UPDATE_CLIPBOARD", // = 22; - "LONG_PRESS_ENTER", // = 23; + "LONG_PRESS_CENTER", // = 23; "PREVENT_TOUCH_ID", // = 24; "WEBCORE_NEED_TOUCH_EVENTS", // = 25; - "INVAL_RECT_MSG_ID" // = 26; + "INVAL_RECT_MSG_ID", // = 26; + "REQUEST_KEYBOARD" // = 27; }; - // width which view is considered to be fully zoomed out - static final int ZOOM_OUT_WIDTH = 1008; - // default scale limit. Depending on the display density private static float DEFAULT_MAX_ZOOM_SCALE; private static float DEFAULT_MIN_ZOOM_SCALE; // scale limit, which can be set through viewport meta tag in the web page private float mMaxZoomScale; private float mMinZoomScale; - private boolean mMinZoomScaleFixed = false; + private boolean mMinZoomScaleFixed = true; // initial scale in percent. 0 means using default. private int mInitialScale = 0; + // while in the zoom overview mode, the page's width is fully fit to the + // current window. The page is alive, in another words, you can click to + // follow the links. Double tap will toggle between zoom overview mode and + // the last zoom scale. + boolean mInZoomOverview = false; + + // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, + // engadget always have wider mContentWidth no matter what viewport size is. + int mZoomOverviewWidth = WebViewCore.DEFAULT_VIEWPORT_WIDTH; + float mLastScale; + // default scale. Depending on the display density. static int DEFAULT_SCALE_PERCENT; private float mDefaultScale; @@ -421,6 +527,8 @@ public class WebView extends AbsoluteLayout private float mZoomScale; private float mInvInitialZoomScale; private float mInvFinalZoomScale; + private int mInitialScrollX; + private int mInitialScrollY; private long mZoomStart; private static final int ZOOM_ANIMATION_LENGTH = 500; @@ -433,7 +541,7 @@ public class WebView extends AbsoluteLayout private static final int SNAP_X_LOCK = 4; private static final int SNAP_Y_LOCK = 5; private boolean mSnapPositive; - + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -456,7 +564,7 @@ public class WebView extends AbsoluteLayout * URI scheme for map address */ public static final String SCHEME_GEO = "geo:0,0?q="; - + private int mBackgroundColor = Color.WHITE; // Used to notify listeners of a new picture. @@ -473,7 +581,8 @@ public class WebView extends AbsoluteLayout public void onNewPicture(WebView view, Picture picture); } - public class HitTestResult { + // FIXME: Want to make this public, but need to change the API file. + public /*static*/ class HitTestResult { /** * Default HitTestResult, where the target is unknown */ @@ -543,8 +652,7 @@ public class WebView extends AbsoluteLayout private ExtendedZoomControls mZoomControls; private Runnable mZoomControlRunnable; - private ZoomButtonsController mZoomButtonsController; - private ImageView mZoomOverviewButton; + private ZoomButtonsController mZoomButtonsController; // These keep track of the center point of the zoom. They are used to // determine the point around which we should zoom. @@ -567,11 +675,11 @@ public class WebView extends AbsoluteLayout } else { zoomOut(); } - + updateZoomButtonsEnabled(); } }; - + /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. @@ -596,24 +704,33 @@ public class WebView extends AbsoluteLayout * @param defStyle The default style resource ID. */ public WebView(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, null); + } + + /** + * Construct a new WebView with layout parameters, a default style and a set + * of custom Javscript interfaces to be added to the WebView at initialization + * time. This guraratees that these interfaces will be available when the JS + * context is initialized. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + * @param javascriptInterfaces is a Map of intareface names, as keys, and + * object implementing those interfaces, as values. + * @hide pending API council approval. + */ + protected WebView(Context context, AttributeSet attrs, int defStyle, + Map javascriptInterfaces) { super(context, attrs, defStyle); init(); mCallbackProxy = new CallbackProxy(context, this); - mWebViewCore = new WebViewCore(context, this, mCallbackProxy); + mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); - mFocusData = new WebViewCore.FocusData(); - mFocusData.mFrame = 0; - mFocusData.mNode = 0; - mFocusData.mX = 0; - mFocusData.mY = 0; mScroller = new Scroller(context); - initZoomController(context); - } + mViewManager = new ViewManager(this); - private void initZoomController(Context context) { - // Create the buttons controller mZoomButtonsController = new ZoomButtonsController(this); mZoomButtonsController.setOnZoomListener(mZoomListener); // ZoomButtonsController positions the buttons at the bottom, but in @@ -626,30 +743,11 @@ public class WebView extends AbsoluteLayout params; frameParams.gravity = Gravity.RIGHT; } - - // Create the accessory buttons - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup container = mZoomButtonsController.getContainer(); - inflater.inflate(com.android.internal.R.layout.zoom_browser_accessory_buttons, container); - mZoomOverviewButton = - (ImageView) container.findViewById(com.android.internal.R.id.zoom_page_overview); - mZoomOverviewButton.setOnClickListener( - new View.OnClickListener() { - public void onClick(View v) { - mZoomButtonsController.setVisible(false); - zoomScrollOut(); - if (mLogEvent) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0); - } - } - }); } private void updateZoomButtonsEnabled() { boolean canZoomIn = mActualScale < mMaxZoomScale; - boolean canZoomOut = mActualScale > mMinZoomScale; + boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview; if (!canZoomIn && !canZoomOut) { // Hide the zoom in and out buttons, as well as the fit to page // button, if the page cannot zoom @@ -663,8 +761,6 @@ public class WebView extends AbsoluteLayout mZoomButtonsController.setZoomInEnabled(canZoomIn); mZoomButtonsController.setZoomOutEnabled(canZoomOut); } - mZoomOverviewButton.setVisibility(canZoomScrollOut() ? View.VISIBLE: - View.GONE); } private void init() { @@ -675,9 +771,11 @@ public class WebView extends AbsoluteLayout setLongClickable(true); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - final int slop = configuration.getScaledTouchSlop(); + int slop = configuration.getScaledTouchSlop(); mTouchSlopSquare = slop * slop; mMinLockSnapReverseDistance = slop; + slop = configuration.getScaledDoubleTapSlop(); + mDoubleTapSlopSquare = slop * slop; final float density = getContext().getResources().getDisplayMetrics().density; // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link @@ -811,8 +909,9 @@ public class WebView extends AbsoluteLayout /* * Return the width of the view where the content of WebView should render * to. + * Note: this can be called from WebCoreThread. */ - private int getViewWidth() { + /* package */ int getViewWidth() { if (!isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) { return getWidth(); } else { @@ -820,16 +919,36 @@ public class WebView extends AbsoluteLayout } } + /* + * returns the height of the titlebarview (if any). Does not care about + * scrolling + */ + private int getTitleHeight() { + return mTitleBar != null ? mTitleBar.getHeight() : 0; + } + + /* + * Return the amount of the titlebarview (if any) that is visible + */ + private int getVisibleTitleHeight() { + return Math.max(getTitleHeight() - mScrollY, 0); + } + /* * Return the height of the view where the content of WebView should render - * to. + * to. Note that this excludes mTitleBar, if there is one. + * Note: this can be called from WebCoreThread. */ - private int getViewHeight() { - if (!isHorizontalScrollBarEnabled() || mOverlayHorizontalScrollbar) { - return getHeight(); - } else { - return getHeight() - getHorizontalScrollbarHeight(); + /* package */ int getViewHeight() { + return getViewHeightWithTitle() - getVisibleTitleHeight(); + } + + private int getViewHeightWithTitle() { + int height = getHeight(); + if (isHorizontalScrollBarEnabled() && !mOverlayHorizontalScrollbar) { + height -= getHorizontalScrollbarHeight(); } + return height; } /** @@ -899,7 +1018,7 @@ public class WebView extends AbsoluteLayout clearTextEntry(); if (mWebViewCore != null) { // Set the handlers to null before destroying WebViewCore so no - // more messages will be posted. + // more messages will be posted. mCallbackProxy.setWebViewClient(null); mCallbackProxy.setWebChromeClient(null); // Tell WebViewCore to destroy itself @@ -930,12 +1049,23 @@ public class WebView extends AbsoluteLayout /** * If platform notifications are enabled, this should be called - * from onPause() or onStop(). + * from the Activity's onPause() or onStop(). */ public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } - + + /** + * Sets JavaScript engine flags. + * + * @param flags JS engine flags in a String + * + * @hide pending API solidification + */ + public void setJsFlags(String flags) { + mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); + } + /** * Inform WebView of the network state. This is used to set * the javascript property window.navigator.isOnline and @@ -948,7 +1078,7 @@ public class WebView extends AbsoluteLayout } /** - * Save the state of this WebView used in + * Save the state of this WebView used in * {@link android.app.Activity#onSaveInstanceState}. Please note that this * method no longer stores the display data for this WebView. The previous * behavior could potentially leak files if {@link #restoreState} was never @@ -981,6 +1111,12 @@ public class WebView extends AbsoluteLayout ArrayList history = new ArrayList(size); for (int i = 0; i < size; i++) { WebHistoryItem item = list.getItemAtIndex(i); + if (null == item) { + // FIXME: this shouldn't happen + // need to determine how item got set to null + Log.w(LOGTAG, "saveState: Unexpected null history item."); + return null; + } byte[] data = item.getFlattenedData(); if (data == null) { // It would be very odd to not have any data for a given history @@ -1026,6 +1162,9 @@ public class WebView extends AbsoluteLayout b.putInt("scrollX", mScrollX); b.putInt("scrollY", mScrollY); b.putFloat("scale", mActualScale); + if (mInZoomOverview) { + b.putFloat("lastScale", mLastScale); + } return true; } return false; @@ -1070,6 +1209,13 @@ public class WebView extends AbsoluteLayout // onSizeChanged() is called, the rest will be set // correctly mActualScale = scale; + float lastScale = b.getFloat("lastScale", -1.0f); + if (lastScale > 0) { + mInZoomOverview = true; + mLastScale = lastScale; + } else { + mInZoomOverview = false; + } invalidate(); return true; } @@ -1079,10 +1225,10 @@ public class WebView extends AbsoluteLayout /** * Restore the state of this WebView from the given map used in - * {@link android.app.Activity#onRestoreInstanceState}. This method should - * be called to restore the state of the WebView before using the object. If - * it is called after the WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable + * {@link android.app.Activity#onRestoreInstanceState}. This method should + * be called to restore the state of the WebView before using the object. If + * it is called after the WebView has had a chance to build state (load + * pages, create a back/forward list, etc.) there may be undesirable * side-effects. Please note that this method no longer restores the * display data for this WebView. See {@link #savePicture} and {@link * #restorePicture} for saving and restoring the display data. @@ -1143,6 +1289,9 @@ public class WebView extends AbsoluteLayout * @param url The url of the resource to load. */ public void loadUrl(String url) { + if (url == null) { + return; + } switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.LOAD_URL, url); clearTextEntry(); @@ -1152,18 +1301,16 @@ public class WebView extends AbsoluteLayout * Load the url with postData using "POST" method into the WebView. If url * is not a network url, it will be loaded with {link * {@link #loadUrl(String)} instead. - * + * * @param url The url of the resource to load. * @param postData The data will be passed to "POST" request. - * - * @hide pending API solidification */ public void postUrl(String url, byte[] postData) { if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); - HashMap arg = new HashMap(); - arg.put("url", url); - arg.put("data", postData); + WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); + arg.mUrl = url; + arg.mPostData = postData; mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearTextEntry(); } else { @@ -1197,7 +1344,7 @@ public class WebView extends AbsoluteLayout * able to access asset files. If the baseUrl is anything other than * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for * sub resources. - * + * * @param baseUrl Url to resolve relative paths with, if null defaults to * "about:blank" * @param data A String of data in the given encoding. @@ -1208,18 +1355,18 @@ public class WebView extends AbsoluteLayout */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String failUrl) { - + if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; } switchOutDrawHistory(); - HashMap arg = new HashMap(); - arg.put("baseUrl", baseUrl); - arg.put("data", data); - arg.put("mimeType", mimeType); - arg.put("encoding", encoding); - arg.put("failUrl", failUrl); + WebViewCore.BaseUrlData arg = new WebViewCore.BaseUrlData(); + arg.mBaseUrl = baseUrl; + arg.mData = data; + arg.mMimeType = mimeType; + arg.mEncoding = encoding; + arg.mFailUrl = failUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearTextEntry(); } @@ -1238,6 +1385,7 @@ public class WebView extends AbsoluteLayout * Reload the current url. */ public void reload() { + clearTextEntry(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); } @@ -1329,7 +1477,7 @@ public class WebView extends AbsoluteLayout ignoreSnapshot ? 1 : 0); } } - + private boolean extendScroll(int y) { int finalY = mScroller.getFinalY(); int newY = pinLocY(finalY + y); @@ -1338,7 +1486,7 @@ public class WebView extends AbsoluteLayout mScroller.extendDuration(computeDuration(0, y)); return true; } - + /** * Scroll the contents of the view up by half the view size * @param top true to jump to the top of the page @@ -1348,7 +1496,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - nativeClearFocus(-1, -1); + nativeClearCursor(); // start next trackball movement from page edge if (top) { // go to the top of the document return pinScrollTo(mScrollX, 0, true, 0); @@ -1362,10 +1510,10 @@ public class WebView extends AbsoluteLayout y = -h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } - + /** * Scroll the contents of the view down by half the page size * @param bottom true to jump to bottom of page @@ -1375,7 +1523,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - nativeClearFocus(-1, -1); + nativeClearCursor(); // start next trackball movement from page edge if (bottom) { return pinScrollTo(mScrollX, mContentHeight, true, 0); } @@ -1388,7 +1536,7 @@ public class WebView extends AbsoluteLayout y = h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } @@ -1401,7 +1549,7 @@ public class WebView extends AbsoluteLayout mContentHeight = 0; mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } - + /** * Return a new picture that captures the current display of the webview. * This is a copy of the display, and will be unaffected if the webview @@ -1412,7 +1560,7 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { - if (null == mWebViewCore) return null; // check for out of memory tab + if (null == mWebViewCore) return null; // check for out of memory tab return mWebViewCore.copyContentPicture(); } @@ -1420,17 +1568,17 @@ public class WebView extends AbsoluteLayout * Return true if the browser is displaying a TextView for text input. */ private boolean inEditingMode() { - return mTextEntry != null && mTextEntry.getParent() != null - && mTextEntry.hasFocus(); + return mWebTextView != null && mWebTextView.getParent() != null + && mWebTextView.hasFocus(); } private void clearTextEntry() { if (inEditingMode()) { - mTextEntry.remove(); + mWebTextView.remove(); } } - /** + /** * Return the current scale of the WebView * @return The current scale. */ @@ -1471,7 +1619,7 @@ public class WebView extends AbsoluteLayout } /** - * Return a HitTestResult based on the current focus node. If a HTML::a tag + * Return a HitTestResult based on the current cursor node. If a HTML::a tag * is found and the anchor has a non-javascript url, the HitTestResult type * is set to SRC_ANCHOR_TYPE and the url is set in the "extra" field. If the * anchor does not have a url or if it is a javascript url, the type will @@ -1494,26 +1642,26 @@ public class WebView extends AbsoluteLayout } HitTestResult result = new HitTestResult(); - - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { + if (nativeHasCursorNode()) { + if (nativeCursorIsTextInput()) { result.setType(HitTestResult.EDIT_TEXT_TYPE); - } else if (node.mText != null) { - String text = node.mText; - if (text.startsWith(SCHEME_TEL)) { - result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); - } else if (text.startsWith(SCHEME_MAILTO)) { - result.setType(HitTestResult.EMAIL_TYPE); - result.setExtra(text.substring(SCHEME_MAILTO.length())); - } else if (text.startsWith(SCHEME_GEO)) { - result.setType(HitTestResult.GEO_TYPE); - result.setExtra(URLDecoder.decode(text - .substring(SCHEME_GEO.length()))); - } else if (node.mIsAnchor) { - result.setType(HitTestResult.SRC_ANCHOR_TYPE); - result.setExtra(text); + } else { + String text = nativeCursorText(); + if (text != null) { + if (text.startsWith(SCHEME_TEL)) { + result.setType(HitTestResult.PHONE_TYPE); + result.setExtra(text.substring(SCHEME_TEL.length())); + } else if (text.startsWith(SCHEME_MAILTO)) { + result.setType(HitTestResult.EMAIL_TYPE); + result.setExtra(text.substring(SCHEME_MAILTO.length())); + } else if (text.startsWith(SCHEME_GEO)) { + result.setType(HitTestResult.GEO_TYPE); + result.setExtra(URLDecoder.decode(text + .substring(SCHEME_GEO.length()))); + } else if (nativeCursorIsAnchor()) { + result.setType(HitTestResult.SRC_ANCHOR_TYPE); + result.setExtra(text); + } } } } @@ -1521,12 +1669,12 @@ public class WebView extends AbsoluteLayout if (type == HitTestResult.UNKNOWN_TYPE || type == HitTestResult.SRC_ANCHOR_TYPE) { // Now check to see if it is an image. - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); String text = nativeImageURI(contentX, contentY); if (text != null) { - result.setType(type == HitTestResult.UNKNOWN_TYPE ? - HitTestResult.IMAGE_TYPE : + result.setType(type == HitTestResult.UNKNOWN_TYPE ? + HitTestResult.IMAGE_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); result.setExtra(text); } @@ -1538,37 +1686,36 @@ public class WebView extends AbsoluteLayout * Request the href of an anchor element due to getFocusNodePath returning * "href." If hrefMsg is null, this method returns immediately and does not * dispatch hrefMsg to its target. - * + * * @param hrefMsg This message will be dispatched with the result of the * request as the data member with "url" as key. The result can * be null. */ + // FIXME: API change required to change the name of this function. We now + // look at the cursor node, and not the focus node. Also, what is + // getFocusNodePath? public void requestFocusNodeHref(Message hrefMsg) { if (hrefMsg == null || mNativeClass == 0) { return; } - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsAnchor) { - // NOTE: We may already have the url of the anchor stored in - // node.mText but it may be out of date or the caller may want - // to know about javascript urls. - mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF, - node.mFramePointer, node.mNodePointer, hrefMsg); - } + if (nativeCursorIsAnchor()) { + mWebViewCore.sendMessage(EventHub.REQUEST_CURSOR_HREF, + nativeCursorFramePointer(), nativeCursorNodePointer(), + hrefMsg); } } - + /** * Request the url of the image last touched by the user. msg will be sent * to its target with a String representing the url as its object. - * + * * @param msg This message will be dispatched with the result of the request * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + if (0 == mNativeClass) return; // client isn't initialized + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); String ref = nativeImageURI(contentX, contentY); Bundle data = msg.getData(); data.putString("url", ref); @@ -1599,34 +1746,160 @@ public class WebView extends AbsoluteLayout // Expects y in view coordinates private int pinLocY(int y) { - return pinLoc(y, getViewHeight(), computeVerticalScrollRange()); + int titleH = getTitleHeight(); + // if the titlebar is still visible, just pin against 0 + if (y <= titleH) { + return Math.max(y, 0); + } + // convert to 0-based coordinate (subtract the title height) + // pin(), and then add the title height back in + return pinLoc(y - titleH, getViewHeight(), + computeVerticalScrollRange()) + titleH; + } + + /** + * A title bar which is embedded in this WebView, and scrolls along with it + * vertically, but not horizontally. + */ + private View mTitleBar; + + /** + * Since we draw the title bar ourselves, we removed the shadow from the + * browser's activity. We do want a shadow at the bottom of the title bar, + * or at the top of the screen if the title bar is not visible. This + * drawable serves that purpose. + */ + private Drawable mTitleShadow; + + /** + * Add or remove a title bar to be embedded into the WebView, and scroll + * along with it vertically, while remaining in view horizontally. Pass + * null to remove the title bar from the WebView, and return to drawing + * the WebView normally without translating to account for the title bar. + * @hide + */ + public void setEmbeddedTitleBar(View v) { + if (mTitleBar == v) return; + if (mTitleBar != null) { + removeView(mTitleBar); + } + if (null != v) { + addView(v, new AbsoluteLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0)); + if (mTitleShadow == null) { + mTitleShadow = (Drawable) mContext.getResources().getDrawable( + com.android.internal.R.drawable.title_bar_shadow); + } + } + mTitleBar = v; + } + + /** + * Given a distance in view space, convert it to content space. Note: this + * does not reflect translation, just scaling, so this should not be called + * with coordinates, but should be called for dimensions like width or + * height. + */ + private int viewToContentDimension(int d) { + return Math.round(d * mInvActualScale); + } + + /** + * Given an x coordinate in view space, convert it to content space. Also + * may be used for absolute heights (such as for the WebTextView's + * textSize, which is unaffected by the height of the title bar). + */ + /*package*/ int viewToContentX(int x) { + return viewToContentDimension(x); + } + + /** + * Given a y coordinate in view space, convert it to content space. + * Takes into account the height of the title bar if there is one + * embedded into the WebView. + */ + /*package*/ int viewToContentY(int y) { + return viewToContentDimension(y - getTitleHeight()); + } + + /** + * Given a distance in content space, convert it to view space. Note: this + * does not reflect translation, just scaling, so this should not be called + * with coordinates, but should be called for dimensions like width or + * height. + */ + /*package*/ int contentToViewDimension(int d) { + return Math.round(d * mActualScale); + } + + /** + * Given an x coordinate in content space, convert it to view + * space. + */ + /*package*/ int contentToViewX(int x) { + return contentToViewDimension(x); } - /*package*/ int viewToContent(int x) { - return Math.round(x * mInvActualScale); + /** + * Given a y coordinate in content space, convert it to view + * space. Takes into account the height of the title bar. + */ + /*package*/ int contentToViewY(int y) { + return contentToViewDimension(y) + getTitleHeight(); } - private int contentToView(int x) { - return Math.round(x * mActualScale); + private Rect contentToViewRect(Rect x) { + return new Rect(contentToViewX(x.left), contentToViewY(x.top), + contentToViewX(x.right), contentToViewY(x.bottom)); } + /* To invalidate a rectangle in content coordinates, we need to transform + the rect into view coordinates, so we can then call invalidate(...). + + Normally, we would just call contentToView[XY](...), which eventually + calls Math.round(coordinate * mActualScale). However, for invalidates, + we need to account for the slop that occurs with antialiasing. To + address that, we are a little more liberal in the size of the rect that + we invalidate. + + This liberal calculation calls floor() for the top/left, and ceil() for + the bottom/right coordinates. This catches the possible extra pixels of + antialiasing that we might have missed with just round(). + */ + // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { - invalidate(contentToView(l), contentToView(t), contentToView(r), - contentToView(b)); + final float scale = mActualScale; + final int dy = getTitleHeight(); + invalidate((int)Math.floor(l * scale), + (int)Math.floor(t * scale) + dy, + (int)Math.ceil(r * scale), + (int)Math.ceil(b * scale) + dy); } // Called by JNI to invalidate the View after a delay, given rectangle // coordinates in content space private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { - postInvalidateDelayed(delay, contentToView(l), contentToView(t), - contentToView(r), contentToView(b)); + final float scale = mActualScale; + final int dy = getTitleHeight(); + postInvalidateDelayed(delay, + (int)Math.floor(l * scale), + (int)Math.floor(t * scale) + dy, + (int)Math.ceil(r * scale), + (int)Math.ceil(b * scale) + dy); } - private Rect contentToView(Rect x) { - return new Rect(contentToView(x.left), contentToView(x.top) - , contentToView(x.right), contentToView(x.bottom)); + private void invalidateContentRect(Rect r) { + viewInvalidate(r.left, r.top, r.right, r.bottom); + } + + // stop the scroll animation, and don't let a subsequent fling add + // to the existing velocity + private void abortAnimation() { + mScroller.abortAnimation(); + mLastVelocity = 0; } /* call from webcoreview.draw(), so we're still executing in the UI thread @@ -1637,7 +1910,7 @@ public class WebView extends AbsoluteLayout if ((w | h) == 0) { return; } - + // don't abort a scroll animation if we didn't change anything if (mContentWidth != w || mContentHeight != h) { // record new dimensions @@ -1651,12 +1924,15 @@ public class WebView extends AbsoluteLayout int oldY = mScrollY; mScrollX = pinLocX(mScrollX); mScrollY = pinLocY(mScrollY); - // android.util.Log.d("skia", "recordNewContentSize - - // abortAnimation"); - mScroller.abortAnimation(); // just in case if (oldX != mScrollX || oldY != mScrollY) { sendOurVisibleRect(); } + if (!mScroller.isFinished()) { + // We are in the middle of a scroll. Repin the final scroll + // position. + mScroller.setFinalX(pinLocX(mScroller.getFinalX())); + mScroller.setFinalY(pinLocY(mScroller.getFinalY())); + } } } contentSizeChanged(updateLayout); @@ -1688,7 +1964,8 @@ public class WebView extends AbsoluteLayout int oldY = mScrollY; float ratio = scale * mInvActualScale; // old inverse float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; - float sy = ratio * oldY + (ratio - 1) * mZoomCenterY; + float sy = ratio * oldY + (ratio - 1) + * (mZoomCenterY - getTitleHeight()); // now update our new scale and inverse if (scale != mActualScale && !mPreviewZoomOnly) { @@ -1697,7 +1974,10 @@ public class WebView extends AbsoluteLayout mActualScale = scale; mInvActualScale = 1 / scale; - // as we don't have animation for scaling, don't do animation + // Scale all the child views + mViewManager.scaleAll(); + + // as we don't have animation for scaling, don't do animation // for scrolling, as it causes weird intermediate state // pinScrollTo(Math.round(sx), Math.round(sy)); mScrollX = pinLocX(Math.round(sx)); @@ -1718,18 +1998,21 @@ public class WebView extends AbsoluteLayout private Rect sendOurVisibleRect() { Rect rect = new Rect(); calcOurContentVisibleRect(rect); - if (mFindIsUp) { - rect.bottom -= viewToContent(FIND_HEIGHT); - } // Rect.equals() checks for null input. if (!rect.equals(mLastVisibleRectSent)) { + Point pos = new Point(rect.left, rect.top); mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, - rect.left, rect.top); + nativeMoveGeneration(), 0, pos); mLastVisibleRectSent = rect; } Rect globalRect = new Rect(); if (getGlobalVisibleRect(globalRect) && !globalRect.equals(mLastGlobalRect)) { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "sendOurVisibleRect=(" + globalRect.left + "," + + globalRect.top + ",r=" + globalRect.right + ",b=" + + globalRect.bottom); + } // TODO: the global offset is only used by windowRect() // in ChromeClientAndroid ; other clients such as touch // and mouse events could return view + screen relative points. @@ -1744,15 +2027,30 @@ public class WebView extends AbsoluteLayout Point p = new Point(); getGlobalVisibleRect(r, p); r.offset(-p.x, -p.y); + if (mFindIsUp) { + r.bottom -= mFindHeight; + } } // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); - r.left = viewToContent(r.left); - r.top = viewToContent(r.top); - r.right = viewToContent(r.right); - r.bottom = viewToContent(r.bottom); + r.left = viewToContentX(r.left); + // viewToContentY will remove the total height of the title bar. Add + // the visible height back in to account for the fact that if the title + // bar is partially visible, the part of the visible rect which is + // displaying our content is displaced by that amount. + r.top = viewToContentY(r.top + getVisibleTitleHeight()); + r.right = viewToContentX(r.right); + r.bottom = viewToContentY(r.bottom); + } + + static class ViewSizeData { + int mWidth; + int mHeight; + int mTextWrapWidth; + float mScale; + boolean mIgnoreHeight; } /** @@ -1762,7 +2060,8 @@ public class WebView extends AbsoluteLayout * @return true if new values were sent */ private boolean sendViewSizeZoom() { - int newWidth = Math.round(getViewWidth() * mInvActualScale); + int viewWidth = getViewWidth(); + int newWidth = Math.round(viewWidth * mInvActualScale); int newHeight = Math.round(getViewHeight() * mInvActualScale); /* * Because the native side may have already done a layout before the @@ -1777,8 +2076,17 @@ public class WebView extends AbsoluteLayout } // Avoid sending another message if the dimensions have not changed. if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) { - mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, - newWidth, newHeight, new Float(mActualScale)); + ViewSizeData data = new ViewSizeData(); + data.mWidth = newWidth; + data.mHeight = newHeight; + // while in zoom overview mode, the text are wrapped to the screen + // width matching mLastScale. So that we don't trigger re-flow while + // toggling between overview mode and normal mode. + data.mTextWrapWidth = mInZoomOverview ? Math.round(viewWidth + / mLastScale) : newWidth; + data.mScale = mActualScale; + data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure; + mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; return true; @@ -1791,26 +2099,40 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryWidth; } else { - return contentToView(mContentWidth); + // to avoid rounding error caused unnecessary scrollbar, use floor + return (int) Math.floor(mContentWidth * mActualScale); } } - // Make sure this stays in sync with the actual height of the FindDialog. - private static final int FIND_HEIGHT = 79; - @Override protected int computeVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else { - int height = contentToView(mContentHeight); - if (mFindIsUp) { - height += FIND_HEIGHT; - } - return height; + // to avoid rounding error caused unnecessary scrollbar, use floor + return (int) Math.floor(mContentHeight * mActualScale); } } + @Override + protected int computeVerticalScrollOffset() { + return Math.max(mScrollY - getTitleHeight(), 0); + } + + @Override + protected int computeVerticalScrollExtent() { + return getViewHeight(); + } + + /** @hide */ + @Override + protected void onDrawVerticalScrollBar(Canvas canvas, + Drawable scrollBar, + int l, int t, int r, int b) { + scrollBar.setBounds(l, t + getVisibleTitleHeight(), r, b); + scrollBar.draw(canvas); + } + /** * Get the url for the current page. This is not always the same as the url * passed to WebViewClient.onPageStarted because although the load for @@ -1821,10 +2143,10 @@ public class WebView extends AbsoluteLayout WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } - + /** - * Get the original url for the current page. This is not always the same - * as the url passed to WebViewClient.onPageStarted because although the + * Get the original url for the current page. This is not always the same + * as the url passed to WebViewClient.onPageStarted because although the * load for that url has begun, the current page may not have changed. * Also, there may have been redirects resulting in a different url to that * originally requested. @@ -1855,6 +2177,15 @@ public class WebView extends AbsoluteLayout return h != null ? h.getFavicon() : null; } + /** + * Get the touch icon url for the apple-touch-icon element. + * @hide + */ + public String getTouchIconUrl() { + WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); + return h != null ? h.getTouchIconUrl() : null; + } + /** * Get the progress for the current page. * @return The progress for the current page between 0 and 100. @@ -1862,7 +2193,7 @@ public class WebView extends AbsoluteLayout public int getProgress() { return mCallbackProxy.getProgress(); } - + /** * @return the height of the HTML content. */ @@ -1871,30 +2202,84 @@ public class WebView extends AbsoluteLayout } /** - * Pause all layout, parsing, and javascript timers. This can be useful if - * the WebView is not visible or the application has been paused. + * @return the width of the HTML content. + * @hide + */ + public int getContentWidth() { + return mContentWidth; + } + + /** + * Pause all layout, parsing, and javascript timers for all webviews. This + * is a global requests, not restricted to just this webview. This can be + * useful if the application has been paused. */ public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** - * Resume all layout, parsing, and javascript timers. This will resume - * dispatching all timers. + * Resume all layout, parsing, and javascript timers for all webviews. + * This will resume dispatching all timers. */ public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** - * Clear the resource cache. This will cause resources to be re-downloaded - * if accessed again. - *

    - * Note: this really needs to be a static method as it clears cache for all - * WebView. But we need mWebViewCore to send message to WebCore thread, so - * we can't make this static. + * Call this to pause any extra processing associated with this view and + * its associated DOM/plugins/javascript/etc. For example, if the view is + * taken offscreen, this could be called to reduce unnecessary CPU and/or + * network traffic. When the view is again "active", call onResume(). + * + * Note that this differs from pauseTimers(), which affects all views/DOMs + * @hide + */ + public void onPause() { + if (!mIsPaused) { + mIsPaused = true; + mWebViewCore.sendMessage(EventHub.ON_PAUSE); + } + } + + /** + * Call this to balanace a previous call to onPause() + * @hide + */ + public void onResume() { + if (mIsPaused) { + mIsPaused = false; + mWebViewCore.sendMessage(EventHub.ON_RESUME); + } + } + + /** + * Returns true if the view is paused, meaning onPause() was called. Calling + * onResume() sets the paused state back to false. + * @hide + */ + public boolean isPaused() { + return mIsPaused; + } + + /** + * Call this to inform the view that memory is low so that it can + * free any available memory. + */ + public void freeMemory() { + mWebViewCore.sendMessage(EventHub.FREE_MEMORY); + } + + /** + * Clear the resource cache. Note that the cache is per-application, so + * this will clear the cache for all WebViews used. + * + * @param includeDiskFiles If false, only the RAM cache is cleared. */ public void clearCache(boolean includeDiskFiles) { + // Note: this really needs to be a static method as it clears cache for all + // WebView. But we need mWebViewCore to send message to WebCore thread, so + // we can't make this static. mWebViewCore.sendMessage(EventHub.CLEAR_CACHE, includeDiskFiles ? 1 : 0, 0); } @@ -1906,7 +2291,7 @@ public class WebView extends AbsoluteLayout public void clearFormData() { if (inEditingMode()) { AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); + mWebTextView.setAdapterCustom(adapter); } } @@ -1940,12 +2325,13 @@ public class WebView extends AbsoluteLayout /* * Highlight and scroll to the next occurance of String in findAll. - * Wraps the page infinitely, and scrolls. Must be called after + * Wraps the page infinitely, and scrolls. Must be called after * calling findAll. * * @param forward Direction to search. */ public void findNext(boolean forward) { + if (0 == mNativeClass) return; // client isn't initialized nativeFindNext(forward); } @@ -1956,7 +2342,12 @@ public class WebView extends AbsoluteLayout * that were found. */ public int findAll(String find) { - mFindIsUp = true; + if (0 == mNativeClass) return 0; // client isn't initialized + if (mFindIsUp == false) { + recordNewContentSize(mContentWidth, mContentHeight + mFindHeight, + false); + mFindIsUp = true; + } int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); invalidate(); return result; @@ -1965,12 +2356,10 @@ public class WebView extends AbsoluteLayout // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; + private int mFindHeight; - private native int nativeFindAll(String findLower, String findUpper); - private native void nativeFindNext(boolean forward); - /** - * Return the first substring consisting of the address of a physical + * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number @@ -1983,21 +2372,51 @@ public class WebView extends AbsoluteLayout * All names must be correctly capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed + * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { - return WebViewCore.nativeFindAddress(addr); + return findAddress(addr, false); + } + + /** + * @hide + * Return the first substring consisting of the address of a physical + * location. Currently, only addresses in the United States are detected, + * and consist of: + * - a house number + * - a street name + * - a street type (Road, Circle, etc), either spelled out or abbreviated + * - a city name + * - a state or territory, either spelled out or two-letter abbr. + * - an optional 5 digit or 9 digit zip code. + * + * Names are optionally capitalized, and the zip code, if present, + * must be valid for the state. The street type must be a standard USPS + * spelling or abbreviation. The state or territory must also be spelled + * or abbreviated using USPS standards. The house number may not exceed + * five digits. + * @param addr The string to search for addresses. + * @param caseInsensitive addr Set to true to make search ignore case. + * + * @return the address, or if no address is found, return null. + */ + public static String findAddress(String addr, boolean caseInsensitive) { + return WebViewCore.nativeFindAddress(addr, caseInsensitive); } /* * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { - mFindIsUp = false; + if (mFindIsUp) { + recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, + false); + mFindIsUp = false; + } nativeSetFindIsDown(); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. @@ -2005,6 +2424,16 @@ public class WebView extends AbsoluteLayout invalidate(); } + /** + * @hide + */ + public void setFindDialogHeight(int height) { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "setFindDialogHeight height=" + height); + } + mFindHeight = height; + } + /** * Query the document to see if it contains any image references. The * message object will be dispatched with arg1 being set to 1 if images @@ -2047,7 +2476,6 @@ public class WebView extends AbsoluteLayout private boolean pinScrollBy(int dx, int dy, boolean animate, int animationDuration) { return pinScrollTo(mScrollX + dx, mScrollY + dy, animate, animationDuration); } - // helper to pin the scrollTo parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollTo(int x, int y, boolean animate, int animationDuration) { @@ -2059,15 +2487,14 @@ public class WebView extends AbsoluteLayout if ((dx | dy) == 0) { return false; } - - if (true && animate) { + if (animate) { // Log.d(LOGTAG, "startScroll: " + dx + " " + dy); - mScroller.startScroll(mScrollX, mScrollY, dx, dy, animationDuration > 0 ? animationDuration : computeDuration(dx, dy)); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { - mScroller.abortAnimation(); // just in case + abortAnimation(); // just in case scrollTo(x, y); } return true; @@ -2075,16 +2502,16 @@ public class WebView extends AbsoluteLayout // Scale from content to view coordinates, and pin. // Also called by jni webview.cpp - private void setContentScrollBy(int cx, int cy, boolean animate) { + private boolean setContentScrollBy(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. // TODO: as we switchOutDrawHistory when trackball or navigation // keys are hit, this should be safe. Right? - return; + return false; } - cx = contentToView(cx); - cy = contentToView(cy); + cx = contentToViewDimension(cx); + cy = contentToViewDimension(cy); if (mHeightCanMeasure) { // move our visible rect according to scroll request if (cy != 0) { @@ -2098,17 +2525,18 @@ public class WebView extends AbsoluteLayout // FIXME: Why do we only scroll horizontally if there is no // vertical scroll? // Log.d(LOGTAG, "setContentScrollBy cy=" + cy); - if (cy == 0 && cx != 0) { - pinScrollBy(cx, 0, animate, 0); - } + return cy == 0 && cx != 0 && pinScrollBy(cx, 0, animate, 0); } else { - pinScrollBy(cx, cy, animate, 0); + return pinScrollBy(cx, cy, animate, 0); } } // scale from content to view coordinates, and pin - // return true if pin caused the final x/y different than the request cx/cy; - // return false if the view scroll to the exact position as it is requested. + // return true if pin caused the final x/y different than the request cx/cy, + // and a future scroll may reach the request cx/cy after our size has + // changed + // return false if the view scroll to the exact position as it is requested, + // where negative numbers are taken to mean 0 private boolean setContentScrollTo(int cx, int cy) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture @@ -2118,12 +2546,35 @@ public class WebView extends AbsoluteLayout // saved scroll position, it is ok to skip this. return false; } - int vx = contentToView(cx); - int vy = contentToView(cy); + int vx; + int vy; + if ((cx | cy) == 0) { + // If the page is being scrolled to (0,0), do not add in the title + // bar's height, and simply scroll to (0,0). (The only other work + // in contentToView_ is to multiply, so this would not change 0.) + vx = 0; + vy = 0; + } else { + vx = contentToViewX(cx); + vy = contentToViewY(cy); + } // Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" + // vx + " " + vy + "]"); + // Some mobile sites attempt to scroll the title bar off the page by + // scrolling to (0,1). If we are at the top left corner of the + // page, assume this is an attempt to scroll off the title bar, and + // animate the title bar off screen slowly enough that the user can see + // it. + if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0) { + pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + // Since we are animating, we have not yet reached the desired + // scroll position. Do not return true to request another attempt + return false; + } pinScrollTo(vx, vy, false, 0); - if (mScrollX != vx || mScrollY != vy) { + // If the request was to scroll to a negative coordinate, treat it as if + // it was a request to scroll to 0 + if ((mScrollX != vx && cx >= 0) || (mScrollY != vy && cy >= 0)) { return true; } else { return false; @@ -2137,8 +2588,8 @@ public class WebView extends AbsoluteLayout // is used in the view system. return; } - int vx = contentToView(cx); - int vy = contentToView(cy); + int vx = contentToViewX(cx); + int vy = contentToViewY(cy); pinScrollTo(vx, vy, true, 0); } @@ -2155,12 +2606,12 @@ public class WebView extends AbsoluteLayout } if (mHeightCanMeasure) { - if (getMeasuredHeight() != contentToView(mContentHeight) + if (getMeasuredHeight() != contentToViewDimension(mContentHeight) && updateLayout) { requestLayout(); } } else if (mWidthCanMeasure) { - if (getMeasuredWidth() != contentToView(mContentWidth) + if (getMeasuredWidth() != contentToViewDimension(mContentWidth) && updateLayout) { requestLayout(); } @@ -2200,6 +2651,16 @@ public class WebView extends AbsoluteLayout mCallbackProxy.setWebChromeClient(client); } + /** + * Gets the chrome handler. + * @return the current WebChromeClient instance. + * + * @hide API council approval. + */ + public WebChromeClient getWebChromeClient() { + return mCallbackProxy.getWebChromeClient(); + } + /** * Set the Picture listener. This is an interface used to receive * notifications of a new Picture. @@ -2245,10 +2706,9 @@ public class WebView extends AbsoluteLayout * @param interfaceName The name to used to expose the class in Javascript */ public void addJavascriptInterface(Object obj, String interfaceName) { - // Use Hashmap rather than Bundle as Bundles can't cope with Objects - HashMap arg = new HashMap(); - arg.put("object", obj); - arg.put("interfaceName", interfaceName); + WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); + arg.mObject = obj; + arg.mInterfaceName = interfaceName; mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } @@ -2265,26 +2725,19 @@ public class WebView extends AbsoluteLayout /** * Return the list of currently loaded plugins. * @return The list of currently loaded plugins. + * + * @deprecated This was used for Gears, which has been deprecated. */ + @Deprecated public static synchronized PluginList getPluginList() { - if (sPluginList == null) { - sPluginList = new PluginList(); - } - return sPluginList; + return null; } /** - * Signal the WebCore thread to refresh its list of plugins. Use - * this if the directory contents of one of the plugin directories - * has been modified and needs its changes reflecting. May cause - * plugin load and/or unload. - * @param reloadOpenPages Set to true to reload all open pages. + * @deprecated This was used for Gears, which has been deprecated. */ - public void refreshPlugins(boolean reloadOpenPages) { - if (mWebViewCore != null) { - mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages); - } - } + @Deprecated + public void refreshPlugins(boolean reloadOpenPages) { } //------------------------------------------------------------------------- // Override View methods @@ -2292,44 +2745,56 @@ public class WebView extends AbsoluteLayout @Override protected void finalize() throws Throwable { - destroy(); + try { + destroy(); + } finally { + super.finalize(); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (child == mTitleBar) { + // When drawing the title bar, move it horizontally to always show + // at the top of the WebView. + mTitleBar.offsetLeftAndRight(mScrollX - mTitleBar.getLeft()); + } + return super.drawChild(canvas, child, drawingTime); } - + @Override protected void onDraw(Canvas canvas) { // if mNativeClass is 0, the WebView has been destroyed. Do nothing. if (mNativeClass == 0) { return; } - if (mWebViewCore.mEndScaleZoom) { - mWebViewCore.mEndScaleZoom = false; - if (mTouchMode >= FIRST_SCROLL_ZOOM - && mTouchMode <= LAST_SCROLL_ZOOM) { - setHorizontalScrollBarEnabled(true); - setVerticalScrollBarEnabled(true); - mTouchMode = TOUCH_DONE_MODE; - } + int saveCount = canvas.save(); + if (mTitleBar != null) { + canvas.translate(0, (int) mTitleBar.getHeight()); + } + // Update the buttons in the picture, so when we draw the picture + // to the screen, they are in the correct state. + // Tell the native side if user is a) touching the screen, + // b) pressing the trackball down, or c) pressing the enter key + // If the cursor is on a button, we need to draw it in the pressed + // state. + // If mNativeClass is 0, we should not reach here, so we do not + // need to check it again. + nativeRecordButtons(hasFocus() && hasWindowFocus(), + mTouchMode == TOUCH_SHORTPRESS_START_MODE + || mTrackballDown || mGotCenterDown, false); + drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); + canvas.restoreToCount(saveCount); + + // Now draw the shadow. + if (mTitleBar != null) { + int y = mScrollY + getVisibleTitleHeight(); + int height = (int) (5f * getContext().getResources() + .getDisplayMetrics().density); + mTitleShadow.setBounds(mScrollX, y, mScrollX + getWidth(), + y + height); + mTitleShadow.draw(canvas); } - int sc = canvas.save(); - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { - scrollZoomDraw(canvas); - } else { - nativeRecomputeFocus(); - // Update the buttons in the picture, so when we draw the picture - // to the screen, they are in the correct state. - // Tell the native side if user is a) touching the screen, - // b) pressing the trackball down, or c) pressing the enter key - // If the focus is a button, we need to draw it in the pressed - // state. - // If mNativeClass is 0, we should not reach here, so we do not - // need to check it again. - nativeRecordButtons(hasFocus() && hasWindowFocus(), - mTouchMode == TOUCH_SHORTPRESS_START_MODE - || mTrackballDown || mGotEnterDown, false); - drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); - } - canvas.restoreToCount(sc); - if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } @@ -2345,57 +2810,116 @@ public class WebView extends AbsoluteLayout @Override public boolean performLongClick() { + if (mNativeClass != 0 && nativeCursorIsTextInput()) { + // Send the click so that the textfield is in focus + // FIXME: When we start respecting changes to the native textfield's + // selection, need to make sure that this does not change it. + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + rebuildWebTextView(); + } if (inEditingMode()) { - return mTextEntry.performLongClick(); + return mWebTextView.performLongClick(); } else { return super.performLongClick(); } } - private void drawCoreAndFocusRing(Canvas canvas, int color, - boolean drawFocus) { - if (mDrawHistory) { + boolean inAnimateZoom() { + return mZoomScale != 0; + } + + /** + * Need to adjust the WebTextView after a change in zoom, since mActualScale + * has changed. This is especially important for password fields, which are + * drawn by the WebTextView, since it conveys more information than what + * webkit draws. Thus we need to reposition it to show in the correct + * place. + */ + private boolean mNeedToAdjustWebTextView; + + private void drawCoreAndCursorRing(Canvas canvas, int color, + boolean drawCursorRing) { + if (mDrawHistory) { canvas.scale(mActualScale, mActualScale); canvas.drawPicture(mHistoryPicture); return; } boolean animateZoom = mZoomScale != 0; - boolean animateScroll = !mScroller.isFinished() + boolean animateScroll = !mScroller.isFinished() || mVelocityTracker != null; if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); if (interval < ZOOM_ANIMATION_LENGTH) { float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; - zoomScale = 1.0f / (mInvInitialZoomScale + zoomScale = 1.0f / (mInvInitialZoomScale + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); invalidate(); } else { zoomScale = mZoomScale; // set mZoomScale to be 0 as we have done animation mZoomScale = 0; + // call invalidate() again to draw with the final filters + invalidate(); + if (mNeedToAdjustWebTextView) { + mNeedToAdjustWebTextView = false; + Rect contentBounds = nativeFocusCandidateNodeBounds(); + Rect vBox = contentToViewRect(contentBounds); + Rect visibleRect = new Rect(); + calcOurVisibleRect(visibleRect); + if (visibleRect.contains(vBox)) { + // As a result of the zoom, the textfield is now on + // screen. Place the WebTextView in its new place, + // accounting for our new scroll/zoom values. + mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + contentToViewDimension( + nativeFocusCandidateTextSize())); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + // If it is a password field, start drawing the + // WebTextView once again. + if (nativeFocusCandidateIsPassword()) { + mWebTextView.setInPassword(true); + } + } else { + // The textfield is now off screen. The user probably + // was not zooming to see the textfield better. Remove + // the WebTextView. If the user types a key, and the + // textfield is still in focus, we will reconstruct + // the WebTextView and scroll it back on screen. + mWebTextView.remove(); + } + } } - float scale = (mActualScale - zoomScale) * mInvActualScale; - float tx = scale * (mZoomCenterX + mScrollX); - float ty = scale * (mZoomCenterY + mScrollY); - - // this block pins the translate to "legal" bounds. This makes the - // animation a bit non-obvious, but it means we won't pop when the - // "real" zoom takes effect - if (true) { - // canvas.translate(mScrollX, mScrollY); - tx -= mScrollX; - ty -= mScrollY; - tx = -pinLoc(-Math.round(tx), getViewWidth(), Math - .round(mContentWidth * zoomScale)); - ty = -pinLoc(-Math.round(ty), getViewHeight(), Math - .round(mContentHeight * zoomScale)); - tx += mScrollX; - ty += mScrollY; - } + // calculate the intermediate scroll position. As we need to use + // zoomScale, we can't use pinLocX/Y directly. Copy the logic here. + float scale = zoomScale * mInvInitialZoomScale; + int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) + - mZoomCenterX); + tx = -pinLoc(tx, getViewWidth(), Math.round(mContentWidth + * zoomScale)) + mScrollX; + int titleHeight = getTitleHeight(); + int ty = Math.round(scale + * (mInitialScrollY + mZoomCenterY - titleHeight) + - (mZoomCenterY - titleHeight)); + ty = -(ty <= titleHeight ? Math.max(ty, 0) : pinLoc(ty + - titleHeight, getViewHeight(), Math.round(mContentHeight + * zoomScale)) + titleHeight) + mScrollY; canvas.translate(tx, ty); canvas.scale(zoomScale, zoomScale); + if (inEditingMode() && !mNeedToAdjustWebTextView + && mZoomScale != 0) { + // The WebTextView is up. Keep track of this so we can adjust + // its size and placement when we finish zooming + mNeedToAdjustWebTextView = true; + // If it is in password mode, turn it off so it does not draw + // misplaced. + if (nativeFocusCandidateIsPassword()) { + mWebTextView.setInPassword(false); + } + } } else { canvas.scale(mActualScale, mActualScale); } @@ -2404,14 +2928,14 @@ public class WebView extends AbsoluteLayout animateScroll); if (mNativeClass == 0) return; - if (mShiftIsPressed) { + if (mShiftIsPressed && !animateZoom) { if (mTouchSelection) { nativeDrawSelectionRegion(canvas); } else { - nativeDrawSelection(canvas, mSelectX, mSelectY, - mExtendSelection); + nativeDrawSelection(canvas, mInvActualScale, getTitleHeight(), + mSelectX, mSelectY, mExtendSelection); } - } else if (drawFocus) { + } else if (drawCursorRing) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; HitTestResult hitTest = getHitTestResult(); @@ -2422,7 +2946,7 @@ public class WebView extends AbsoluteLayout LONG_PRESS_TIMEOUT); } } - nativeDrawFocusRing(canvas); + nativeDrawCursorRing(canvas); } // When the FindDialog is up, only draw the matches if we are not in // the process of scrolling them into view. @@ -2431,363 +2955,6 @@ public class WebView extends AbsoluteLayout } } - private native void nativeDrawMatches(Canvas canvas); - - private float scrollZoomGridScale(float invScale) { - float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID) - / (float) SCROLL_ZOOM_GRID; - return 1.0f / griddedInvScale; - } - - private float scrollZoomX(float scale) { - int width = getViewWidth(); - float maxScrollZoomX = mContentWidth * scale - width; - int maxX = mContentWidth - width; - return -(maxScrollZoomX > 0 ? mZoomScrollX * maxScrollZoomX / maxX - : maxScrollZoomX / 2); - } - - private float scrollZoomY(float scale) { - int height = getViewHeight(); - float maxScrollZoomY = mContentHeight * scale - height; - int maxY = mContentHeight - height; - return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY - : maxScrollZoomY / 2); - } - - private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) { - final float ADORNMENT_LEN = 16.0f; - float width = frame.width(); - float height = frame.height(); - Path path = new Path(); - path.moveTo(-ADORNMENT_LEN, -ADORNMENT_LEN); - path.lineTo(0, 0); - path.lineTo(width, 0); - path.lineTo(width + ADORNMENT_LEN, -ADORNMENT_LEN); - path.moveTo(-ADORNMENT_LEN, height + ADORNMENT_LEN); - path.lineTo(0, height); - path.lineTo(width, height); - path.lineTo(width + ADORNMENT_LEN, height + ADORNMENT_LEN); - path.moveTo(0, 0); - path.lineTo(0, height); - path.moveTo(width, 0); - path.lineTo(width, height); - path.offset(frame.left, frame.top); - canvas.drawPath(path, paint); - } - - // Returns frame surrounding magified portion of screen while - // scroll-zoom is enabled. The frame is also used to center the - // zoom-in zoom-out points at the start and end of the animation. - private Rect scrollZoomFrame(int width, int height, float halfScale) { - Rect scrollFrame = new Rect(); - scrollFrame.set(mZoomScrollX, mZoomScrollY, - mZoomScrollX + width, mZoomScrollY + height); - if (mContentWidth * mZoomScrollLimit < width) { - float scale = zoomFrameScaleX(width, halfScale, 1.0f); - float offsetX = (width * scale - width) * 0.5f; - scrollFrame.left -= offsetX; - scrollFrame.right += offsetX; - } - if (mContentHeight * mZoomScrollLimit < height) { - float scale = zoomFrameScaleY(height, halfScale, 1.0f); - float offsetY = (height * scale - height) * 0.5f; - scrollFrame.top -= offsetY; - scrollFrame.bottom += offsetY; - } - return scrollFrame; - } - - private float zoomFrameScaleX(int width, float halfScale, float noScale) { - // mContentWidth > width > mContentWidth * mZoomScrollLimit - if (mContentWidth <= width) { - return halfScale; - } - float part = (width - mContentWidth * mZoomScrollLimit) - / (width * (1 - mZoomScrollLimit)); - return halfScale * part + noScale * (1.0f - part); - } - - private float zoomFrameScaleY(int height, float halfScale, float noScale) { - if (mContentHeight <= height) { - return halfScale; - } - float part = (height - mContentHeight * mZoomScrollLimit) - / (height * (1 - mZoomScrollLimit)); - return halfScale * part + noScale * (1.0f - part); - } - - private float scrollZoomMagScale(float invScale) { - return (invScale * 2 + mInvActualScale) / 3; - } - - private void scrollZoomDraw(Canvas canvas) { - float invScale = mZoomScrollInvLimit; - int elapsed = 0; - if (mTouchMode != SCROLL_ZOOM_OUT) { - elapsed = (int) Math.min(System.currentTimeMillis() - - mZoomScrollStart, SCROLL_ZOOM_DURATION); - float transitionScale = (mZoomScrollInvLimit - mInvActualScale) - * elapsed / SCROLL_ZOOM_DURATION; - if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { - invScale = mInvActualScale + transitionScale; - } else { /* if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) */ - invScale = mZoomScrollInvLimit - transitionScale; - } - } - float scale = scrollZoomGridScale(invScale); - invScale = 1.0f / scale; - int width = getViewWidth(); - int height = getViewHeight(); - float halfScale = scrollZoomMagScale(invScale); - Rect scrollFrame = scrollZoomFrame(width, height, halfScale); - if (elapsed == SCROLL_ZOOM_DURATION) { - if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { - setHorizontalScrollBarEnabled(true); - setVerticalScrollBarEnabled(true); - updateTextEntry(); - scrollTo((int) (scrollFrame.centerX() * mActualScale) - - (width >> 1), (int) (scrollFrame.centerY() - * mActualScale) - (height >> 1)); - mTouchMode = TOUCH_DONE_MODE; - } else { - mTouchMode = SCROLL_ZOOM_OUT; - } - } - float newX = scrollZoomX(scale); - float newY = scrollZoomY(scale); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX - + ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", " - + mZoomScrollY + ")" + " invScale=" + invScale + " scale=" - + scale); - } - canvas.translate(newX, newY); - canvas.scale(scale, scale); - boolean animating = mTouchMode != SCROLL_ZOOM_OUT; - if (mDrawHistory) { - int sc = canvas.save(Canvas.CLIP_SAVE_FLAG); - Rect clip = new Rect(0, 0, mHistoryPicture.getWidth(), - mHistoryPicture.getHeight()); - canvas.clipRect(clip, Region.Op.DIFFERENCE); - canvas.drawColor(mBackgroundColor); - canvas.restoreToCount(sc); - canvas.drawPicture(mHistoryPicture); - } else { - mWebViewCore.drawContentPicture(canvas, mBackgroundColor, - animating, true); - } - if (mTouchMode == TOUCH_DONE_MODE) { - return; - } - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(30.0f); - paint.setARGB(0x50, 0, 0, 0); - int maxX = mContentWidth - width; - int maxY = mContentHeight - height; - if (true) { // experiment: draw hint to place finger off magnify area - drawMagnifyFrame(canvas, scrollFrame, paint); - } else { - canvas.drawRect(scrollFrame, paint); - } - int sc = canvas.save(); - canvas.clipRect(scrollFrame); - float halfX = (float) mZoomScrollX / maxX; - if (mContentWidth * mZoomScrollLimit < width) { - halfX = zoomFrameScaleX(width, 0.5f, halfX); - } - float halfY = (float) mZoomScrollY / maxY; - if (mContentHeight * mZoomScrollLimit < height) { - halfY = zoomFrameScaleY(height, 0.5f, halfY); - } - canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX - , mZoomScrollY + height * halfY); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=(" - + width + ", " + height + ") half=(" + halfX + ", " - + halfY + ")"); - } - if (mDrawHistory) { - canvas.drawPicture(mHistoryPicture); - } else { - mWebViewCore.drawContentPicture(canvas, mBackgroundColor, - animating, false); - } - canvas.restoreToCount(sc); - if (mTouchMode != SCROLL_ZOOM_OUT) { - invalidate(); - } - } - - private void zoomScrollTap(float x, float y) { - float scale = scrollZoomGridScale(mZoomScrollInvLimit); - float left = scrollZoomX(scale); - float top = scrollZoomY(scale); - int width = getViewWidth(); - int height = getViewHeight(); - x -= width * scale / 2; - y -= height * scale / 2; - mZoomScrollX = Math.min(mContentWidth - width - , Math.max(0, (int) ((x - left) / scale))); - mZoomScrollY = Math.min(mContentHeight - height - , Math.max(0, (int) ((y - top) / scale))); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left - + ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", " - + mZoomScrollY + ")" + " x=" + x + " y=" + y); - } - } - - private boolean canZoomScrollOut() { - if (mContentWidth == 0 || mContentHeight == 0) { - return false; - } - int width = getViewWidth(); - int height = getViewHeight(); - float x = (float) width / (float) mContentWidth; - float y = (float) height / (float) mContentHeight; - mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y)); - mZoomScrollInvLimit = 1.0f / mZoomScrollLimit; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "canZoomScrollOut" - + " mInvActualScale=" + mInvActualScale - + " mZoomScrollLimit=" + mZoomScrollLimit - + " mZoomScrollInvLimit=" + mZoomScrollInvLimit - + " mContentWidth=" + mContentWidth - + " mContentHeight=" + mContentHeight - ); - } - // don't zoom out unless magnify area is at least half as wide - // or tall as content - float limit = mZoomScrollLimit * 2; - return mContentWidth >= width * limit - || mContentHeight >= height * limit; - } - - private void startZoomScrollOut() { - setHorizontalScrollBarEnabled(false); - setVerticalScrollBarEnabled(false); - if (getSettings().getBuiltInZoomControls()) { - if (mZoomButtonsController.isVisible()) { - mZoomButtonsController.setVisible(false); - } - } else { - if (mZoomControlRunnable != null) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - } - int width = getViewWidth(); - int height = getViewHeight(); - int halfW = width >> 1; - mLastTouchX = halfW; - int halfH = height >> 1; - mLastTouchY = halfH; - mScroller.abortAnimation(); - mZoomScrollStart = System.currentTimeMillis(); - Rect zoomFrame = scrollZoomFrame(width, height - , scrollZoomMagScale(mZoomScrollInvLimit)); - mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale) - - (zoomFrame.width() >> 1)); - mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale) - - (zoomFrame.height() >> 1)); - scrollTo(0, 0); // triggers inval, starts animation - clearTextEntry(); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=(" - + mZoomScrollX + ", " + mZoomScrollY +")"); - } - } - - private void zoomScrollOut() { - if (canZoomScrollOut() == false) { - mTouchMode = TOUCH_DONE_MODE; - return; - } - startZoomScrollOut(); - mTouchMode = SCROLL_ZOOM_ANIMATION_OUT; - invalidate(); - } - - private void moveZoomScrollWindow(float x, float y) { - if (Math.abs(x - mLastZoomScrollRawX) < 1.5f - && Math.abs(y - mLastZoomScrollRawY) < 1.5f) { - return; - } - mLastZoomScrollRawX = x; - mLastZoomScrollRawY = y; - int oldX = mZoomScrollX; - int oldY = mZoomScrollY; - int width = getViewWidth(); - int height = getViewHeight(); - int maxZoomX = mContentWidth - width; - if (maxZoomX > 0) { - int maxScreenX = width - (int) Math.ceil(width - * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-X" - + " maxScreenX=" + maxScreenX + " width=" + width - + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); - } - x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX; - x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit); - mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x)); - } - int maxZoomY = mContentHeight - height; - if (maxZoomY > 0) { - int maxScreenY = height - (int) Math.ceil(height - * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-Y" - + " maxScreenY=" + maxScreenY + " height=" + height - + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); - } - y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY; - y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit); - mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y)); - } - if (oldX != mZoomScrollX || oldY != mZoomScrollY) { - invalidate(); - } - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow" - + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")" - + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")" - + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")" - + " last=("+mLastScrollX+", "+mLastScrollY+")" - + " x=" + x + " y=" + y); - } - } - - private void setZoomScrollIn() { - mZoomScrollStart = System.currentTimeMillis(); - } - - private float mZoomScrollLimit; - private float mZoomScrollInvLimit; - private int mLastScrollX; - private int mLastScrollY; - private long mZoomScrollStart; - private int mZoomScrollX; - private int mZoomScrollY; - private float mLastZoomScrollRawX = -1000.0f; - private float mLastZoomScrollRawY = -1000.0f; - // The zoomed scale varies from 1.0 to DEFAULT_MIN_ZOOM_SCALE == 0.25. - // The zoom animation duration SCROLL_ZOOM_DURATION == 0.5. - // Two pressures compete for gridding; a high frame rate (e.g. 20 fps) - // and minimizing font cache allocations (fewer frames is better). - // A SCROLL_ZOOM_GRID of 6 permits about 20 zoom levels over 0.5 seconds: - // the inverse of: 1.0, 1.16, 1.33, 1.5, 1.67, 1.84, 2.0, etc. to 4.0 - private static final int SCROLL_ZOOM_GRID = 6; - private static final int SCROLL_ZOOM_DURATION = 500; - // Make it easier to get to the bottom of a document by reserving a 32 - // pixel buffer, for when the starting drag is a bit below the bottom of - // the magnify frame. - private static final int SCROLL_ZOOM_FINGER_BUFFER = 32; - // draw history private boolean mDrawHistory = false; private Picture mHistoryPicture = null; @@ -2802,7 +2969,7 @@ public class WebView extends AbsoluteLayout // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this - if (mDrawHistory) { + if (mDrawHistory && mWebViewCore.pictureReady()) { mDrawHistory = false; invalidate(); int oldScrollX = mScrollX; @@ -2818,72 +2985,29 @@ public class WebView extends AbsoluteLayout } } - /** - * Class representing the node which is focused. - */ - private class FocusNode { - public FocusNode() { - mBounds = new Rect(); - } - // Only to be called by JNI - private void setAll(boolean isTextField, boolean isTextArea, boolean - isPassword, boolean isAnchor, boolean isRtlText, int maxLength, - int textSize, int boundsX, int boundsY, int boundsRight, int - boundsBottom, int nodePointer, int framePointer, String text, - String name, int rootTextGeneration) { - mIsTextField = isTextField; - mIsTextArea = isTextArea; - mIsPassword = isPassword; - mIsAnchor = isAnchor; - mIsRtlText = isRtlText; - - mMaxLength = maxLength; - mTextSize = textSize; - - mBounds.set(boundsX, boundsY, boundsRight, boundsBottom); - - - mNodePointer = nodePointer; - mFramePointer = framePointer; - mText = text; - mName = name; - mRootTextGeneration = rootTextGeneration; - } - public boolean mIsTextField; - public boolean mIsTextArea; - public boolean mIsPassword; - public boolean mIsAnchor; - public boolean mIsRtlText; - - public int mSelectionStart; - public int mSelectionEnd; - public int mMaxLength; - public int mTextSize; - - public Rect mBounds; - - public int mNodePointer; - public int mFramePointer; - public String mText; - public String mName; - public int mRootTextGeneration; - } - - // Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(), - // and ONLY if it returns true; - private FocusNode mFocusNode = new FocusNode(); - + WebViewCore.CursorData cursorData() { + WebViewCore.CursorData result = new WebViewCore.CursorData(); + result.mMoveGeneration = nativeMoveGeneration(); + result.mFrame = nativeCursorFramePointer(); + Point position = nativeCursorPosition(); + result.mX = position.x; + result.mY = position.y; + return result; + } + /** * Delete text from start to end in the focused textfield. If there is no - * focus, or if start == end, silently fail. If start and end are out of + * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { mTextGeneration++; - mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + WebViewCore.TextSelectionData data + = new WebViewCore.TextSelectionData(start, end); + mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, mTextGeneration, 0, + data); } /** @@ -2893,119 +3017,128 @@ public class WebView extends AbsoluteLayout * @param end End of selection. */ /* package */ void setSelection(int start, int end) { - mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } // Called by JNI when a touch event puts a textfield into focus. - private void displaySoftKeyboard() { + private void displaySoftKeyboard(boolean isTextView) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEntry, 0); - mTextEntry.enableScrollOnScreen(true); - // Now we need to fake a touch event to place the cursor where the - // user touched. - AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) - mTextEntry.getLayoutParams(); - if (lp != null) { - // Take the last touch and adjust for the location of the - // TextDialog. - float x = mLastTouchX + (float) (mScrollX - lp.x); - float y = mLastTouchY + (float) (mScrollY - lp.y); - mTextEntry.fakeTouchEvent(x, y); - } - } - - private void updateTextEntry() { - if (mTextEntry == null) { - mTextEntry = new TextDialog(mContext, WebView.this); - // Initialize our generation number. - mTextGeneration = 0; + + if (isTextView) { + if (mWebTextView == null) return; + + imm.showSoftInput(mWebTextView, 0); + if (mInZoomOverview) { + // if in zoom overview mode, call doDoubleTap() to bring it back + // to normal mode so that user can enter text. + doDoubleTap(); + } + } + else { // used by plugins + imm.showSoftInput(this, 0); } - // If we do not have focus, do nothing until we gain focus. - if (!hasFocus() && !mTextEntry.hasFocus() - || (mTouchMode >= FIRST_SCROLL_ZOOM - && mTouchMode <= LAST_SCROLL_ZOOM)) { - mNeedsUpdateTextEntry = true; + } + + // Called by WebKit to instruct the UI to hide the keyboard + private void hideSoftKeyboard() { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + imm.hideSoftInputFromWindow(this.getWindowToken(), 0); + } + + /* + * This method checks the current focus and cursor and potentially rebuilds + * mWebTextView to have the appropriate properties, such as password, + * multiline, and what text it contains. It also removes it if necessary. + */ + /* package */ void rebuildWebTextView() { + // If the WebView does not have focus, do nothing until it gains focus. + if (!hasFocus() && (null == mWebTextView || !mWebTextView.hasFocus())) { return; } boolean alreadyThere = inEditingMode(); - if (0 == mNativeClass || !nativeUpdateFocusNode()) { + // inEditingMode can only return true if mWebTextView is non-null, + // so we can safely call remove() if (alreadyThere) + if (0 == mNativeClass || !nativeFocusCandidateIsTextInput()) { if (alreadyThere) { - mTextEntry.remove(); + mWebTextView.remove(); } return; } - FocusNode node = mFocusNode; - if (!node.mIsTextField && !node.mIsTextArea) { - if (alreadyThere) { - mTextEntry.remove(); - } - return; + // At this point, we know we have found an input field, so go ahead + // and create the WebTextView if necessary. + if (mWebTextView == null) { + mWebTextView = new WebTextView(mContext, WebView.this); + // Initialize our generation number. + mTextGeneration = 0; } - mTextEntry.setTextSize(contentToView(node.mTextSize)); - Rect visibleRect = sendOurVisibleRect(); + mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + contentToViewDimension(nativeFocusCandidateTextSize())); + Rect visibleRect = new Rect(); + calcOurContentVisibleRect(visibleRect); // Note that sendOurVisibleRect calls viewToContent, so the coordinates // should be in content coordinates. - if (!Rect.intersects(node.mBounds, visibleRect)) { - // Node is not on screen, so do not bother. - return; + Rect bounds = nativeFocusCandidateNodeBounds(); + if (!Rect.intersects(bounds, visibleRect)) { + mWebTextView.bringIntoView(); } - int x = node.mBounds.left; - int y = node.mBounds.top; - int width = node.mBounds.width(); - int height = node.mBounds.height(); - if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) { + String text = nativeFocusCandidateText(); + int nodePointer = nativeFocusCandidatePointer(); + if (alreadyThere && mWebTextView.isSameTextField(nodePointer)) { // It is possible that we have the same textfield, but it has moved, // i.e. In the case of opening/closing the screen. // In that case, we need to set the dimensions, but not the other // aspects. // We also need to restore the selection, which gets wrecked by // calling setTextEntryRect. - Spannable spannable = (Spannable) mTextEntry.getText(); + Spannable spannable = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(spannable); int end = Selection.getSelectionEnd(spannable); - setTextEntryRect(x, y, width, height); // If the text has been changed by webkit, update it. However, if // there has been more UI text input, ignore it. We will receive // another update when that text is recognized. - if (node.mText != null && !node.mText.equals(spannable.toString()) - && node.mRootTextGeneration == mTextGeneration) { - mTextEntry.setTextAndKeepSelection(node.mText); + if (text != null && !text.equals(spannable.toString()) + && nativeTextGeneration() == mTextGeneration) { + mWebTextView.setTextAndKeepSelection(text); } else { Selection.setSelection(spannable, start, end); } } else { - String text = node.mText; - setTextEntryRect(x, y, width, height); - mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT : - Gravity.NO_GRAVITY); + Rect vBox = contentToViewRect(bounds); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + mWebTextView.setGravity(nativeFocusCandidateIsRtlText() ? + Gravity.RIGHT : Gravity.NO_GRAVITY); // this needs to be called before update adapter thread starts to - // ensure the mTextEntry has the same node pointer - mTextEntry.setNodePointer(node.mNodePointer); + // ensure the mWebTextView has the same node pointer + mWebTextView.setNodePointer(nodePointer); int maxLength = -1; - if (node.mIsTextField) { - maxLength = node.mMaxLength; + boolean isTextField = nativeFocusCandidateIsTextField(); + if (isTextField) { + maxLength = nativeFocusCandidateMaxLength(); + String name = nativeFocusCandidateName(); if (mWebViewCore.getSettings().getSaveFormData() - && node.mName != null) { - HashMap data = new HashMap(); - data.put("text", node.mText); + && name != null) { Message update = mPrivateHandler.obtainMessage( - UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0, - data); - UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter( - node.mName, getUrl(), update); + REQUEST_FORM_DATA, nodePointer); + RequestFormData updater = new RequestFormData(name, + getUrl(), update); Thread t = new Thread(updater); t.start(); } } - mTextEntry.setMaxLength(maxLength); + mWebTextView.setMaxLength(maxLength); AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); - mTextEntry.setSingleLine(node.mIsTextField); - mTextEntry.setInPassword(node.mIsPassword); + mWebTextView.setAdapterCustom(adapter); + mWebTextView.setSingleLine(isTextField); + mWebTextView.setInPassword(nativeFocusCandidateIsPassword()); if (null == text) { - mTextEntry.setText("", 0, 0); + mWebTextView.setText("", 0, 0); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView null == text"); + } } else { // Change to true to enable the old style behavior, where // entering a textfield/textarea always set the selection to the @@ -3016,24 +3149,35 @@ public class WebView extends AbsoluteLayout // textarea. Testing out a new behavior, where textfields set // selection at the end, and textareas at the beginning. if (false) { - mTextEntry.setText(text, 0, text.length()); - } else if (node.mIsTextField) { + mWebTextView.setText(text, 0, text.length()); + } else if (isTextField) { int length = text.length(); - mTextEntry.setText(text, length, length); + mWebTextView.setText(text, length, length); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView length=" + length); + } } else { - mTextEntry.setText(text, 0, 0); + mWebTextView.setText(text, 0, 0); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView !isTextField"); + } } } - mTextEntry.requestFocus(); + mWebTextView.requestFocus(); } } - private class UpdateTextEntryAdapter implements Runnable { + /* + * This class requests an Adapter for the WebTextView which shows past + * entries stored in the database. It is a Runnable so that it can be done + * in its own thread, without slowing down the UI. + */ + private class RequestFormData implements Runnable { private String mName; private String mUrl; private Message mUpdateMessage; - public UpdateTextEntryAdapter(String name, String url, Message msg) { + public RequestFormData(String name, String url, Message msg) { mName = name; mUrl = url; mUpdateMessage = msg; @@ -3044,29 +3188,21 @@ public class WebView extends AbsoluteLayout if (pastEntries.size() > 0) { AutoCompleteAdapter adapter = new AutoCompleteAdapter(mContext, pastEntries); - ((HashMap) mUpdateMessage.obj).put("adapter", adapter); + mUpdateMessage.obj = adapter; mUpdateMessage.sendToTarget(); } } } - private void setTextEntryRect(int x, int y, int width, int height) { - x = contentToView(x); - y = contentToView(y); - width = contentToView(width); - height = contentToView(height); - mTextEntry.setRect(x, y, width, height); - } - - // This is used to determine long press with the enter key, or - // a center key. Does not affect long press with the trackball/touch. - private boolean mGotEnterDown = false; + // This is used to determine long press with the center key. Does not + // affect long press with the trackball/touch. + private boolean mGotCenterDown = false; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() - + ", " + event); + + ", " + event + ", unicode=" + event.getUnicodeChar()); } if (mNativeClass == 0) { @@ -3084,37 +3220,33 @@ public class WebView extends AbsoluteLayout // Bubble up the key event if // 1. it is a system key; or - // 2. the host application wants to handle it; or - // 3. webview is in scroll-zoom state; + // 2. the host application wants to handle it; if (event.isSystem() - || mCallbackProxy.uiOverrideKeyEvent(event) - || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) { + || mCallbackProxy.uiOverrideKeyEvent(event)) { return false; } - if (mShiftIsPressed == false && nativeFocusNodeWantsKeyEvents() == false - && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (mShiftIsPressed == false && nativeCursorWantsKeyEvents() == false + && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { mExtendSelection = false; mShiftIsPressed = true; - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - mSelectX = contentToView(node.mBounds.left); - mSelectY = contentToView(node.mBounds.top); + if (nativeHasCursorNode()) { + Rect rect = nativeCursorNodeBounds(); + mSelectX = contentToViewX(rect.left); + mSelectY = contentToViewY(rect.top); } else { mSelectX = mScrollX + (int) mLastTouchX; mSelectY = mScrollY + (int) mLastTouchY; } - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - nativeClearFocus(contentX, contentY); + nativeHideCursor(); } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { // always handle the navigation keys in the UI thread switchOutDrawHistory(); - if (navHandledKey(keyCode, 1, false, event.getEventTime())) { + if (navHandledKey(keyCode, 1, false, event.getEventTime(), false)) { playSoundEffect(keyCodeToSoundsEffect(keyCode)); return true; } @@ -3122,13 +3254,12 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { - mGotEnterDown = true; + mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); + .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); // Already checked mNativeClass, so we do not need to check it // again. nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); @@ -3138,6 +3269,15 @@ public class WebView extends AbsoluteLayout return false; } + if (keyCode != KeyEvent.KEYCODE_SHIFT_LEFT + && keyCode != KeyEvent.KEYCODE_SHIFT_RIGHT) { + // turn off copy select if a shift-key combo is pressed + mExtendSelection = mShiftIsPressed = false; + if (mTouchMode == TOUCH_SELECT_MODE) { + mTouchMode = TOUCH_INIT_MODE; + } + } + if (getSettings().getNavDump()) { switch (keyCode) { case KeyEvent.KEYCODE_4: @@ -3166,8 +3306,30 @@ public class WebView extends AbsoluteLayout } } + if (nativeCursorIsPlugin()) { + nativeUpdatePluginReceivesEvents(); + invalidate(); + } else if (nativeCursorIsTextInput()) { + // This message will put the node in focus, for the DOM's notion + // of focus, and make the focuscontroller active + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + // This will bring up the WebTextView and put it in focus, for + // our view system's notion of focus + rebuildWebTextView(); + // Now we need to pass the event to it + return mWebTextView.onKeyDown(keyCode, event); + } else if (nativeHasFocusNode()) { + // In this case, the cursor is not on a text input, but the focus + // might be. Check it, and if so, hand over to the WebTextView. + rebuildWebTextView(); + if (inEditingMode()) { + return mWebTextView.onKeyDown(keyCode, event); + } + } + // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_DOWN, event); // return true as DOM handles the key @@ -3180,20 +3342,19 @@ public class WebView extends AbsoluteLayout @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() - + ", " + event); + + ", " + event + ", unicode=" + event.getUnicodeChar()); } if (mNativeClass == 0) { return false; } - // special CALL handling when focus node's href is "tel:XXX" - if (keyCode == KeyEvent.KEYCODE_CALL && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - String text = node.mText; - if (!node.mIsTextField && !node.mIsTextArea && text != null + // special CALL handling when cursor node's href is "tel:XXX" + if (keyCode == KeyEvent.KEYCODE_CALL && nativeHasCursorNode()) { + String text = nativeCursorText(); + if (!nativeCursorIsTextInput() && text != null && text.startsWith(SCHEME_TEL)) { Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); getContext().startActivity(intent); @@ -3208,19 +3369,7 @@ public class WebView extends AbsoluteLayout return false; } - // special handling in scroll_zoom state - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode - && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) { - setZoomScrollIn(); - mTouchMode = SCROLL_ZOOM_ANIMATION_IN; - invalidate(); - return true; - } - return false; - } - - if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { if (commitCopy()) { return true; @@ -3234,55 +3383,42 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { // remove the long press message first - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); - mGotEnterDown = false; + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); + mGotCenterDown = false; - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { - if (mShiftIsPressed) { - return false; - } - if (getSettings().supportZoom()) { - if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - zoomScrollOut(); - } else { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); - } - mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT); - mTouchMode = TOUCH_DOUBLECLICK_MODE; - } - return true; - } + if (mShiftIsPressed) { + return false; } + // perform the single click Rect visibleRect = sendOurVisibleRect(); // Note that sendOurVisibleRect calls viewToContent, so the // coordinates should be in content coordinates. - if (nativeUpdateFocusNode()) { - if (Rect.intersects(mFocusNode.mBounds, visibleRect)) { - nativeSetFollowedLink(true); - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, - EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, - new WebViewCore.FocusData(mFocusData)); - playSoundEffect(SoundEffectConstants.CLICK); - if (!mCallbackProxy.uiOverrideUrlLoading(mFocusNode.mText)) { - // use CLICK instead of KEY_DOWN/KEY_UP so that we can - // trigger mouse click events - mWebViewCore.sendMessage(EventHub.CLICK); - } - } - return true; + if (!nativeCursorIntersects(visibleRect)) { + return false; } - // Bubble up the key event as WebView doesn't handle it - return false; + nativeSetFollowedLink(true); + nativeUpdatePluginReceivesEvents(); + WebViewCore.CursorData data = cursorData(); + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); + playSoundEffect(SoundEffectConstants.CLICK); + boolean isTextInput = nativeCursorIsTextInput(); + if (isTextInput || !mCallbackProxy.uiOverrideUrlLoading( + nativeCursorText())) { + mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, + nativeCursorNodePointer()); + } + if (isTextInput) { + rebuildWebTextView(); + displaySoftKeyboard(true); + } + return true; } // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_UP, event); // return true as DOM handles the key @@ -3292,16 +3428,15 @@ public class WebView extends AbsoluteLayout // Bubble up the key event as WebView doesn't handle it return false; } - + /** * @hide */ public void emulateShiftHeld() { + if (0 == mNativeClass) return; // client isn't initialized mExtendSelection = false; mShiftIsPressed = true; - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - nativeClearFocus(contentX, contentY); + nativeHideCursor(); } private boolean commitCopy() { @@ -3349,16 +3484,13 @@ public class WebView extends AbsoluteLayout // Clean up the zoom controller mZoomButtonsController.setVisible(false); } - + // Implementation for OnHierarchyChangeListener public void onChildViewAdded(View parent, View child) {} - + public void onChildViewRemoved(View p, View child) { if (child == this) { - if (inEditingMode()) { - clearTextEntry(); - mNeedsUpdateTextEntry = true; - } + clearTextEntry(); } } @@ -3371,26 +3503,25 @@ public class WebView extends AbsoluteLayout public void onGlobalFocusChanged(View oldFocus, View newFocus) { } - // To avoid drawing the focus ring, and remove the TextView when our window + // To avoid drawing the cursor ring, and remove the TextView when our window // loses focus. @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (hasWindowFocus) { if (hasFocus()) { // If our window regained focus, and we have focus, then begin - // drawing the focus ring, and restore the TextView if - // necessary. - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); - } + // drawing the cursor ring + mDrawCursorRing = true; if (mNativeClass != 0) { nativeRecordButtons(true, false, true); + if (inEditingMode()) { + mWebViewCore.sendMessage(EventHub.SET_ACTIVE, 1, 0); + } } } else { // If our window gained focus, but we do not have it, do not - // draw the focus ring. - mDrawFocusRing = false; + // draw the cursor ring. + mDrawCursorRing = false; // We do not call nativeRecordButtons here because we assume // that when we lost focus, or window focus, it got called with // false for the first parameter @@ -3399,39 +3530,49 @@ public class WebView extends AbsoluteLayout if (getSettings().getBuiltInZoomControls() && !mZoomButtonsController.isVisible()) { /* * The zoom controls come in their own window, so our window - * loses focus. Our policy is to not draw the focus ring if + * loses focus. Our policy is to not draw the cursor ring if * our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ - // If our window has lost focus, stop drawing the focus ring - mDrawFocusRing = false; + // If our window has lost focus, stop drawing the cursor ring + mDrawCursorRing = false; } mGotKeyDown = false; mShiftIsPressed = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerInactive(); } invalidate(); super.onWindowFocusChanged(hasWindowFocus); } + /* + * Pass a message to WebCore Thread, telling the WebCore::Page's + * FocusController to be "inactive" so that it will + * not draw the blinking cursor. It gets set to "active" to draw the cursor + * in WebViewCore.cpp, when the WebCore thread receives key events/clicks. + */ + /* package */ void setFocusControllerInactive() { + // Do not need to also check whether mWebViewCore is null, because + // mNativeClass is only set if mWebViewCore is non null + if (mNativeClass == 0) return; + mWebViewCore.sendMessage(EventHub.SET_ACTIVE, 0, 0); + } + @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction); } if (focused) { // When we regain focus, if we have window focus, resume drawing - // the focus ring, and add the TextView if necessary. + // the cursor ring if (hasWindowFocus()) { - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); - mNeedsUpdateTextEntry = false; - } + mDrawCursorRing = true; if (mNativeClass != 0) { nativeRecordButtons(true, false, true); } @@ -3442,12 +3583,13 @@ public class WebView extends AbsoluteLayout } } else { // When we lost focus, unless focus went to the TextView (which is - // true if we are in editing mode), stop drawing the focus ring. + // true if we are in editing mode), stop drawing the cursor ring. if (!inEditingMode()) { - mDrawFocusRing = false; + mDrawCursorRing = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerInactive(); } mGotKeyDown = false; } @@ -3459,13 +3601,20 @@ public class WebView extends AbsoluteLayout protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; + if (mZoomScale == 0) { // unless we're already zooming + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + } // update mMinZoomScale if the minimum zoom scale is not fixed if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) getViewWidth() - / Math.max(ZOOM_OUT_WIDTH, mContentWidth); + // when change from narrow screen to wide screen, the new viewWidth + // can be wider than the old content width. We limit the minimum + // scale to 1.0f. The proper minimum scale will be calculated when + // the new picture shows up. + mMinZoomScale = Math.min(1.0f, (float) getViewWidth() + / (mDrawHistory ? mHistoryPicture.getWidth() + : mZoomOverviewWidth)); } // we always force, in case our height changed, in which case we still @@ -3476,10 +3625,11 @@ public class WebView extends AbsoluteLayout @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); + sendOurVisibleRect(); } - - + + @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean dispatch = true; @@ -3523,7 +3673,7 @@ public class WebView extends AbsoluteLayout return false; } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode=" + mTouchMode); } @@ -3540,20 +3690,17 @@ public class WebView extends AbsoluteLayout if (x > getViewWidth() - 1) { x = getViewWidth() - 1; } - if (y > getViewHeight() - 1) { - y = getViewHeight() - 1; + if (y > getViewHeightWithTitle() - 1) { + y = getViewHeightWithTitle() - 1; } // pass the touch events from UI thread to WebCore thread - if (mForwardTouchEvents && mTouchMode != SCROLL_ZOOM_OUT - && mTouchMode != SCROLL_ZOOM_ANIMATION_IN - && mTouchMode != SCROLL_ZOOM_ANIMATION_OUT - && (action != MotionEvent.ACTION_MOVE || - eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) { + if (mForwardTouchEvents && (action != MotionEvent.ACTION_MOVE + || eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) { WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData(); ted.mAction = action; - ted.mX = viewToContent((int) x + mScrollX); - ted.mY = viewToContent((int) y + mScrollY); + ted.mX = viewToContentX((int) x + mScrollX); + ted.mY = viewToContentY((int) y + mScrollY); mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); mLastSentTouchTime = eventTime; } @@ -3563,15 +3710,11 @@ public class WebView extends AbsoluteLayout switch (action) { case MotionEvent.ACTION_DOWN: { - if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN - || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { - // no interaction while animation is in progress - break; - } else if (mTouchMode == SCROLL_ZOOM_OUT) { - mLastScrollX = mZoomScrollX; - mLastScrollY = mZoomScrollY; - // If two taps are close, ignore the first tap - } else if (!mScroller.isFinished()) { + mPreventDrag = PREVENT_DRAG_NO; + if (!mScroller.isFinished()) { + // stop the current scroll animation, but if this is + // the start of a fling, allow it to add to the current + // fling's velocity mScroller.abortAnimation(); mTouchMode = TOUCH_DRAG_START_MODE; mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); @@ -3579,22 +3722,35 @@ public class WebView extends AbsoluteLayout mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; mTouchMode = TOUCH_SELECT_MODE; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), false); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), false); mTouchSelection = mExtendSelection = true; + } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { + mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); + if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { + mTouchMode = TOUCH_DOUBLE_TAP_MODE; + } else { + // commit the short press action for the previous tap + doShortPress(); + // continue, mTouchMode should be still TOUCH_INIT_MODE + } } else { mTouchMode = TOUCH_INIT_MODE; - mPreventDrag = mForwardTouchEvents; + mPreventDrag = mForwardTouchEvents ? PREVENT_DRAG_MAYBE_YES + : PREVENT_DRAG_NO; + mWebViewCore.sendMessage( + EventHub.UPDATE_FRAME_CACHE_IF_LOADING); if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } } // Trigger the link - if (mTouchMode == TOUCH_INIT_MODE) { + if (mTouchMode == TOUCH_INIT_MODE + || mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT); } @@ -3607,40 +3763,38 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_MOVE: { - if (mTouchMode == TOUCH_DONE_MODE - || mTouchMode == SCROLL_ZOOM_ANIMATION_IN - || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { + if (mTouchMode == TOUCH_DONE_MODE) { // no dragging during scroll zoom animation break; } - if (mTouchMode == SCROLL_ZOOM_OUT) { - // while fully zoomed out, move the virtual window - moveZoomScrollWindow(x, y); - break; - } mVelocityTracker.addMovement(ev); if (mTouchMode != TOUCH_DRAG_MODE) { if (mTouchMode == TOUCH_SELECT_MODE) { mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), true); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), true); invalidate(); break; } - if (mPreventDrag || (deltaX * deltaX + deltaY * deltaY) - < mTouchSlopSquare) { + if ((deltaX * deltaX + deltaY * deltaY) < mTouchSlopSquare) { + break; + } + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + // track mLastTouchTime as we may need to do fling at + // ACTION_UP + mLastTouchTime = eventTime; break; } - if (mTouchMode == TOUCH_SHORTPRESS_MODE || mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); - } else if (mTouchMode == TOUCH_INIT_MODE) { + } else if (mTouchMode == TOUCH_INIT_MODE + || mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); } @@ -3657,24 +3811,22 @@ public class WebView extends AbsoluteLayout mTouchMode = TOUCH_DRAG_MODE; WebViewCore.pauseUpdate(mWebViewCore); - int contentX = viewToContent((int) x + mScrollX); - int contentY = viewToContent((int) y + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } - nativeClearFocus(contentX, contentY); - // remove the zoom anchor if there is any - if (mZoomScale != 0) { - mWebViewCore - .sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0); + if (!mDragFromTextInput) { + nativeHideCursor(); } WebSettings settings = getSettings(); if (settings.supportZoom() && settings.getBuiltInZoomControls() && !mZoomButtonsController.isVisible() - && (canZoomScrollOut() || - mMinZoomScale < mMaxZoomScale)) { + && mMinZoomScale < mMaxZoomScale) { mZoomButtonsController.setVisible(true); + int count = settings.getDoubleTapToastCount(); + if (mInZoomOverview && count > 0) { + settings.setDoubleTapToastCount(--count); + Toast.makeText(mContext, + com.android.internal.R.string.double_tap_toast, + Toast.LENGTH_LONG).show(); + } } } @@ -3698,7 +3850,7 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ax > MAX_SLOPE_FOR_DIAG * ay) && - ((mSnapPositive && + ((mSnapPositive && deltaX < -mMinLockSnapReverseDistance) || (!mSnapPositive && deltaX > mMinLockSnapReverseDistance))) { @@ -3712,9 +3864,9 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ay > MAX_SLOPE_FOR_DIAG * ax) && - ((mSnapPositive && + ((mSnapPositive && deltaY < -mMinLockSnapReverseDistance) - || (!mSnapPositive && + || (!mSnapPositive && deltaY > mMinLockSnapReverseDistance))) { mSnapScrollMode = SNAP_Y_LOCK; } @@ -3723,11 +3875,25 @@ public class WebView extends AbsoluteLayout if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) { - scrollBy(deltaX, 0); + if (deltaX == 0) { + // keep the scrollbar on the screen even there is no + // scroll + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + } else { + scrollBy(deltaX, 0); + } mLastTouchX = x; } else if (mSnapScrollMode == SNAP_Y || mSnapScrollMode == SNAP_Y_LOCK) { - scrollBy(0, deltaY); + if (deltaY == 0) { + // keep the scrollbar on the screen even there is no + // scroll + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + } else { + scrollBy(0, deltaY); + } mLastTouchY = y; } else { scrollBy(deltaX, deltaY); @@ -3740,12 +3906,11 @@ public class WebView extends AbsoluteLayout if (!getSettings().getBuiltInZoomControls()) { boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; - boolean showMagnify = canZoomScrollOut(); - if (mZoomControls != null && (showPlusMinus || showMagnify)) { + if (mZoomControls != null && showPlusMinus) { if (mZoomControls.getVisibility() == View.VISIBLE) { mPrivateHandler.removeCallbacks(mZoomControlRunnable); } else { - mZoomControls.show(showPlusMinus, showMagnify); + mZoomControls.show(showPlusMinus, false); } mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -3753,6 +3918,9 @@ public class WebView extends AbsoluteLayout } if (done) { + // keep the scrollbar on the screen even there is no scroll + awakenScrollBars(ViewConfiguration.getScrollDefaultDelay(), + false); // return false to indicate that we can't pan out of the // view space return false; @@ -3762,41 +3930,54 @@ public class WebView extends AbsoluteLayout case MotionEvent.ACTION_UP: { mLastTouchUpTime = eventTime; switch (mTouchMode) { - case TOUCH_INIT_MODE: // tap - case TOUCH_SHORTPRESS_START_MODE: - case TOUCH_SHORTPRESS_MODE: + case TOUCH_DOUBLE_TAP_MODE: // double tap mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); - mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; - doShortPress(); + doDoubleTap(); break; case TOUCH_SELECT_MODE: commitCopy(); mTouchSelection = false; break; - case SCROLL_ZOOM_ANIMATION_IN: - case SCROLL_ZOOM_ANIMATION_OUT: - // no action during scroll animation - break; - case SCROLL_ZOOM_OUT: - if (LOGV_ENABLED) { - Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT" - + " eventTime - mLastTouchTime=" - + (eventTime - mLastTouchTime)); - } - // for now, always zoom back when the drag completes - if (true || eventTime - mLastTouchTime < TAP_TIMEOUT) { - // but if we tap, zoom in where we tap - if (eventTime - mLastTouchTime < TAP_TIMEOUT) { - zoomScrollTap(x, y); + case TOUCH_INIT_MODE: // tap + case TOUCH_SHORTPRESS_START_MODE: + case TOUCH_SHORTPRESS_MODE: + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquare) { + Log.w(LOGTAG, "Miss a drag as we are waiting for" + + " WebCore's response for touch down."); + if (computeHorizontalScrollExtent() < computeHorizontalScrollRange() + || computeVerticalScrollExtent() < computeVerticalScrollRange()) { + // we will not rewrite drag code here, but we + // will try fling if it applies. + WebViewCore.pauseUpdate(mWebViewCore); + // fall through to TOUCH_DRAG_MODE + } else { + break; + } + } else { + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + // if mPreventDrag is not confirmed, treat it as + // no so that it won't block tap or double tap. + mPreventDrag = PREVENT_DRAG_NO; + } + if (mPreventDrag == PREVENT_DRAG_NO) { + if (mTouchMode == TOUCH_INIT_MODE) { + mPrivateHandler.sendMessageDelayed( + mPrivateHandler.obtainMessage( + RELEASE_SINGLE_TAP), + ViewConfiguration.getDoubleTapTimeout()); + } else { + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } } - // start zooming in back to the original view - setZoomScrollIn(); - mTouchMode = SCROLL_ZOOM_ANIMATION_IN; - invalidate(); + break; } - break; case TOUCH_DRAG_MODE: + // redraw in high-quality, as we're done dragging + invalidate(); // if the user waits a while w/o moving before the // up, we don't want to do a fling if (eventTime - mLastTouchTime <= MIN_FLING_TIME) { @@ -3804,6 +3985,7 @@ public class WebView extends AbsoluteLayout doFling(); break; } + mLastVelocity = 0; WebViewCore.resumeUpdate(mWebViewCore); break; case TOUCH_DRAG_START_MODE: @@ -3828,27 +4010,19 @@ public class WebView extends AbsoluteLayout mVelocityTracker.recycle(); mVelocityTracker = null; } - if (mTouchMode == SCROLL_ZOOM_OUT || - mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { - scrollTo(mZoomScrollX, mZoomScrollY); - } else if (mTouchMode == TOUCH_DRAG_MODE) { + if (mTouchMode == TOUCH_DRAG_MODE) { WebViewCore.resumeUpdate(mWebViewCore); } mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } - nativeClearFocus(contentX, contentY); + nativeHideCursor(); break; } } return true; } - + private long mTrackballFirstTime = 0; private long mTrackballLastTime = 0; private float mTrackballRemainsX = 0.0f; @@ -3870,14 +4044,14 @@ public class WebView extends AbsoluteLayout private boolean mShiftIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; - private long mLastFocusTime = 0; - private Rect mLastFocusBounds; + private long mLastCursorTime = 0; + private Rect mLastCursorBounds; // Set by default; BrowserActivity clears to interpret trackball data - // directly for movement. Currently, the framework only passes + // directly for movement. Currently, the framework only passes // arrow key events, not trackball events, from one child to the next private boolean mMapTrackballToArrowKeys = true; - + public void setMapTrackballToArrowKeys(boolean setMap) { mMapTrackballToArrowKeys = setMap; } @@ -3895,26 +4069,29 @@ public class WebView extends AbsoluteLayout return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mPrivateHandler.removeMessages(SWITCH_TO_ENTER); + if (mShiftIsPressed) { + return true; // discard press if copy in progress + } mTrackballDown = true; - if (mNativeClass != 0) { - nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + if (mNativeClass == 0) { + return false; } - if (time - mLastFocusTime <= TRACKBALL_TIMEOUT - && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { - nativeSelectBestAt(mLastFocusBounds); + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + if (time - mLastCursorTime <= TRACKBALL_TIMEOUT + && !mLastCursorBounds.equals(nativeGetCursorRingBounds())) { + nativeSelectBestAt(mLastCursorBounds); } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent down ev=" + ev - + " time=" + time - + " mLastFocusTime=" + mLastFocusTime); + + " time=" + time + + " mLastCursorTime=" + mLastCursorTime); } if (isInTouchMode()) requestFocusFromTouch(); return false; // let common code in onKeyDown at it - } + } if (ev.getAction() == MotionEvent.ACTION_UP) { - // LONG_PRESS_ENTER is set in common onKeyDown - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + // LONG_PRESS_CENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mTrackballDown = false; mTrackballUpTime = time; if (mShiftIsPressed) { @@ -3923,43 +4100,39 @@ public class WebView extends AbsoluteLayout } else { mExtendSelection = true; } + return true; // discard press if copy in progress } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent up ev=" + ev - + " time=" + time + + " time=" + time ); } return false; // let common code in onKeyUp at it } if (mMapTrackballToArrowKeys && mShiftIsPressed == false) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent gmail quit"); return false; } - // no move if we're still waiting on SWITCH_TO_ENTER timeout - if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit"); - return true; - } if (mTrackballDown) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent down quit"); return true; // discard move if trackball is down } if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); return true; } // TODO: alternatively we can do panning as touch does switchOutDrawHistory(); if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "onTrackballEvent time=" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "onTrackballEvent time=" + time + " last=" + mTrackballLastTime); } mTrackballFirstTime = time; mTrackballXMove = mTrackballYMove = 0; } mTrackballLastTime = time; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time); } mTrackballRemainsX += ev.getX(); @@ -3967,7 +4140,7 @@ public class WebView extends AbsoluteLayout doTrackball(time); return true; } - + void moveSelection(float xRate, float yRate) { if (mNativeClass == 0) return; @@ -3981,8 +4154,8 @@ public class WebView extends AbsoluteLayout , mSelectX)); mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET , mSelectY)); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveSelection" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveSelection" + " mSelectX=" + mSelectX + " mSelectY=" + mSelectY + " mScrollX=" + mScrollX @@ -3991,13 +4164,13 @@ public class WebView extends AbsoluteLayout + " yRate=" + yRate ); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), mExtendSelection); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), mExtendSelection); int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET - : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET - : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; pinScrollBy(scrollX, scrollY, true, 0); Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1); @@ -4054,7 +4227,7 @@ public class WebView extends AbsoluteLayout if (elapsed == 0) { elapsed = TRACKBALL_TIMEOUT; } - float xRate = mTrackballRemainsX * 1000 / elapsed; + float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; if (mShiftIsPressed) { moveSelection(xRate, yRate); @@ -4064,7 +4237,7 @@ public class WebView extends AbsoluteLayout float ax = Math.abs(xRate); float ay = Math.abs(yRate); float maxA = Math.max(ax, ay); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball elapsed=" + elapsed + " xRate=" + xRate + " yRate=" + yRate @@ -4075,25 +4248,6 @@ public class WebView extends AbsoluteLayout int height = mContentHeight - getViewHeight(); if (width < 0) width = 0; if (height < 0) height = 0; - if (mTouchMode == SCROLL_ZOOM_OUT) { - int oldX = mZoomScrollX; - int oldY = mZoomScrollY; - int maxWH = Math.max(width, height); - mZoomScrollX += scaleTrackballX(xRate, maxWH); - mZoomScrollY += scaleTrackballY(yRate, maxWH); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT" - + " mZoomScrollX=" + mZoomScrollX - + " mZoomScrollY=" + mZoomScrollY); - } - mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX)); - mZoomScrollY = Math.min(height, Math.max(0, mZoomScrollY)); - if (oldX != mZoomScrollX || oldY != mZoomScrollY) { - invalidate(); - } - mTrackballRemainsX = mTrackballRemainsY = 0; - return; - } ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER); maxA = Math.max(ax, ay); @@ -4101,18 +4255,18 @@ public class WebView extends AbsoluteLayout int oldScrollX = mScrollX; int oldScrollY = mScrollY; if (count > 0) { - int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? - KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : + int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? + KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT; count = Math.min(count, TRACKBALL_MOVE_COUNT); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + " count=" + count + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - if (navHandledKey(selectKeyCode, count, false, time)) { + if (navHandledKey(selectKeyCode, count, false, time, false)) { playSoundEffect(keyCodeToSoundsEffect(selectKeyCode)); } mTrackballRemainsX = mTrackballRemainsY = 0; @@ -4120,12 +4274,12 @@ public class WebView extends AbsoluteLayout if (count >= TRACKBALL_SCROLL_COUNT) { int xMove = scaleTrackballX(xRate, width); int yMove = scaleTrackballY(yRate, height); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball pinScrollBy" + " count=" + count + " xMove=" + xMove + " yMove=" + yMove - + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) - + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) + + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) + + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) ); } if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) { @@ -4138,24 +4292,28 @@ public class WebView extends AbsoluteLayout pinScrollBy(xMove, yMove, true, 0); } mUserScroll = true; - } - mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS); + } + } + + private int computeMaxScrollY() { + int maxContentH = computeVerticalScrollRange() + getTitleHeight(); + return Math.max(maxContentH - getHeight(), getTitleHeight()); } public void flingScroll(int vx, int vy) { int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); - int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); - + int maxY = computeMaxScrollY(); + mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY); invalidate(); } - + private void doFling() { if (mVelocityTracker == null) { return; } int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); - int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); + int maxY = computeMaxScrollY(); mVelocityTracker.computeCurrentVelocity(1000, mMaximumFling); int vx = (int) mVelocityTracker.getXVelocity(); @@ -4168,12 +4326,40 @@ public class WebView extends AbsoluteLayout vx = 0; } } - + if (true /* EMG release: make our fling more like Maps' */) { // maps cuts their velocity in half vx = vx * 3 / 4; vy = vy * 3 / 4; } + if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) { + WebViewCore.resumeUpdate(mWebViewCore); + return; + } + float currentVelocity = mScroller.getCurrVelocity(); + if (mLastVelocity > 0 && currentVelocity > 0) { + float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX) + - Math.atan2(vy, vx))); + final float circle = (float) (Math.PI) * 2.0f; + if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) { + vx += currentVelocity * mLastVelX / mLastVelocity; + vy += currentVelocity * mLastVelY / mLastVelocity; + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy); + } + } else if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling missed " + deltaR / circle); + } + } else if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling start last=" + mLastVelocity + + " current=" + currentVelocity + + " vx=" + vx + " vy=" + vy + + " maxX=" + maxX + " maxY=" + maxY + + " mScrollX=" + mScrollX + " mScrollY=" + mScrollY); + } + mLastVelX = vx; + mLastVelY = vy; + mLastVelocity = (float) Math.hypot(vx, vy); mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); // TODO: duration is calculated based on velocity, if the range is @@ -4182,11 +4368,14 @@ public class WebView extends AbsoluteLayout // resume the webcore update. final int time = mScroller.getDuration(); mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time); + awakenScrollBars(time); invalidate(); } private boolean zoomWithPreview(float scale) { float oldScale = mActualScale; + mInitialScrollX = mScrollX; + mInitialScrollY = mScrollY; // snap to DEFAULT_SCALE if it is close if (scale > (mDefaultScale - 0.05) && scale < (mDefaultScale + 0.05)) { @@ -4201,6 +4390,9 @@ public class WebView extends AbsoluteLayout mInvInitialZoomScale = 1.0f / oldScale; mInvFinalZoomScale = 1.0f / mActualScale; mZoomScale = mActualScale; + if (!mInZoomOverview) { + mLastScale = scale; + } invalidate(); return true; } else { @@ -4229,7 +4421,7 @@ public class WebView extends AbsoluteLayout } if (mZoomControls == null) { mZoomControls = createZoomControls(); - + /* * need to be set to VISIBLE first so that getMeasuredHeight() in * {@link #onSizeChanged()} can return the measured value for proper @@ -4238,7 +4430,7 @@ public class WebView extends AbsoluteLayout mZoomControls.setVisibility(View.VISIBLE); mZoomControlRunnable = new Runnable() { public void run() { - + /* Don't dismiss the controls if the user has * focus on them. Wait and check again later. */ @@ -4276,21 +4468,13 @@ public class WebView extends AbsoluteLayout zoomOut(); } }); - zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() { - public void onClick(View v) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomScrollOut(); - } - }); return zoomControls; } /** * Gets the {@link ZoomButtonsController} which can be used to add * additional buttons to the zoom controls window. - * + * * @return The instance of {@link ZoomButtonsController} used by this class, * or null if it is unavailable. * @hide @@ -4306,7 +4490,18 @@ public class WebView extends AbsoluteLayout public boolean zoomIn() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - return zoomWithPreview(mActualScale * 1.25f); + // Center zooming to the center of the screen. + if (mInZoomOverview) { + // if in overview mode, bring it back to normal mode + mLastTouchX = getViewWidth() * .5f; + mLastTouchY = getViewHeight() * .5f; + doDoubleTap(); + return true; + } else { + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(mActualScale * 1.25f); + } } /** @@ -4316,7 +4511,18 @@ public class WebView extends AbsoluteLayout public boolean zoomOut() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - return zoomWithPreview(mActualScale * 0.8f); + float scale = mActualScale * 0.8f; + if (scale < (mMinZoomScale + 0.1f) + && mWebViewCore.getSettings().getUseWideViewPort()) { + // when zoom out to min scale, switch to overview mode + doDoubleTap(); + return true; + } else { + // Center zooming to the center of the screen. + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(scale); + } } private void updateSelection() { @@ -4324,23 +4530,91 @@ public class WebView extends AbsoluteLayout return; } // mLastTouchX and mLastTouchY are the point in the current viewport - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); Rect rect = new Rect(contentX - mNavSlop, contentY - mNavSlop, contentX + mNavSlop, contentY + mNavSlop); - // If we were already focused on a textfield, update its cache. - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } nativeSelectBestAt(rect); } + /** + * Scroll the focused text field/area to match the WebTextView + * @param xPercent New x position of the WebTextView from 0 to 1. + * @param y New y position of the WebTextView in view coordinates + */ + /*package*/ void scrollFocusedTextInput(float xPercent, int y) { + if (!inEditingMode() || mWebViewCore == null) { + return; + } + mWebViewCore.sendMessage(EventHub.SCROLL_TEXT_INPUT, + // Since this position is relative to the top of the text input + // field, we do not need to take the title bar's height into + // consideration. + viewToContentDimension(y), + new Float(xPercent)); + } + + /** + * Set our starting point and time for a drag from the WebTextView. + */ + /*package*/ void initiateTextFieldDrag(float x, float y, long eventTime) { + if (!inEditingMode()) { + return; + } + mLastTouchX = x + (float) (mWebTextView.getLeft() - mScrollX); + mLastTouchY = y + (float) (mWebTextView.getTop() - mScrollY); + mLastTouchTime = eventTime; + if (!mScroller.isFinished()) { + abortAnimation(); + mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); + } + mSnapScrollMode = SNAP_NONE; + mVelocityTracker = VelocityTracker.obtain(); + mTouchMode = TOUCH_DRAG_START_MODE; + } + + /** + * Given a motion event from the WebTextView, set its location to our + * coordinates, and handle the event. + */ + /*package*/ boolean textFieldDrag(MotionEvent event) { + if (!inEditingMode()) { + return false; + } + mDragFromTextInput = true; + event.offsetLocation((float) (mWebTextView.getLeft() - mScrollX), + (float) (mWebTextView.getTop() - mScrollY)); + boolean result = onTouchEvent(event); + mDragFromTextInput = false; + return result; + } + + /** + * Do a touch up from a WebTextView. This will be handled by webkit to + * change the selection. + * @param event MotionEvent in the WebTextView's coordinates. + */ + /*package*/ void touchUpOnTextField(MotionEvent event) { + if (!inEditingMode()) { + return; + } + int x = viewToContentX((int) event.getX() + mWebTextView.getLeft()); + int y = viewToContentY((int) event.getY() + mWebTextView.getTop()); + // In case the soft keyboard has been dismissed, bring it back up. + InputMethodManager.getInstance(getContext()).showSoftInput(mWebTextView, + 0); + if (nativeFocusNodePointer() != nativeCursorNodePointer()) { + nativeMotionUp(x, y, mNavSlop); + } + nativeTextInputMotionUp(x, y); + } + /*package*/ void shortPressOnTextField() { if (inEditingMode()) { - View v = mTextEntry; - int x = viewToContent((v.getLeft() + v.getRight()) >> 1); - int y = viewToContent((v.getTop() + v.getBottom()) >> 1); - nativeMotionUp(x, y, mNavSlop, true); + View v = mWebTextView; + int x = viewToContentX((v.getLeft() + v.getRight()) >> 1); + int y = viewToContentY((v.getTop() + v.getBottom()) >> 1); + nativeTextInputMotionUp(x, y); } } @@ -4350,31 +4624,81 @@ public class WebView extends AbsoluteLayout } switchOutDrawHistory(); // mLastTouchX and mLastTouchY are the point in the current viewport - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (nativeMotionUp(contentX, contentY, mNavSlop, true)) { + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); + if (nativeMotionUp(contentX, contentY, mNavSlop)) { if (mLogEvent) { Checkin.updateStats(mContext.getContentResolver(), Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0); } } - if (nativeUpdateFocusNode() && !mFocusNode.mIsTextField - && !mFocusNode.mIsTextArea) { + if (nativeHasCursorNode() && !nativeCursorIsTextInput()) { playSoundEffect(SoundEffectConstants.CLICK); } } + private void doDoubleTap() { + if (mWebViewCore.getSettings().getUseWideViewPort() == false) { + return; + } + mZoomCenterX = mLastTouchX; + mZoomCenterY = mLastTouchY; + mInZoomOverview = !mInZoomOverview; + // remove the zoom control after double tap + WebSettings settings = getSettings(); + if (settings.getBuiltInZoomControls()) { + if (mZoomButtonsController.isVisible()) { + mZoomButtonsController.setVisible(false); + } + } else { + if (mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + if (mZoomControls != null) { + mZoomControls.hide(); + } + } + settings.setDoubleTapToastCount(0); + if (mInZoomOverview) { + // Force the titlebar fully reveal in overview mode + if (mScrollY < getTitleHeight()) mScrollY = 0; + zoomWithPreview((float) getViewWidth() / mZoomOverviewWidth); + } else { + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); + int left = nativeGetBlockLeftEdge(contentX, contentY, mActualScale); + if (left != NO_LEFTEDGE) { + // add a 5pt padding to the left edge. Re-calculate the zoom + // center so that the new scroll x will be on the left edge. + mZoomCenterX = left < 5 ? 0 : (left - 5) * mLastScale + * mActualScale / (mLastScale - mActualScale); + } + zoomWithPreview(mLastScale); + } + } + // Called by JNI to handle a touch on a node representing an email address, // address, or phone number private void overrideLoading(String url) { mCallbackProxy.uiOverrideUrlLoading(url); } + // called by JNI + private void sendPluginState(int state) { + WebViewCore.PluginStateData psd = new WebViewCore.PluginStateData(); + psd.mFrame = nativeCursorFramePointer(); + psd.mNode = nativeCursorNodePointer(); + psd.mState = state; + mWebViewCore.sendMessage(EventHub.PLUGIN_STATE, psd); + } + @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; if (inEditingMode()) { - result = mTextEntry.requestFocus(direction, previouslyFocusedRect); + result = mWebTextView.requestFocus(direction, + previouslyFocusedRect); } else { result = super.requestFocus(direction, previouslyFocusedRect); if (mWebViewCore.getSettings().getNeedInitialFocus()) { @@ -4398,8 +4722,8 @@ public class WebView extends AbsoluteLayout default: return result; } - if (mNativeClass != 0 && !nativeUpdateFocusNode()) { - navHandledKey(fakeKeyDirection, 1, true, 0); + if (mNativeClass != 0 && !nativeHasCursorNode()) { + navHandledKey(fakeKeyDirection, 1, true, 0, true); } } } @@ -4419,8 +4743,8 @@ public class WebView extends AbsoluteLayout int measuredWidth = widthSize; // Grab the content size from WebViewCore. - int contentHeight = mContentHeight; - int contentWidth = mContentWidth; + int contentHeight = contentToViewDimension(mContentHeight); + int contentWidth = contentToViewDimension(mContentWidth); // Log.d(LOGTAG, "------- measure " + heightMode); @@ -4461,20 +4785,25 @@ public class WebView extends AbsoluteLayout rect.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); - int height = getHeight() - getHorizontalScrollbarHeight(); + int height = getViewHeightWithTitle(); int screenTop = mScrollY; int screenBottom = screenTop + height; int scrollYDelta = 0; - if (rect.bottom > screenBottom && rect.top > screenTop) { - if (rect.height() > height) { - scrollYDelta += (rect.top - screenTop); + if (rect.bottom > screenBottom) { + int oneThirdOfScreenHeight = height / 3; + if (rect.height() > 2 * oneThirdOfScreenHeight) { + // If the rectangle is too tall to fit in the bottom two thirds + // of the screen, place it at the top. + scrollYDelta = rect.top - screenTop; } else { - scrollYDelta += (rect.bottom - screenBottom); + // If the rectangle will still fit on screen, we want its + // top to be in the top third of the screen. + scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); } } else if (rect.top < screenTop) { - scrollYDelta -= (screenTop - rect.top); + scrollYDelta = rect.top - screenTop; } int width = getWidth() - getVerticalScrollbarWidth(); @@ -4499,33 +4828,40 @@ public class WebView extends AbsoluteLayout return false; } - + /* package */ void replaceTextfieldText(int oldStart, int oldEnd, String replace, int newStart, int newEnd) { - HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); - arg.put("replace", replace); - arg.put("start", new Integer(newStart)); - arg.put("end", new Integer(newEnd)); + WebViewCore.ReplaceTextData arg = new WebViewCore.ReplaceTextData(); + arg.mReplace = replace; + arg.mNewStart = newStart; + arg.mNewEnd = newEnd; mTextGeneration++; + arg.mTextGeneration = mTextGeneration; mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); } /* package */ void passToJavaScript(String currentText, KeyEvent event) { - HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); - arg.put("event", event); - arg.put("currentText", currentText); + if (nativeCursorWantsKeyEvents() && !nativeCursorMatchesFocus()) { + mWebViewCore.sendMessage(EventHub.CLICK); + if (mWebTextView.mOkayForFocusNotToMatch) { + int select = nativeFocusCandidateIsTextField() ? + nativeFocusCandidateMaxLength() : 0; + setSelection(select, select); + } + } + WebViewCore.JSKeyData arg = new WebViewCore.JSKeyData(); + arg.mEvent = event; + arg.mCurrentText = currentText; // Increase our text generation number, and pass it to webcore thread mTextGeneration++; mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg); // WebKit's document state is not saved until about to leave the page. - // To make sure the host application, like Browser, has the up to date - // document state when it goes to background, we force to save the + // To make sure the host application, like Browser, has the up to date + // document state when it goes to background, we force to save the // document state. mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE); mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE, - new WebViewCore.FocusData(mFocusData), 1000); + cursorData(), 1000); } /* package */ WebViewCore getWebViewCore() { @@ -4543,11 +4879,15 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what - > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } + if (mWebViewCore == null) { + // after WebView's destroy() is called, skip handling messages. + return; + } switch (msg.what) { case REMEMBER_PASSWORD: { mDatabase.setUsernamePassword( @@ -4564,33 +4904,40 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_SHORTPRESS: { + // if mPreventDrag is not confirmed, treat it as no so that + // it won't block panning the page. + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + mPreventDrag = PREVENT_DRAG_NO; + } if (mTouchMode == TOUCH_INIT_MODE) { mTouchMode = TOUCH_SHORTPRESS_START_MODE; updateSelection(); + } else if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { + mTouchMode = TOUCH_DONE_MODE; } break; } case SWITCH_TO_LONGPRESS: { - if (!mPreventDrag) { + if (mPreventDrag == PREVENT_DRAG_NO) { mTouchMode = TOUCH_DONE_MODE; performLongClick(); - updateTextEntry(); + rebuildWebTextView(); } break; } - case SWITCH_TO_ENTER: - if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER"); - mTouchMode = TOUCH_DONE_MODE; - onKeyUp(KeyEvent.KEYCODE_ENTER - , new KeyEvent(KeyEvent.ACTION_UP - , KeyEvent.KEYCODE_ENTER)); + case RELEASE_SINGLE_TAP: { + if (mPreventDrag == PREVENT_DRAG_NO) { + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } break; + } case SCROLL_BY_MSG_ID: setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj); break; case SYNC_SCROLL_TO_MSG_ID: if (mUserScroll) { - // if user has scrolled explicitly, don't sync the + // if user has scrolled explicitly, don't sync the // scroll position any more mUserScroll = false; break; @@ -4599,7 +4946,7 @@ public class WebView extends AbsoluteLayout case SCROLL_TO_MSG_ID: if (setContentScrollTo(msg.arg1, msg.arg2)) { // if we can't scroll to the exact position due to pin, - // send a message to WebCore to re-scroll when we get a + // send a message to WebCore to re-scroll when we get a // new picture mUserScroll = false; mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, @@ -4609,24 +4956,58 @@ public class WebView extends AbsoluteLayout case SPAWN_SCROLL_TO_MSG_ID: spawnContentScrollTo(msg.arg1, msg.arg2); break; - case NEW_PICTURE_MSG_ID: + case NEW_PICTURE_MSG_ID: { + WebSettings settings = mWebViewCore.getSettings(); // called for new content - final WebViewCore.DrawData draw = + final int viewWidth = getViewWidth(); + final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; final Point viewSize = draw.mViewPoint; - if (mZoomScale > 0) { - // use the same logic in sendViewSizeZoom() to make sure - // the mZoomScale has matched the viewSize so that we - // can clear mZoomScale - if (Math.round(getViewWidth() / mZoomScale) == viewSize.x) { - mZoomScale = 0; - mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR, - 0, 0); + boolean useWideViewport = settings.getUseWideViewPort(); + WebViewCore.RestoreState restoreState = draw.mRestoreState; + if (restoreState != null) { + mInZoomOverview = false; + mLastScale = restoreState.mTextWrapScale; + if (restoreState.mMinScale == 0) { + if (restoreState.mMobileSite) { + if (draw.mMinPrefWidth > + Math.max(0, draw.mViewPoint.x)) { + mMinZoomScale = (float) viewWidth + / draw.mMinPrefWidth; + mMinZoomScaleFixed = false; + } else { + mMinZoomScale = restoreState.mDefaultScale; + mMinZoomScaleFixed = true; + } + } else { + mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + mMinZoomScaleFixed = false; + } + } else { + mMinZoomScale = restoreState.mMinScale; + mMinZoomScaleFixed = true; } - } - if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) getViewWidth() - / Math.max(ZOOM_OUT_WIDTH, draw.mWidthHeight.x); + if (restoreState.mMaxScale == 0) { + mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + } else { + mMaxZoomScale = restoreState.mMaxScale; + } + setNewZoomScale(mLastScale, false); + setContentScrollTo(restoreState.mScrollX, + restoreState.mScrollY); + if (useWideViewport + && settings.getLoadWithOverviewMode()) { + if (restoreState.mViewScale == 0 + || (restoreState.mMobileSite + && mMinZoomScale < restoreState.mDefaultScale)) { + mInZoomOverview = true; + } + } + // As we are on a new page, remove the WebTextView. This + // is necessary for page loads driven by webkit, and in + // particular when the user was on a password field, so + // the WebTextView was visible. + clearTextEntry(); } // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to @@ -4634,125 +5015,88 @@ public class WebView extends AbsoluteLayout // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; - recordNewContentSize(draw.mWidthHeight.x, - draw.mWidthHeight.y, updateLayout); - if (LOGV_ENABLED) { + recordNewContentSize(draw.mWidthHeight.x, + draw.mWidthHeight.y + + (mFindIsUp ? mFindHeight : 0), updateLayout); + if (DebugFlags.WEB_VIEW) { Rect b = draw.mInvalRegion.getBounds(); Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + b.left+","+b.top+","+b.right+","+b.bottom+"}"); } - invalidate(contentToView(draw.mInvalRegion.getBounds())); + invalidateContentRect(draw.mInvalRegion.getBounds()); if (mPictureListener != null) { mPictureListener.onNewPicture(WebView.this, capturePicture()); } + if (useWideViewport) { + mZoomOverviewWidth = Math.max(draw.mMinPrefWidth, + draw.mViewPoint.x); + } + if (!mMinZoomScaleFixed) { + mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; + } + if (!mDrawHistory && mInZoomOverview) { + // fit the content width to the current view. Ignore + // the rounding error case. + if (Math.abs((viewWidth * mInvActualScale) + - mZoomOverviewWidth) > 1) { + setNewZoomScale((float) viewWidth + / mZoomOverviewWidth, false); + } + } break; + } case WEBCORE_INITIALIZED_MSG_ID: // nativeCreate sets mNativeClass to a non-zero value nativeCreate(msg.arg1); break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused - // and representing the same node as the pointer. - if (inEditingMode() && - mTextEntry.isSameTextField(msg.arg1)) { + // and representing the same node as the pointer. + if (inEditingMode() && + mWebTextView.isSameTextField(msg.arg1)) { if (msg.getData().getBoolean("password")) { - Spannable text = (Spannable) mTextEntry.getText(); + Spannable text = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(text); int end = Selection.getSelectionEnd(text); - mTextEntry.setInPassword(true); + mWebTextView.setInPassword(true); // Restore the selection, which may have been // ruined by setInPassword. - Spannable pword = (Spannable) mTextEntry.getText(); + Spannable pword = + (Spannable) mWebTextView.getText(); Selection.setSelection(pword, start, end); // If the text entry has created more events, ignore // this one. } else if (msg.arg2 == mTextGeneration) { - mTextEntry.setTextAndKeepSelection( + mWebTextView.setTextAndKeepSelection( (String) msg.obj); } } break; - case DID_FIRST_LAYOUT_MSG_ID: - if (mNativeClass == 0) { - break; - } -// Do not reset the focus or clear the text; the user may have already -// navigated or entered text at this point. The focus should have gotten -// reset, if need be, when the focus cache was built. Similarly, the text -// view should already be torn down and rebuilt if needed. -// nativeResetFocus(); -// clearTextEntry(); - HashMap scaleLimit = (HashMap) msg.obj; - int minScale = (Integer) scaleLimit.get("minScale"); - if (minScale == 0) { - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; - mMinZoomScaleFixed = false; - } else { - mMinZoomScale = (float) (minScale / 100.0); - mMinZoomScaleFixed = true; - } - int maxScale = (Integer) scaleLimit.get("maxScale"); - if (maxScale == 0) { - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - } else { - mMaxZoomScale = (float) (maxScale / 100.0); - } - // If history Picture is drawn, don't update zoomWidth - if (mDrawHistory) { - break; + case UPDATE_TEXT_SELECTION_MSG_ID: + if (inEditingMode() + && mWebTextView.isSameTextField(msg.arg1) + && msg.arg2 == mTextGeneration) { + WebViewCore.TextSelectionData tData + = (WebViewCore.TextSelectionData) msg.obj; + mWebTextView.setSelectionFromWebKit(tData.mStart, + tData.mEnd); } - int width = getViewWidth(); - if (width == 0) { - break; - } - int initialScale = msg.arg1; - int viewportWidth = msg.arg2; - // start a new page with DEFAULT_SCALE zoom scale. - float scale = mDefaultScale; - if (mInitialScale > 0) { - scale = mInitialScale / 100.0f; - } else { - if (mWebViewCore.getSettings().getUseWideViewPort()) { - // force viewSizeChanged by setting mLastWidthSent - // to 0 - mLastWidthSent = 0; - } - if (initialScale == 0) { - // if viewportWidth is defined and it is smaller - // than the view width, zoom in to fill the view - if (viewportWidth > 0 && viewportWidth < width) { - scale = (float) width / viewportWidth; - } - } else { - scale = initialScale / 100.0f; - } - } - setNewZoomScale(scale, false); break; - case MARK_NODE_INVALID_ID: - nativeMarkNodeInvalid(msg.arg1); - break; - case NOTIFY_FOCUS_SET_MSG_ID: - if (mNativeClass != 0) { - nativeNotifyFocusSet(inEditingMode()); + case MOVE_OUT_OF_PLUGIN: + if (nativePluginEatsNavKey()) { + navHandledKey(msg.arg1, 1, false, 0, true); } break; case UPDATE_TEXT_ENTRY_MSG_ID: - // this is sent after finishing resize in WebViewCore. Make + // this is sent after finishing resize in WebViewCore. Make // sure the text edit box is still on the screen. - boolean alreadyThere = inEditingMode(); - if (alreadyThere && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { - mTextEntry.bringIntoView(); - } + if (inEditingMode() && nativeCursorIsTextInput()) { + mWebTextView.bringIntoView(); + rebuildWebTextView(); } - updateTextEntry(); break; - case RECOMPUTE_FOCUS_MSG_ID: - if (mNativeClass != 0) { - nativeRecomputeFocus(); - } + case CLEAR_TEXT_ENTRY: + clearTextEntry(); break; case INVAL_RECT_MSG_ID: { Rect r = (Rect)msg.obj; @@ -4765,17 +5109,15 @@ public class WebView extends AbsoluteLayout } break; } - case UPDATE_TEXT_ENTRY_ADAPTER: - HashMap data = (HashMap) msg.obj; - if (mTextEntry.isSameTextField(msg.arg1)) { - AutoCompleteAdapter adapter = - (AutoCompleteAdapter) data.get("adapter"); - mTextEntry.setAdapterCustom(adapter); + case REQUEST_FORM_DATA: + AutoCompleteAdapter adapter = (AutoCompleteAdapter) msg.obj; + if (mWebTextView.isSameTextField(msg.arg1)) { + mWebTextView.setAdapterCustom(adapter); } break; case UPDATE_CLIPBOARD: String str = (String) msg.obj; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str); } try { @@ -4790,12 +5132,12 @@ public class WebView extends AbsoluteLayout WebViewCore.resumeUpdate(mWebViewCore); break; - case LONG_PRESS_ENTER: + case LONG_PRESS_CENTER: // as this is shared by keydown and trackballdown, reset all // the states - mGotEnterDown = false; + mGotCenterDown = false; mTrackballDown = false; - // LONG_PRESS_ENTER is sent as a delayed message. If we + // LONG_PRESS_CENTER is sent as a delayed message. If we // switch to windows overview, the WebView will be // temporarily removed from the view system. In that case, // do nothing. @@ -4810,13 +5152,26 @@ public class WebView extends AbsoluteLayout case PREVENT_TOUCH_ID: if (msg.arg1 == MotionEvent.ACTION_DOWN) { - mPreventDrag = msg.arg2 == 1; - if (mPreventDrag) { - mTouchMode = TOUCH_DONE_MODE; + // dont override if mPreventDrag has been set to no due + // to time out + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + mPreventDrag = msg.arg2 == 1 ? PREVENT_DRAG_YES + : PREVENT_DRAG_NO; + if (mPreventDrag == PREVENT_DRAG_YES) { + mTouchMode = TOUCH_DONE_MODE; + } } } break; + case REQUEST_KEYBOARD: + if (msg.arg1 == 0) { + hideSoftKeyboard(); + } else { + displaySoftKeyboard(false); + } + break; + default: super.handleMessage(msg); break; @@ -4826,16 +5181,12 @@ public class WebView extends AbsoluteLayout // Class used to use a dropdown for a